对于指针接收器和值接收器方法的一些拙见

本文主要解决了下文代码中

m := inputType.Method(i)

在使用指针接收器(pointer receiver)

func (this *User) Call() {
    fmt.Print("user is called ..")
    fmt.Printf("%v\n", this)
}

定义方法时Method()无预期输出的问题😫😫😫,详见代码如下:

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Id   int
    Name string
    Age  int
}

func (this *User) Call() {  //改成func (this User) Call()即可被下文inputType.Method(i)实现
    fmt.Print("user is called ..")
    fmt.Printf("%v\n", this)
}

func main() {
    user := User{1, "Aceld", 18}
    user.Call()  //可寻址值变量(左值) 上调用指针接收器方法,Golang 会自动隐式的为变量取地址后调用方法,Golang 的语法糖让我们不用再啰嗦的显式取地址。同理,当指针类型调用值接收器方法时,Golang 也会通过指针找到值类型,在值类型上调用方法。此处如果写成 User{1, "Aceld", 18}.Call() 会报错,因为 User{1, "Aceld", 18} 不是左值。
    DoFiledAndMethod(user)
}

func DoFiledAndMethod(input interface{}) {
    //获取input的type
    inputType := reflect.TypeOf(input)
    fmt.Println("inputType is :", inputType.Name())

    //获取input的value
    inputValue := reflect.ValueOf(input)
    fmt.Println("inputValue is:", inputValue)

    //通过type 获取里面的字段
    //1. 获取interface的reflect.Type,通过Type得到NumField ,进行遍历
    //2. 得到每个field,数据类型
    //3. 通过filed有一个Interface()方法等到 对应的value
    for i := 0; i < inputType.NumField(); i++ {
       field := inputType.Field(i)
       value := inputValue.Field(i).Interface()

       fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
    }

    //通过type 获取里面的方法,调用
    for i := 0; i < inputType.NumMethod(); i++ {
       m := inputType.Method(i)
       fmt.Printf("%s: %v\n", m.Name, m.Type)
    }

}

在Go语言中,方法是否可以被反射(通过reflect包)获取取决于它们是如何定义的。当使用指针接收器(pointer receiver)定义一个方法时,该方法仅在指针类型上存在,而不是在值类型上。相反,当使用值接收器(value receiver)定义方法时,该方法将在值类型和指针类型上均可用。

让我们分析提供的两种情况:

  1. 使用指针接收器的方法:
func (this *User) Call() {
    fmt.Print("user is called ..")
    fmt.Printf("%v\n", this)
}

在这种情况下,Call方法是一个指针方法。这意味着它只能被*User类型的变量调用,而不能被User类型的变量调用。当使用指针接收器定义方法时,Go语言会在运行时为该方法创建一个新的函数,该函数接受一个指向接收者类型的指针作为第一个参数。因此,当尝试通过值类型的变量调用这个方法时,它不会出现在方法的列表中。

  1. 使用值接收器的方法:
func (this User) Call() {
    fmt.Print("user is called ..")
    fmt.Printf("%v\n", this)
}

在这种情况下,Call方法是一个值方法。它可以被User类型的变量直接调用。当使用值接收器定义方法时,Go语言会为该方法创建一个新的函数,该函数接受接收者类型的值作为第一个参数。因此,当通过值类型的变量调用方法时,这个方法会出现在方法的列表中。

DoFiledAndMethod函数中,是通过一个interface{}类型的变量来获取方法的。如果interface{}变量包含的是一个User类型的值,那么使用值接收器定义的方法或者指针接收器定义的方法都会被反射包检测到。如果interface{}变量包含的是一个*User类型的指针,那么只有使用指针接收器定义的方法才会被检测到。😋😋😋 

为了通过反射获取所有定义在User类型上的方法(无论它们是使用值接收器还是指针接收器定义的),需要在DoFiledAndMethod函数中处理这两种情况。可以通过检查inputTypeKind来确定它是否是一个指针,并相应地获取方法。下面是一个修改后的DoFiledAndMethod函数,它可以处理这两种情况:

func DoFiledAndMethod(input interface{}) {
    // 获取input的type
    inputType := reflect.TypeOf(input)
    fmt.Println("inputType is :", inputType.Name())

    // 获取input的value
    inputValue := reflect.ValueOf(input)
    fmt.Println("inputValue is:", inputValue)

    // 获取字段
    for i := 0; i < inputType.NumField(); i++ {
        field := inputType.Field(i)
        value := inputValue.Field(i).Interface()
        fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
    }

    // 获取方法
    var userType reflect.Type
    if inputType.Kind() == reflect.Ptr {
        // 如果input是一个指针,获取它指向的实际类型
        userType = inputType.Elem()
    } else {
        // 否则,直接使用input的类型
        userType = inputType
    }

    for i := 0; i < userType.NumMethod(); i++ {
        m := userType.Method(i)
        fmt.Printf("%s: %v\n", m.Name, m.Type)
    }
}

在这个修改后的函数中,我们检查了inputTypeKind是否为reflect.Ptr,如果是,则使用inputType.Elem()来获取指针指向的实际类型。然后,我们在这个实际类型上调用NumMethodMethod来获取和打印方法。这样,无论input是一个User类型的值还是一个*User类型的指针,函数都会正确地打印出定义在User类型上的所有方法。ヾ(≧▽≦*)o

那究竟怎么选择是指针接收器还是值接收器呢?

  • 何时使用值类型

(1)如果接收器是一个 map,func 或者 chan,使用值类型(因为它们本身就是引用类型)。 (2)如果接收器是一个 slice,并且方法不执行 reslice 操作,也不重新分配内存给 slice,使用值类型。 (3)如果接收器是一个小的数组或者原生的值类型结构体类型(比如 time.Time 类型),而且没有可修改的字段和指针,又或者接收器是一个简单地基本类型像是 int 和 string,使用值类型就好了。

值类型的接收器可以减少一定数量的内存垃圾生成,值类型接收器一般会在栈上分配到内存(但也不一定),在没搞明白代码想干什么之前,别为这个原因而选择值类型接收器。

  • 何时使用指针类型

(1)如果方法需要修改接收器里的数据,则接收器必须是指针类型。 (2)如果接收器是一个包含了 sync.Mutex 或者类似同步字段的结构体,接收器必须是指针,这样可以避免拷贝。 (3)如果接收器是一个大的结构体或者数组,那么指针类型接收器更有效率。 (4)如果接收器是一个结构体,数组或者 slice,它们中任意一个元素是指针类型而且可能被修改,建议使用指针类型接收器,这样会增加程序的可读性。

无论你声明方法的接收器是指针接收器还是值接收器,Go都可以帮你隐式转换为正确的方法使用。

只需要记住,值类型不能调用指针接收器方法

**也即:值接收器方法(value methods)可以通过指针和值调用,但是指针接收器方法(pointer methods)只能通过指针来调用。**但有一个例外,如果某个值是可寻址的(addressable,或者说左值),那么编译器会在值调用指针方法时自动插入取地址符,使得在此情形下看起来像指针方法也可以通过值来调用(语法糖)。

从逻辑上理解为什么 “值类型不能调用指针接收器方法”

指针接收器方法,很可能在方法中会对调用者的属性进行更改操作,从而影响接收器;而对于值接收器方法,在方法中不会对接收器本身产生影响。

指针接收器方法,很可能在方法中会对调用者的属性进行更改操作,从而影响接收器;而对于值接收器方法,在方法中不会对接收器本身产生影响。所以,当实现了一个值接收器方法,就可以自动生成一个指针接收器方法,因为两者都不会影响接收器。但是,当实现了一个指针接收器方法,如果此时自动生成一个值接收器方法,原本期望对接收器的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响接收器。

如果实现了值接收器方法,会隐含地也实现了指针接收器方法。

最后如果实在还是不知道该使用哪种接收器,那么记住使用指针接收器是最靠谱的。

e.g.

1.值接收器&&值调用,ok

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func (this User) Call() {
	fmt.Print("user is called ..")
	fmt.Printf("%v\n", this)
}

func main() {
	user := User{1, "Aceld", 18}
	user.Call()
	DoFiledAndMethod(user)
}

func DoFiledAndMethod(input interface{}) {
	//获取input的type
	inputType := reflect.TypeOf(input)
	//通过type 获取里面的方法,调用
	for i := 0; i < inputType.NumMethod(); i++ {
		m := inputType.Method(i)
		fmt.Printf("%v %s: %v\n", i, m.Name, m.Type)
	}

}

2.值接收器&&指针调用,ok(如果实现了值接收器方法,会隐含地也实现了指针接收器方法。

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func (this User) Call() {
	fmt.Print("user is called ..")
	fmt.Printf("%v\n", this)
}

func main() {
	user := &User{1, "Aceld", 18}
	user.Call()
	DoFiledAndMethod(user)
}

func DoFiledAndMethod(input interface{}) {
	//获取input的type
	inputType := reflect.TypeOf(input)
	//通过type 获取里面的方法,调用
	for i := 0; i < inputType.NumMethod(); i++ {
		m := inputType.Method(i)
		fmt.Printf("%v %s: %v\n", i, m.Name, m.Type)
	}

}

3.指针接收器&&值调用,no!!!

/**当你使用 `reflect.TypeOf(user)` 来获取 `user` 变量的类型时,你得到的是 `User` 类型,而不是 `*User` 类型。由于 `Call` 方法现在是一个指针接收器方法,它只存在于 `*User` 类型上,而不是 `User` 类型上。
这就是为什么在 `DoFiledAndMethod` 函数中找不到 `Call` 方法的原因。`DoFiledAndMethod` 函数正在查看 `User` 类型的方法,而不是 `*User` 类型的方法。
**/
package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func (this *User) Call() {
	fmt.Print("user is called ..")
	fmt.Printf("%v\n", this)
}

func main() {
	user := User{1, "Aceld", 18}
	user.Call()
	DoFiledAndMethod(user)
}

func DoFiledAndMethod(input interface{}) {
	//获取input的type
	inputType := reflect.TypeOf(input)
	//通过type 获取里面的方法,调用
	for i := 0; i < inputType.NumMethod(); i++ {
		m := inputType.Method(i)
		fmt.Printf("%v %s: %v\n", i, m.Name, m.Type)
	}

}

4.指针接收器&&指针调用,ok

package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id   int
	Name string
	Age  int
}

func (this User) Call() {
	fmt.Print("user is called ..")
	fmt.Printf("%v\n", this)
}

func main() {
	user := User{1, "Aceld", 18}
	user.Call()
	DoFiledAndMethod(user)
}

func DoFiledAndMethod(input interface{}) {
	//获取input的type
	inputType := reflect.TypeOf(input)
	//通过type 获取里面的方法,调用
	for i := 0; i < inputType.NumMethod(); i++ {
		m := inputType.Method(i)
		fmt.Printf("%v %s: %v\n", i, m.Name, m.Type)
	}

}