Golang中结构体成员的函数指针

huangapple go评论89阅读模式
英文:

Golang func pointer of a struct member

问题

给定以下类型:

type A struct {
    ...
}

func (a *A) Process() {
    ...
}

我想将类型A的方法Process传递给另一个函数,并能够访问A的底层实例的内容。

我应该如何将该方法传递给另一个函数?通过指针吗?如何调用它?

Process()方法不会修改A的实例,我在方法接收器上使用指针,因为结构体相当大。我提出这个问题的想法是为了避免在结构体外部声明函数Process()并传递大量参数给它(而是访问结构体的成员)。

英文:

Given the following types :

type A struct {
    ...
}

func (a *A) Process() {
    ...
}

I would like to pass the method Process of the type A to another function and be able to access the content of the underlying instance of A.

How should I pass the method to another function? Via a pointer? And how should it be called ?

The Process() method won't modify the instance of A, I am using a pointer on the method receiver because the struct is quite large. The idea behind my question is to avoid declaring the function Process() outside the struct and pass a ton of arguments to it (instead it access to the members of the struct).

答案1

得分: 8

你甚至可以直接这样做,不需要界面:

package main

import "fmt"

type A struct {
    Name string
}

func (a *A) Go() {
    fmt.Printf("GO: %v\n", a)
}

func Test(fn func()) {
    fn()
}

func main() {
    aa := &A{Name: "FOO"}
    bb := (*A)(nil)
    cc := &A{}
    Test(aa.Go)
    Test(bb.Go)
    Test(cc.Go)
}

输出结果:

GO: &{FOO}
GO: <nil>
GO: &{}

在 playground 上查看:https://play.golang.org/p/V-q2_zwX8h

英文:

You can even do it directly, without an interface:

package main

import &quot;fmt&quot;

type A struct {
	Name string
}

func (a *A) Go() {
	fmt.Printf(&quot;GO: %v\n&quot;, a)
}

func Test(fn func()) {
	fn()
}

func main() {
	aa := &amp;A{Name: &quot;FOO&quot;}
	bb := (*A)(nil)
	cc := &amp;A{}
	Test(aa.Go)
	Test(bb.Go)
	Test(cc.Go)
}

Output:

GO: &amp;{FOO}
GO: &lt;nil&gt;
GO: &amp;{}

On the playground: https://play.golang.org/p/V-q2_zwX8h

答案2

得分: 7

另一种选择是将func定义为一种类型:

type Process func(a *A)

然后在调用其他函数时将其作为参数使用:

func Test(p Process)

需要记住的一件事是,这些定义实际上是完全相同的:

  • func (a *A) Process() { a.MyVar }
  • func Process(a *A) { a.MyVar }

指针接收器只是一个具有指向结构体指针的局部变量的函数。相反,值接收器是一个具有指向结构体值副本的局部变量的函数。

为什么不像选项1那样在结构体上使用方法呢?有很多原因。像选项1那样将相关方法分组到结构体本身上似乎很直观(特别是如果来自其他面向对象的语言,如Java或.NET,在那里通常将大量方法放在单个结构体上)。但是,由于你提到struct本身相当大,这可能意味着它太大了,需要进行拆分。

就我个人而言,在使用上述的选项2时,我遵循以下规则:

  • 如果func没有使用整个结构体的属性(例如,它只对数据的子集进行操作,甚至根本不操作),我会使用带有指针的选项2(或者,使用一个零字节结构体的接口本身)。

这样可以更容易地进行单元测试,通过拆分我所说的“相当大”的结构体,只模拟我需要支持该方法的功能接口。

现在,函数定义本身也是类型。到目前为止,我们有了这个类型:

func(a *A)

这可以作为输入传递给另一个方法,如你所要求的,像这样:

func AnotherFunc(fn func(a *A)) {
  a := &A{}
  fn(a)
}

但对我来说,这使得事情变得有点难以阅读,更不用说脆弱了-有人可能会更改那里的函数定义并破坏其他地方的东西。

这就是我更喜欢定义一种类型的地方:

type Process func(a *A)

这样,我可以这样使用它:

func AnotherFunc(p Process) {
  a := &A{}
  p(a)
}

这允许你将p作为指向函数的指针访问,并根据需要传递它。(注意,你不必访问p的实际指针。也就是说,不要这样做&p,因为在Golang中,func类型像slicesmaps一样是按引用传递的。)

总的来说,当你想将逻辑分解为更小的可管理(且更易于测试)的部分时,通常会遵循这种模式-通过使用更小、更易于管理的AnotherFunc()方法来导出和单元测试API合同,同时隐藏内部细节。

工作示例

http://play.golang.org/p/RAJ2t0nWEc

package main

import "fmt"

type Process func(a *A)

type A struct {
  MyVar string
}

func processA(a *A) {
  fmt.Println(a.MyVar)
}

func AnotherFunc(a *A, p Process) {
  p(a)
}

func main() {
  
  a := &A{
    MyVar: "I'm here!",
  }
  
  AnotherFunc(a, processA)
}

单元测试

将函数类型的概念提升到另一个级别,可以简化单元测试。

你可以为Process()函数定义全局变量:

var Process = func(a *A)

它仍然可以以完全相同的方式使用:

func Test(p Process)

现在的区别是,在单元测试期间,你可以覆盖该函数:

package mypackage_test

import "testing"

func TestProcessHasError(t *testing.T) {
    // 保留原始定义的备份副本
    originalFunctionality := Process

    // 现在,覆盖它
    Process = func(a *A) error {
        // 在这里执行不同的操作,比如返回错误
        return errors.New("force it to error")
    }

    // 正常调用你的Test函数,使用它
    err := Test(Process)

    // 检查错误
    assert.Error(err)

    // 做一个好的测试人员,并将Process恢复到正常状态
    Process = originalFunctionality 
}

当我接手一个现有的代码库时,这些是我开始实施的一些技巧,以帮助将应用程序与自身解耦-并允许更多的测试。

英文:

Another option would be to define the func as a type:

type Process func(a *A)

Then use it as a parameter when calling your other func:

func Test(p Process)

One thing to remember is that these definitions are exactly the same thing:

  • func (a *A) Process() { a.MyVar }
  • func Process(a *A) { a.MyVar }

A pointer receiver is just a func with a local variable of a pointer to the struct. And conversely, a value receiver is a func with a local variable to a value copy of the struct.

Why would you not use a method on the struct, like option 1? There are many reasons. It does seem intuitive to group related methods onto the struct itself like option 1 (especially if coming from other OOP languages like Java or .NET where you normally stick upteen-thousand methods on a single struct). But, since you stated that the struct itself is quite large, this smells of SoC (that it is too large) and may need to be broken up.

Personally, the rule I follow when using option 2 above is:

  • If the func is not using the entire struct's properties (e.g. it is only operating on a sub-set of data, or even none at all), I instead use option 2 with a pointer. (or, use an interface itself with a zero-byte struct)

This allows for much easier unit testing by breaking up my struct that is "quite large" as you say, allowing me to mock up only the interface of functionality I need to support that method.

Now, func definitions are, by definitions, types themselves. So far we have this type:

func(a *A)

This can be used as an input into another method, as you asked for, like so:

func AnotherFunc(fn func(a *A)) {
  a := &amp;A{}
  fn(a)
}

But to me, this makes things a bit hard to read not to mention brittle - someone could change the func definition there and break other things elsewhere.

This is where I prefer to define a type:

type Process func(a *A)

That way, I can consume it like:

func AnotherFunc(p Process) {
  a := &amp;A{}
  p(a)
}

This allows you to access p as your pointer to the func, to pass around as you like. (Note though, you don't have to access the actual pointer of p. IOW, don't do this &amp;p because func types are passed by reference in Golang anyways, just like slices and maps.)

Overall, you typically follow this kind of pattern when you want to break up your logic into smaller manageable (and more testable) pieces - by using smaller, more manageable AnotherFunc() methods to export and unit test an API contract for, while hiding the internals.

Working Example

http://play.golang.org/p/RAJ2t0nWEc

package main

import &quot;fmt&quot;

type Process func(a *A)

type A struct {
  MyVar string
}

func processA(a *A) {
  fmt.Println(a.MyVar)
}

func AnotherFunc(a *A, p Process) {
  p(a)
}

func main() {
	
	a := &amp;A{
		MyVar: &quot;I&#39;m here!&quot;,
	}
	
	AnotherFunc(a, processA)
}

Unit Testing

Taking the concept of func types to another level, would be to ease unit testing.

You can define global variables for your Process() function:

var Process = func(a *A)

It would continue to be used the exact same way:

 func Test(p Process)

The difference now is during unit testing, you can override the function:

package mypackage_test

import &quot;testing&quot;

func TestProcessHasError(t *testing.T) {
    // keep a backup copy of original definition
    originalFunctionality := Process

    // now, override it
    Process = func(a *A) error {
        // do something different here, like return error
        return errors.New(&quot;force it to error&quot;)
    }

    // call your Test func normally, using it
    err := Test(Process)

    // check for error
    assert.Error(err)

    // be a good tester and restore Process back to normal
    Process = originalFunctionality 
}

When I get my hands on an existing codebase, these are some of the tricks I start implementing to help decouple the application from itself - and allow for more testing.

答案3

得分: 3

你可以通过接口来实现这个:

type Processor interface {
    Process()
}

func anotherFunction(p Processor) {
    p.Process()
}

...
var a A
anotherFunction(a)
英文:

You can achieve this with an interface:

type Processor interface {
    Process()
}

func anotherFunction(p Processor) {
    p.Process()
}

...
var a A
anotherFunction(a)

huangapple
  • 本文由 发表于 2016年4月1日 10:20:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/36346972.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定