在 Go 語言中,運算式 `foo.bar` 可能表示兩件事。如果 *foo* 是一個包名,那麼運算式就是一個所謂的`限定標識符`,用來引用包 *foo* 中的匯出的標識符。由於它只用來處理匯出的標識符,*bar* 必須以大寫字母開頭(譯註:如果首字母大寫,則可以被其他的包訪問;如果首字母小寫,則只能在本包中使用):```gopackage fooimport "fmt"func Foo() { fmt.Println("foo")}func bar() { fmt.Println("bar")}package mainimport "github.com/mlowicki/foo"func main() { foo.Foo()}```這樣的程式會工作正常。但是(主函數)調用 `foo.bar()` 會在編譯時間報錯 —— `cannot refer to unexported name foo.bar(無法引用未匯出的名稱 foo.bar)`。如果 *foo* 不是 一個包名,那麼 `foo.bar` 就是一個選取器運算式。它訪問 *foo* 運算式的欄位或方法。點之後的標識符被稱為 *selector*(選取器)。關於首字母大寫的規則並不適用於這裡。它允許從定義了 *foo* 類型的包中選擇未匯出的欄位或方法:```gopackage mainimport "fmt"type T struct { age byte}func main() { fmt.Println(T{age: 30}.age)}```該[程式](https://play.golang.org/p/DwQFPZ3bG7)列印:`30`## 選取器的深度語言規範定義了選取器的 *depth*(深度)。讓我們來看看它是如何工作的吧。選取器運算式 `foo.bar` 可以表示定義在 *foo* 類型的欄位或方法或者定義在 *foo* 類型中的匿名欄位:```gotype E struct { name string}func (e E) SayHi() { fmt.Printf("Hi %s!\n", e.name)}type T struct { age byte E}func (t T) IsStillYoung() bool { return t.age <= 18}func main() { t := T{30, E{"Michał"}} fmt.Println(t.IsStillYoung()) // false fmt.Println(t.age) // 30 t.SayHi() // Hi Michał! fmt.Println(t.name) // Michał}```在上面的[代碼](https://play.golang.org/p/GWbEzILDdg)中,我們可以看到可以調用方法或者訪問定義在嵌入欄位中欄位。欄位 `t.name` 和方法 `t.SayHi` 都被提升了,這是因為類型 *E* 嵌套在 *T* 的定義中:```gotype T struct { age byte E}```定義在類型 *T* 中表示欄位或類型的選取器深度為 0(譯註:表示在類型 T 中定義的欄位或方法的選取器的深度為 0)。如果欄位或方法定義在嵌入(也就是 匿名)欄位,那麼深度等於匿名欄位遍曆這樣欄位或方法的數量。在上一個片段中,*age* 欄位深度是 0,因為它在 *T* 中聲明,但是因為 *E* 是放在 *T* 中,*name* 或者 *SayHi* 的深度是 1。讓我們來看看更複雜的[例子](https://play.golang.org/p/8-8xi_JpaU):```gopackage mainimport "fmt"type A struct { a string}type B struct { b string A}type C struct { c string B}func main() { v := C{"c", B{"b", A{"a"}}} fmt.Println(v.c) // c fmt.Println(v.b) // b fmt.Println(v.a) // a}```* *c* 的深度是 `v.c`,其值為 0。這是因為欄位是在 *C* 中聲明的* `v.b` 中 *b* 的深度是 1。這是因為它的欄位定義在類型 *B* 中,其(類型B)又嵌入在 *C* 中* `v.a` 中 *a* 的深度是 2。這是因為需要遍曆兩個匿名欄位(*B* 和 *A*)才能訪問它## 有效選取器go 語言中有關哪些選取器有效,哪些無效有著明確規則。讓我們來深入瞭解他們。### 唯一性+最淺深度當 *T* 不是指標或者介面類型,第一條規則適用於類型 `T` 與 `*T`。選取器 *foo.bar* 表示欄位和方法在定義了 *bar* 的類型 *T* 中的最淺深度。在這樣的深度,恰好可以定義一個(唯一的)這樣的欄位或者方法([原始碼](https://play.golang.org/p/mGtRxnrAQR)):```gotype A struct { B C}type B struct { age byte name string}type C struct { age byte D}type D struct { name string}func main() { a := A{B{1, "b"}, C{2, D{"d"}}} fmt.Println(a) // {{1 b} {2 {d}}} // fmt.Println(a.age) ambiguous selector a.age fmt.Println(a.name) // b}```類型嵌入的結構如下:``` A / \B C \ D```選取器 *a.name* 是有效,並且表示欄位 *name*(*B* 類型內)的深度為 1。*C* 類型中的欄位 *name* 是 “shadowed(淺的)”。有關 *age* 欄位則是不同的。在深度 1 處有這樣兩個欄位(在 *B* 和 *C* 類型中),所以編譯器會拋出 `ambiguous selector a.age` 錯誤。當被提升的欄位或方法有歧義時,Gopher 仍然可以使用完整的選取器。```gofmt.Println(a.B.name) // bfmt.Println(a.C.D.name) // dfmt.Println(a.C.name) // d```值得重申的是,該規則也適用於 `*T` —— [例子](https://play.golang.org/p/8AfF4ie3HB)。### null 指標```gopackage mainimport "fmt"type T struct { num int}func (t T) m() {}func main() { var p *T fmt.Println(p.num) p.m()}```如果選取器是有效,但 *foo* 是一個null 指標,那麼評估 *foo.bar* 造成 runtime panic:`panic invalid memory address or nil pointer dereference`([原始碼](https://play.golang.org/p/hxiU6S8jTS))### 介面如果 *foo* 是一個介面類型值,那麼 *foo.bar* 實際上是 *foo* 的動態值的一個方法:```gotype I interface { m()}type T struct{}func (T) m() { fmt.Println("I’m alive!")}func main() { var i I i = T{} i.m()}```上面的[片段](https://play.golang.org/p/j8zo9Th2N0)輸出 `I'm alive!`。當然,調用不在介面的方法集合中的方法時,會產生編譯時間錯誤,如 `i.f undefined (type I has no field or method f)`如果 *foo* 為 *nil*,那麼它將會導致一個執行階段錯誤:```gotype I interface { f()}func main() { var i I i.f()}```這樣的[程式](https://play.golang.org/p/noOpOVwpV_)將會因為錯誤 `panic: runtime error: invalid memory address or nil pointer dereference` 而崩潰。這和null 指標情況類似,而且由於諸如沒有值賦值和介面[零值](https://golang.org/ref/spec#The_zero_value)為 *nil* 而發生錯誤。### 一個特殊情況除了到現在為止關於有效選取器的描述外,這還有一個情境:假設這裡有一個命名指標類型:```gotype P *T```類型 *P* 的[方法集](https://golang.org/ref/spec#Method_sets)不包含類型 *T* 的任何方法。如果有類型 *P* 的變數,則無法調用任何 *T* 的方法。但是,規範允許選擇類型 *T* 的欄位(非方法)([原始碼](https://play.golang.org/p/7wJI4F34ij)):```gotype T struct { num int}func (t T) m() {}type P *Tfunc main() { var p P = &T{num: 10} fmt.Println(p.num) // p.m() // compile-time error: p.m undefined (type P has no field or method m) (*p).m()}````p.num` 在 hood 下被轉化為 `(*p).num`。## 在 hood 下如果你對選取器朝朝和驗證的實際實現感興趣的話,請查看 [selector](https://github.com/golang/go/blob/6bdb0c11c73ecf2337918d784c54f9dda2207ca7/src/go/types/call.go#L341) 和 [LookupFieldOrMethod](https://github.com/golang/go/blob/6bdb0c11c73ecf2337918d784c54f9dda2207ca7/src/go/types/lookup.go) 函數。[這裡](https://play.golang.org/p/hjGWpBor2l)是最後一個使用的例子。
via: https://medium.com/golangspec/selectors-in-go-c53a016702cf
作者:Michał Łowicki 譯者:cureking 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
199 次點擊