Go中Struct的指针方法和非指针方法

在go中,我们可以为结构体绑定方法,使某个方法属于某个函数,但是却有两种不一样的方法

例如:

type T struct {
    Name string
}

func (t T) M1() {
    t.Name = "name1"
}

func (t *T) M2() {
    t.Name = "name2"
}

这两种方法看起来都是对Name进行了修改,我们来运行一下

func main(){
    t:=T{Name: "name"}
    fmt.Println("T:",t.Name)
    t.M1()
    fmt.Println("M1:",t.Name)
    t.M2()
    fmt.Println("M2:",t.Name)
}

输出:

T: name
M1: name
M2: name2

可以看到M1方法并不能对原对象进行修改,这是因为M1是值引用,当运行函数的时候,会复制一个相同的对象来执行函数,相当于执行M1(t) M2(&t),M1修改的是复制的对象而不是原对象,因此如果期望对原对象进行修改,则必须采用指针方法。

但是如果t是指针呢?

func main(){
    t:=&T{Name: "name"}
    fmt.Println("T:",t.Name)
    t.M1()
    fmt.Println("M1:",t.Name)
    t.M2()
    fmt.Println("M2:",t.Name)
}

输出:

T: name
M1: name
M2: name2

结果是一样的。

这是因为,运行t2.M1的时候,会取t2地址的值复制一遍再传递给M1.

但是请注意,T是拥有M1方法的,*T是拥有M1,M2这两种方法的

如果运行

type Intf interface {
    M1()
    M2()
}

func main(){
    t:=T{Name: "name"}

    var t2 Intf = t
    fmt.Println("T:",t.Name)
    t2.M1()
    fmt.Println("M1:",t.Name)
    t2.M2()
    fmt.Println("M2:",t.Name)
}

将报错

.\funcTest.go:25:6: cannot use t (type T) as type Intf in assignment:
    T does not implement Intf (M2 method has pointer receiver)

一个小案例

package main

import "sync"

type MyMap struct {
    m map[int]int
    lock sync.Mutex
}

func (myMap *MyMap) Set(i int)  {
    myMap.lock.Lock()
    defer myMap.lock.Unlock()

    myMap.m[i]=i
}

func (myMap MyMap) Get(i int) int {
    myMap.lock.Lock()
    defer myMap.lock.Unlock()

    return myMap.m[i]
}

func main(){
    m:=&MyMap{m:make(map[int]int)}

    wg:=sync.WaitGroup{}

    for i:=0;i<100;i++{
        wg.Add(1)
        go func(num int){
            defer wg.Done()
            // 交替执行Set,Get
            if num%2==0{
                m.Set(num)
            }else{
                m.Get(num)
            }
        }(i)
    }

    wg.Wait()
}

代码很简单,一个并发安全的map,看起来没有任何问题,但是当运行代码时,非常可能将会报错fatal error: all goroutines are asleep - deadlock! 发生了死锁,为什么呢?不是最后都Unlock() 了吗?其实可以注意到func (myMap MyMap) Get(i int) ,这是非指针引用,而sync.Mutex也不是指针对象,因此当调用Get()的时候将会复制一份新的结构体,因此这个锁就已经不是原来的锁了,也就是说加锁失败了。但是这其实也不是发生死锁的原因(笑),这会导致另一个错误fatal error: concurrent map read and map write ,对一个map同时读写。出现死锁的原因是,当进行值拷贝的时候,同时也会拷贝锁的状态!!!,也就是说我们可能拷贝了一份上锁了的锁过来,然后我们又在等待锁的释放,但是并没有人可以对其进行释放,因此就进入了死锁状态。

所以请在编写代码是时候习惯性的采用指针方法,一来提高性能(不用进行数据的拷贝),二来避免出现奇奇怪怪的BUG。