24|方法:理解“方法”的本质

你好,我是Tony Bai。

在前面的几讲中,我们对Go函数做了一个全面系统的学习。我们知道,函数是Go代码中的基本功能逻辑单元,它承载了Go程序的所有执行逻辑。可以说,Go程序的执行流本质上就是在函数调用栈中上下流动,从一个函数到另一个函数

讲到这里,如果你做过提前预习,你可能要站出来反驳我了:“老师,你的说法太过绝对了,Go语言还有一种语法元素,方法(method),它也可以承载代码逻辑,程序也可以从一个方法流动到另外一个方法”。

别急!我这么说自然有我的道理,等会儿你就知道了。从这节课开始,我们会花三节课的时间,系统讲解Go语言中的方法。我们将围绕方法的本质、方法receiver的类型选择、方法集合,以及如何实现方法的“继承”这几个主题,进行讲解。

那么,在这一节课中,我就先来解答我们开头提到的这个问题,看看Go语言中的方法究竟是什么。等你掌握了方法的本质后,再来评判我的说法是否正确也不迟。

认识Go方法

我们知道,Go语言从设计伊始,就不支持经典的面向对象语法元素,比如类、对象、继承,等等,但Go语言仍保留了名为“方法(method)”的语法元素。当然,Go语言中的方法和面向对象中的方法并不是一样的。Go引入方法这一元素,并不是要支持面向对象编程范式,而是Go践行组合设计哲学的一种实现层面的需要。这个我们后面课程会展开细讲,这里你先了解一下就可以了。

简单了解之后,我们就以Go标准库net/http包中*Server类型的方法ListenAndServeTLS为例,讲解一下Go方法的一般形式:

Go中方法的声明和函数的声明有很多相似之处,我们可以参照着来学习。比如,Go的方法也是以func关键字修饰的,并且和函数一样,也包含方法名(对应函数名)、参数列表、返回值列表与方法体(对应函数体)。

而且,方法中的这几个部分和函数声明中对应的部分,在形式与语义方面都是一致的,比如:方法名字首字母大小写决定该方法是否是导出方法;方法参数列表支持变长参数;方法的返回值列表也支持具名返回值等。

不过,它们也有不同的地方。从上面这张图我们可以看到,和由五个部分组成的函数声明不同,Go方法的声明有六个组成部分,多的一个就是图中的receiver部分。在receiver部分声明的参数,Go称之为receiver参数,这个receiver参数也是方法与类型之间的纽带,也是方法与函数的最大不同。

接下来我们就重点说说这部分声明的receiver参数。

Go中的方法必须是归属于一个类型的,而receiver参数的类型就是这个方法归属的类型,或者说这个方法就是这个类型的一个方法。我们以上图中的ListenAndServeTLS为例,这里的receiver参数srv的类型为*Server,那么我们可以说,这个方法就是*Server类型的方法,

注意!这里我说的是ListenAndServeTLS是*Server类型的方法,而不是Server类型的方法。具体的原因,我们在后面课程还会细讲,这里你先有这个认知就好了。

为了方便讲解,我们将上面例子中的方法声明,转换为一个方法的一般声明形式:

func (t *T或T) MethodName(参数列表) (返回值列表) {
    // 方法体
}

无论receiver参数的类型为*T还是T,我们都把一般声明形式中的T叫做receiver参数t的基类型。如果t的类型为T,那么说这个方法是类型T的一个方法;如果t的类型为*T,那么就说这个方法是类型*T的一个方法。而且,要注意的是,每个方法只能有一个receiver参数,Go不支持在方法的receiver部分放置包含多个receiver参数的参数列表,或者变长receiver参数。

那么,receiver参数的作用域是什么呢?

你还记得我们在第11讲中提到过的、关于函数/方法作用域的结论吗?我们这里再复习一下:方法接收器(receiver)参数、函数/方法参数,以及返回值变量对应的作用域范围,都是函数/方法体对应的显式代码块

这就意味着,receiver部分的参数名不能与方法参数列表中的形参名,以及具名返回值中的变量名存在冲突,必须在这个方法的作用域中具有唯一性。如果这个不唯一不存在,比如像下面例子中那样,Go编译器就会报错:

type T struct{}

func (t T) M(t string) { // 编译器报错:duplicate argument t (重复声明参数t)
    ... ...
}

不过,如果在方法体中,我们没有用到receiver参数,我们也可以省略receiver的参数名,就像下面这样:

type T struct{}

func (T) M(t string) { 
    ... ...
}

仅当方法体中的实现不需要receiver参数参与时,我们才会省略receiver参数名,不过这一情况很少使用,你了解一下就好了。

除了receiver参数名字要保证唯一外,Go语言对receiver参数的基类型也有约束,那就是receiver参数的基类型本身不能为指针类型或接口类型。下面的例子分别演示了基类型为指针类型和接口类型时,Go编译器报错的情况:

type MyInt *int
func (r MyInt) String() string { // r的基类型为MyInt,编译器报错:invalid receiver type MyInt (MyInt is a pointer type)
    return fmt.Sprintf("%d", *(*int)(r))
}

type MyReader io.Reader
func (r MyReader) Read(p []byte) (int, error) { // r的基类型为MyReader,编译器报错:invalid receiver type MyReader (MyReader is an interface type)
    return r.Read(p)
}

最后,Go对方法声明的位置也是有约束的,Go要求,方法声明要与receiver参数的基类型声明放在同一个包内。基于这个约束,我们还可以得到两个推论。

  • 第一个推论:我们不能为原生类型(诸如int、float64、map等)添加方法
    比如,下面的代码试图为Go原生类型int增加新方法Foo,这样做,Go编译器会报错:
func (i int) Foo() string { // 编译器报错:cannot define new methods on non-local type int
    return fmt.Sprintf("%d", i) 
}
  • 第二个推论:不能跨越Go包为其他包的类型声明新方法
    比如,下面的代码试图跨越包边界,为Go标准库中的http.Server类型添加新方法Foo,这样做,Go编译器同样会报错:
import "net/http"

func (s http.Server) Foo() { // 编译器报错:cannot define new methods on non-local type http.Server
}

到这里,我们已经基本了解了Go方法的声明形式以及对receiver参数的相关约束。有了这些基础后,我们就可以看一下如何使用这些方法(method)。

我们直接还是通过一个例子理解一下。如果receiver参数的基类型为T,那么我们说receiver参数绑定在T上,我们可以通过*T或T的变量实例调用该方法:

type T struct{}

func (t T) M(n int) {
}

func main() {
    var t T
    t.M(1) // 通过类型T的变量实例调用方法M

    p := &T{}
    p.M(2) // 通过类型*T的变量实例调用方法M
}

不过,看到这里你可能会问,这段代码中,方法M是类型T的方法,那为什么通过*T类型变量也可以调用M方法呢?关于这个问题,我会在下一讲中告诉你原因,这里你先了解方法的调用方式就好了。

从上面这些分析中,我们也可以看到,和其他主流编程语言相比,Go语言的方法,只比函数多出了一个receiver参数,这就大大降低了Gopher们学习方法这一语法元素的门槛。

但即便如此,你在使用方法时可能仍然会有一些疑惑,比如,方法的类型是什么?我们是否可以将方法赋值给函数类型的变量?调用方法时方法对receiver参数的修改是不是外部可见的?要想解除你心中这些疑惑,我们就必须深入到方法的本质层面。

接下来我们就来看看本质上Go方法究竟是什么。

方法的本质是什么?

通过前面的学习,我们知道了Go的方法与Go中的类型是通过receiver联系在一起,我们可以为任何非内置原生类型定义方法,比如下面的类型T:

type T struct { 
    a int
}

func (t T) Get() int {  
    return t.a 
}

func (t *T) Set(a int) int { 
    t.a = a 
    return t.a 
}

我们可以和典型的面向对象语言C++做下对比。如果你了解C++语言,尤其是看过C++大牛、《C++ Primer》作者Stanley B·Lippman的大作《深入探索C++对象模型》,你大约会知道,C++中的对象在调用方法时,编译器会自动传入指向对象自身的this指针作为方法的第一个参数。

而Go方法中的原理也是相似的,只不过我们是将receiver参数以第一个参数的身份并入到方法的参数列表中。按照这个原理,我们示例中的类型T和*T的方法,就可以分别等价转换为下面的普通函数:

// 类型T的方法Get的等价函数
func Get(t T) int {  
    return t.a 
}

// 类型*T的方法Set的等价函数
func Set(t *T, a int) int { 
    t.a = a 
    return t.a 
}

这种等价转换后的函数的类型就是方法的类型。只不过在Go语言中,这种等价转换是由Go编译器在编译和生成代码时自动完成的。Go语言规范中还提供了方法表达式(Method Expression)的概念,可以让我们更充分地理解上面的等价转换,我们来看一下。

我们还以上面类型T以及它的方法为例,结合前面说过的Go方法的调用方式,我们可以得到下面代码:

var t T
t.Get()
(&t).Set(1)

我们可以用另一种方式,把上面的方法调用做一个等价替换:

var t T
T.Get(t)
(*T).Set(&t, 1)

这种直接以类型名T调用方法的表达方式,被称为 Method Expression。通过Method Expression这种形式,类型T只能调用T的方法集合(Method Set)中的方法,同理类型*T也只能调用*T的方法集合中的方法。关于方法集合,我们会在下一讲中详细讲解。

我们看到,Method Expression 有些类似于C++中的静态方法(Static Method),C++中的静态方法在使用时,以该C++类的某个对象实例作为第一个参数,而Go语言的Method Expression在使用时,同样以receiver参数所代表的类型实例作为第一个参数。

这种通过Method Expression对方法进行调用的方式,与我们之前所做的方法到函数的等价转换是如出一辙的。所以,Go语言中的方法的本质就是,一个以方法的receiver参数作为第一个参数的普通函数

而且,Method Expression就是Go方法本质的最好体现,因为方法自身的类型就是一个普通函数的类型,我们甚至可以将它作为右值,赋值给一个函数类型的变量,比如下面示例:

func main() {
    var t T
    f1 := (*T).Set // f1的类型,也是*T类型Set方法的类型:func (t *T, int)int
    f2 := T.Get    // f2的类型,也是T类型Get方法的类型:func(t T)int
    fmt.Printf("the type of f1 is %T\n", f1) // the type of f1 is func(*main.T, int) int
    fmt.Printf("the type of f2 is %T\n", f2) // the type of f2 is func(main.T) int
    f1(&t, 3)
    fmt.Println(f2(t)) // 3
}

既然方法本质上也是函数,那么我们在这节课开头的争论也就有了答案,这已经能够证明我的说法是正确的。但看到这里,你可能会问:我知道方法的本质是函数又怎么样呢?它对我在实际编码工作有什么帮助吗?

下面我们就以一个实际例子来看看,如何基于对方法本质的深入理解,来分析解决实际编码工作中遇到的真实问题。

巧解难题

这个例子是来自于我个人博客的一次真实的读者咨询,他的问题代码是这样的:

package main

import (
    "fmt"
    "time"
)

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go v.print()
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}

这段代码在我的多核macOS上的运行结果是这样(由于Goroutine调度顺序不同,你自己的运行结果中的行序可能与下面的有差异):

one
two
three
six
six
six

这位读者的问题显然是:为什么对data2迭代输出的结果是三个“six”,而不是four、five、six?

那我们就来分析一下。

首先,我们根据 Go方法的本质,也就是一个以方法的receiver参数作为第一个参数的普通函数,对这个程序做个等价变换。这里我们利用Method Expression方式,等价变换后的源码如下:

type field struct {
    name string
}

func (p *field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go (*field).print(v)
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go (*field).print(&v)
    }

    time.Sleep(3 * time.Second)
}

这段代码中,我们把对field的方法print的调用,替换为Method Expression形式,替换前后的程序输出结果是一致的。但变换后,问题是不是豁然开朗了!我们可以很清楚地看到使用go关键字启动一个新Goroutine时,method expression形式的print函数是如何绑定参数的:

  • 迭代data1时,由于data1中的元素类型是field指针(*field),因此赋值后v就是元素地址,与print的receiver参数类型相同,每次调用(*field).print函数时直接传入的v即可,实际上传入的也是各个field元素的地址;

  • 迭代data2时,由于data2中的元素类型是field(非指针),与print的receiver参数类型不同,因此需要将其取地址后再传入(*field).print函数。这样每次传入的&v实际上是变量v的地址,而不是切片data2中各元素的地址。

第19讲《控制结构:Go的for循环,仅此一种》中,我们学习过for range使用时应注意的几个问题,其中循环变量复用是关键的一个。这里的v在整个for range过程中只有一个,因此data2迭代完成之后,v是元素“six”的拷贝

这样,一旦启动的各个子goroutine在main goroutine执行到Sleep时才被调度执行,那么最后的三个goroutine在打印&v时,实际打印的也就是在v中存放的值“six”。而前三个子goroutine各自传入的是元素“one”、“two”和“three”的地址,所以打印的就是“one”、“two”和“three”了。

那么原程序要如何修改,才能让它按我们期望,输出“one”、“two”、“three”、“four”、 “five”、“six”呢?

其实,我们只需要将field类型print方法的receiver类型由*field改为field就可以了。我们直接来看一下修改后的代码:

type field struct {
    name string
}

func (p field) print() {
    fmt.Println(p.name)
}

func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
        go v.print()
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
        go v.print()
    }

    time.Sleep(3 * time.Second)
}

修改后的程序的输出结果是这样的(因Goroutine调度顺序不同,在你的机器上的结果输出顺序可能会有不同):

one
two
three
four
five
six

为什么这回就可以输出预期的值了呢?我把它留作这节课的思考题,你可以参考我的分析思路自行分析一下,欢迎你在留言区给出你的答案。

小结

好了,今天的课讲到这里就结束了,现在我们一起来回顾一下吧。

在这一讲中,我们开始讲解Go语言中除函数之外的、另一种可承载代码执行逻辑的语法元素:方法(method)。

我们要知道,Go提供方法这种语法,并非出自对经典面向对象编程范式支持的考虑,而是出自Go的组合设计哲学下类型系统实现层面上的需要。

Go方法在声明形式上相较于Go函数多了一个receiver组成部分,这个部分是方法与类型之间联系的纽带。我们可以在receiver部分声明receiver参数。但Go对receiver参数有诸多限制,比如只能有一个、参数名唯一、不能是变长参数等等。

除此之外,Go对receiver参数的基类型也是有约束的,即基类型本身不能是指针类型或接口类型。Go方法声明的位置也受到了Go规范的约束,方法声明必须与receiver参数的基类型在同一个包中。

Go方法本质上其实是一个函数,这个函数以方法的receiver参数作为第一个参数,Go编译器会在我们进行方法调用时协助进行这样的转换。牢记并理解方法的这个本质可以帮助我们在实际编码中解决一些奇怪的问题。

思考题

在“巧解难题”部分,我给你留了个问题,为啥我们只需要将field类型print方法的receiver类型,由*field改为field就可以输出预期的结果了呢?期待在留言区看到你的答案。

欢迎你把这节课分享给更多对Go语言的方法感兴趣的朋友。我是Tony Bai,我们下节课见。

精选留言

  • ddh

    2021-12-08 15:19:37

    思考题解答:
    由 *field 改为 field结果正确的原因是, *field的方法的第一个参数是*field, 这个对于[]*field数组直接传入成员就可以了, 而对于[]field数组, 则是要取地址,也就是指针。 但是这个指针指的是for range 循环的局部变量的地址, 这个地址在for 循环中是不变的, 在for循环结束后这个地址就指向了最后一个元素, goroutine真正实行打印的解引用的地址是局部变量的地址, 自然只会打印最后一个元素了
    field 的方法, 不涉及引用, 传参都是拷贝复制
    作者回复

    👍

    2021-12-15 17:57:27

  • 每天晒白牙

    2022-07-15 11:24:09

    go方法的本质是一个以方法的 receiver 参数作为第一个参数的普通函数
    函数是第一等公民,那大家都写函数就行了,方法存在的意义是啥呢?
    作者回复

    你这个问题很好👍。

    我可以将其转换为另外一个几乎等价的问题:我们知道c++的方法(成员函数)本质就是以编译器插入的一个this指针作为首个参数的普通函数。那么大家为什么不直接用c的函数,非要用面向对象的c++呢?

    其实你的问题本质上是一个编程范式演进的过程。Go类型+方法(类比于c++的类+方法)和oo范式一样,是一种“封装”概念的实现,即隐藏自身状态,仅提供方法供调用者对其状态进行正确改变操作,防止其他事物对其进行错误的状态改变操作。

    2022-07-17 08:00:12

  • Calvin

    2021-12-08 02:39:26

    思考题,reciever是 field 值类型非 *field 指针类型,转换后的方法表达式如下:
    1) field.print(*v)
    2) field.print(v)
    打印的都是切片的元素的值。
    作者回复

    👍

    2021-12-15 16:56:58

  • 左耳朵东

    2021-12-11 14:22:26

    如果 print 方法的 receiver 类型为 field:
    首先,两个 for range 循环中的 go v.print() 分别等同于 go field.print(*v) 和 go field.print(v),
    然后,第一个 for range 循环,用 *field 去调用 print 方法时,编译器检测到 print 方法只接受 field 值类型参数,所以自动做了隐式类型转换,转成 *v 后传入 print 方法
    可以看到两个 for range 中实际传到 print 的实参都是 field 值类型而非指针类型,所以就得到了预期结果
    作者回复

    👍

    2021-12-21 10:30:35

  • 罗杰

    2021-12-08 09:48:18

    老师在我心目中就是 “Go 语言百科全书”。
    作者回复

    这.... :)

    2021-12-15 17:57:16

  • 进化菌

    2021-12-08 21:02:55

    *field 改为 field,由指针类型变成普通类型。goroutine在编译的时候就初始化了变量吧,那么指针类型的自然会随着变化而变化,普通类型被值拷贝而不会发生变化。
    * 和 & 都是值得花时间学习和理解的东西,不知道老师后面会不会特别的说一下呢?
    作者回复

    指针在Go中被弱化,所以我在设计大纲时没有特意为之留出章节,如果大家有这方面的想法,我和编辑老师看看是否可以在加餐中补充一下。但可能要放在后面了

    2021-12-15 18:11:34

  • Roway

    2022-07-07 10:55:07

    *T &T T _ 这四个分别是什么意思?还有哪些基本的概念
    作者回复

    T泛指一个go类型
    *T 是T类型的指针类型
    &T{} 返回一个T类型实例的指针
    _ 是go语法中的空标识符

    2022-07-07 13:45:07

  • aoe

    2021-12-10 13:18:58

    一直以为 func 开头的就是方法,原来还分函数和方法!我对方法的理解:
    1. 提供了良好的封装,receiver 限定了使用对象,方法名可以表使用达对象可以提供的行为
    2. 使用起来更方便简洁,因为可以少传一个参数
    3. Go 语言设计者的思维真是缜密啊,“方法声明必须与 receiver 参数的基类型在同一个包中”这个规则解决了无数可能出现的奇奇怪怪的情况
    4. 可以促进包中代码功能的高内聚,因为你出了包,定义方法时会受到限制,可以及时发现:哎呀,有问题
    作者回复

    👍

    2021-12-18 13:10:41

  • Untitled

    2022-02-10 08:32:47

    receiver 参数的基类型本身不能为指针类型或接口类型??
    *T不是指针类型吗?不理解
    作者回复

    T是基类型,说的是T本身不能为指针类型。

    type T *int
    func (T) M1() {} // invalid receiver type T (pointer or interface type)

    2022-02-11 22:56:51

  • 不说话装糕手

    2022-11-11 15:27:26

    白老师您好,关于文章中“没有用到 receiver 参数,我们也可以省略 receiver 的参数名”情况,如果把方法看作是第一个参数为receiver的函数,那么这个没有形参名字的receiver类型参数,实际上是否传入了函数,并且该如何设计代码验证呢?
    作者回复

    实际上当然会传入,来个例子证明一下吧。

    type Foo struct {
    }

    func (Foo) M1(a int, b int) int {
    return a + b
    }

    func main() {
    m1 := Foo.M1
    fmt.Printf("%T\n", m1)
    }

    运行这个例子输出:func(main.Foo, int, int) int

    2022-11-14 14:34:03

  • Geek_7254f2

    2022-02-17 19:48:09

    建议老师把data1 := []*field{{"one"}, {"two"}, {"three"}}和data2 := []field{{"four"}, {"five"}, {"six"}}其中data1和date2中[]*、[]类型的区别讲一下,就好理解了。特别是还有*[]类型,这三个类型很像,很容易混淆
    作者回复

    好建议👍,感谢。

    2022-02-23 14:28:02

  • return

    2021-12-08 11:24:34

    老师不仅把原理讲透,每篇还罗列了各种坑,讲的太好了。
    有个疑问,
    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
    go (*field).print(&v)
    }
    关于这一段, 按道理 goroutine 注册的时候 就会对参数求值, receiver也是参数,
    我自己打印了一下, &v的值 确实是 &{four} &{five} &{six},
    但是 goroutine打印出来就变成了 3个six。
    而且 尝试很多次后发现, 少数情况 会出现 2个six 另一个事 five。
    很懵!!!
    作者回复

    最后的少数情况的结果也正常,看goroutine调度的时机。

    2021-12-15 17:55:52

  • Geek_1621b6

    2021-12-10 12:09:31

    我觉得第二种情况会打印三个six是因为go (*field).print(&v)中&v是不变的,在循环结束后指向six。而第一种情况go (*field).print(v)中v的值是在变化的。
  • DullBird

    2022-12-30 09:46:42

    由指针修改成非指针后,方法调用的时候,是拷贝入参值,不是一个指针地址,所以没问题
    作者回复

    2022-12-30 12:25:13

  • Geralt

    2021-12-09 15:29:23

    *field 改为 field 之后,每次调用v.print()时v的值都是不一样的。
    作者回复

    👍

    2021-12-15 18:29:23

  • Witt

    2021-12-09 10:29:00

    这算不算一种解决方法,迭代 data2的时候在 for 内遮蔽 v 的值 v:=v 😀
    作者回复

    应该可以。这样每个&v就是一块独立的地址👍。你可以写代码试一下。

    2021-12-15 18:13:41

  • 酥宝话不多

    2023-05-26 22:42:25

    本来学得一知半解,看老师的解答,就基本略懂略懂了哈
    作者回复

    👍

    2023-05-27 21:45:32

  • 义务教育漏网之鱼

    2023-04-21 17:48:23

    type field struct {
    name string
    }

    func (p *field) method() {
    fmt.Println(p.name)
    }

    func main() {
    f := field{"ffff"}
    f.method()
    }
    请老师解惑。如上这个示例,方法 method 是绑定在 *field 类型上的。按照本章的讲解应该只允许 *field 类型的实例调用。根据我测试的结果来看 field 类型的实例也可以调用 method 方法,这是为什么呢?
    作者回复

    在25讲中有详细说明:)

    2023-04-22 15:35:36

  • A_唐波涛

    2023-03-12 15:15:05

    这到底是go并发的原因,还是老师说的原因?如下代码运行,感觉更多是并发影响了结果!
    package main

    import (
    "fmt"
    "time"
    )

    type field struct {
    name string
    }

    func (p *field) print() {
    fmt.Println(p.name)
    }

    func main() {
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for _, v := range data1 {
    fmt.Print(v, " + ")
    v.print()
    }

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for _, v := range data2 {
    fmt.Print(v, " + ")
    v.print()
    }

    data3 := field{"kkk3"}
    data3.print()

    data4 := &field{"kkk4"}
    data4.print()

    time.Sleep(3 * time.Second)
    }
    ------输出结果------
    &{one} + one
    &{two} + two
    &{three} + three
    {four} + four
    {five} + five
    {six} + six
    kkk3
    kkk4
    作者回复

    你这个代码中也不存在并发的问题,因为没有使用go关键字创建goroutine来执行for循环,因此都是顺序执行的。

    2023-03-13 18:40:40

  • 无咎

    2023-02-10 10:23:39

    还是dump地址的方式,Playground: https://go.dev/play/p/yGHjXmlVCyZ
    代码
    ```
    package main

    import (
    "fmt"
    "time"
    )

    type field struct {
    name string
    }

    func (p *field) print() {
    fmt.Printf("fields.print(%p, %p, %+v)\n", &p, p, p)
    // fmt.Println(p.name)
    }

    func main() {
    /*
    data1 := []*field{{"one"}, {"two"}, {"three"}}
    for i := 0; i < len(data1); i++ {
    fmt.Printf("data1-&v:%p, v=%#v\n", &data1[i], data1[i])
    }
    for _, v := range data1 {
    fmt.Printf("data1-&v:%p, v=%#v\n", &v, v)
    go v.print()
    }
    fmt.Println("---")
    */

    data2 := []field{{"four"}, {"five"}, {"six"}}
    for i := 0; i < len(data2); i++ {
    fmt.Printf("data2-&v:%p, v=%#v\n", &data2[i], data2[i])
    }
    fmt.Println("---1---")
    for _, v := range data2 {
    fmt.Printf("data2-&v:%p, v=%#v\n", &v, v)
    go v.print()
    }
    fmt.Println("---2---")

    time.Sleep(3 * time.Second)
    fmt.Println("---3---")
    }
    ```
    输出
    ```
    data2-&v:0xc00008c180, v=main.field{name:"four"}
    data2-&v:0xc00008c190, v=main.field{name:"five"}
    data2-&v:0xc00008c1a0, v=main.field{name:"six"}
    ---1---
    data2-&v:0xc00008a050, v=main.field{name:"four"}
    data2-&v:0xc00008a050, v=main.field{name:"five"}
    data2-&v:0xc00008a050, v=main.field{name:"six"}
    ---2---
    fields.print(0xc000012010, 0xc00008a050, &{name:six})
    fields.print(0xc000100000, 0xc00008a050, &{name:six})
    fields.print(0xc000194000, 0xc00008a050, &{name:six})
    ---3---

    Program exited.
    ```
    其中v的地址是0xc00008a050,传入到field的也是0xc00008a050这个地址。