Skip to content

Go方法

方法與函數的區別在於,方法擁有接收者,而函數沒有,且只有自定義類型能夠擁有方法。先來看一個例子。

go
type IntSlice []int

func (i IntSlice) Get(index int) int {
  return i[index]
}
func (i IntSlice) Set(index, val int) {
  i[index] = val
}

func (i IntSlice) Len() int {
  return len(i)
}

先聲明了一個類型IntSlice,其底層類型為[]int,再聲明了三個方法GetSetLen,方法的長相與函數並無太大的區別,只是多了一小段(i IntSlice)i就是接收者,IntSlice就是接收者的類型,接收者就類似於其他語言中的thisself,只不過在 Go 中需要顯式的指明。

go
func main() {
   var intSlice IntSlice
   intSlice = []int{1, 2, 3, 4, 5}
   fmt.Println(intSlice.Get(0))
   intSlice.Set(0, 2)
   fmt.Println(intSlice)
   fmt.Println(intSlice.Len())
}

方法的使用就類似於調用一個類的成員方法,先聲明,再初始化,再調用。

值接收者

接收者也分兩種類型,值接收者和指針接收者,先看一個例子

go
type MyInt int

func (i MyInt) Set(val int) {
   i = MyInt(val) // 修改了,但是不會造成任何影響
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

上述代碼運行過後,會發現myInt的值依舊是 1,並沒有被修改成 2。方法在被調用時,會將接收者的值傳入方法中,上例的接收者就是一個值接收者,可以簡單的看成一個形參,而修改一個形參的值,並不會對方法外的值造成任何影響,那麼如果通過指針調用會如何呢?

go
func main() {
  myInt := MyInt(1)
  (&myInt).Set(2)
  fmt.Println(myInt)
}

遺憾的是,這樣的代碼依舊不能修改內部的值,為了能夠匹配上接收者的類型,Go 會將其解引用,解釋為(*(&myInt)).Set(2)

指針接收者

稍微修改了一下,就能正常修改myInt的值。

go
type MyInt int

func (i *MyInt) Set(val int) {
   *i = MyInt(val)
}

func main() {
   myInt := MyInt(1)
   myInt.Set(2)
   fmt.Println(myInt)
}

現在的接收者就是一個指針接收者,雖然myInt是一個值類型,在通過值類型調用指針接收者的方法時,Go 會將其解釋為(&myint).Set(2)。所以方法的接收者為指針時,不管調用者是不是指針,都可以修改內部的值。

函數的參數傳遞過程中,是值拷貝的,如果傳遞的是一個整型,那就拷貝這個整型,如果是一個切片,那就拷貝這個切片,但如果是一個指針,就只需要拷貝這個指針,顯然傳遞一個指針比起傳遞一個切片所消耗的資源更小,接收者也不例外,值接收者和指針接收者也是同樣的道理。在大多數情況下,都推薦使用指針接收者,不過兩者並不應該混合使用,要麼都用,要麼就都不用,看下面一個例子。

TIP

需要先了解接口

go
type Animal interface {
   Run()
}

type Dog struct {
}

func (d *Dog) Run() {
   fmt.Println("Run")
}

func main() {
   var an Animal
   an = Dog{}
   // an = &Dog{} 正確方式
   an.Run()
}

這一段代碼將會無法通過編譯,編譯器將會輸出如下錯誤

cannot use Dog{} (value of type Dog) as type Animal in assignment:
  Dog does not implement Animal (Run method has pointer receiver)

翻譯過來就是,無法使用Dog{}初始化Animal類型的變量,因為Dog沒有實現Animal ,解決辦法有兩種,一是將指針接收者改為值接收者,二是將Dog{}改為&Dog{},接下來逐個講解。

go
type Dog struct {
}

func (d Dog) Run() { // 改為了值接收者
   fmt.Println("Run")
}

func main() { // 可以正常運行
   var an Animal
   an = Dog{}
   // an = &Dog{} 同樣可以
   an.Run()
}

在原來的代碼中,Run 方法的接收者是*Dog ,自然而然實現Animal接口的就是Dog指針,而不是Dog結構體,這是兩個不同的類型,所以編譯器就會認為Dog{}並不是Animal的實現,因此無法賦值給變量an,所以第二種解決辦法就是賦值Dog指針給變量an。不過在使用值接收者時,Dog指針依然可以正常賦值給animal,這是因為 Go 會在適當情況下對指針進行解引用,因為通過指針可以找到Dog結構體,但是反過來的話,無法通過Dog結構體找到Dog指針。如果單純的在結構體中混用值接收者和指針接收者的話無傷大雅,但是和接口一起使用後,就會出現錯誤,倒不如無論何時要麼都用值接收者,要麼就都用指針接收者,形成一個良好的規范,也可以減少後續維護的負擔。

還有一種情況,就是當值接收者是可尋址的時候,Go 會自動的插入指針運算符來進行調用,例如切片是可尋址,依舊可以通過值接收者來修改其內部值。比如下面這個代碼

go
type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func main() {
  s := make(Slice, 1)
  s.Set(0, 1)
  fmt.Println(s)
}

輸出

[1]

但這樣會引發另一個問題,如果對其添加元素的話,情況就不同了。看下面的例子

type Slice []int

func (s Slice) Set(i int, v int) {
  s[i] = v
}

func (s Slice) Append(a int) {
  s = append(s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}
[1]

它的輸出還是和之前一樣,append函數是有返回值的,向切片添加完元素後必須覆蓋原切片,尤其是在擴容後,在方法中對值接收者修改並不會產生任何影響,這也就導致了例子中的結果,改成指針接收者就正常了。

go
type Slice []int

func (s *Slice) Set(i int, v int) {
  (*s)[i] = v
}

func (s *Slice) Append(a int) {
  *s = append(*s, a)
}

func main() {
  s := make(Slice, 1, 2)
  s.Set(0, 1)
  s.Append(2)
  fmt.Println(s)
}

輸出

[1 2]

Golang學習網由www.golangdev.cn整理維護