Go語言聖經:基礎數據類型、複合數據類型、函數、方法章節摘錄

Go語言將數據類型分爲四類:基礎類型、複合類型、引用類型和接口類型。本章介紹基礎類型,包括:數字、字符串和布爾型。複合數據類型——數組(§4.1)和結構體(§4.2)——是通過組合簡單類型,來表達更加複雜的數據結構。引用類型包括指針(§2.3.2)、切片(§4.2))字典(§4.3)、函數(§5)、通道(§8),雖然數據種類很多,但它們都是對程序中一個變量或狀態的間接引用。這意味着對任一引用類型數據的修改都會影響所有該引用的拷貝。我們將在第7章介紹接口類型。

當使用fmt包打印一個數值時,我們可以用%d、%o或%x(%X)參數控制輸出的進制格式

fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", 0xdeadbeef)
// 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF

注意fmt的兩個使用技巧。通常Printf格式化字符串包含多個%參數時將會包含對應相同數量的額外操作數,但是%之後的[1]副詞告訴Printf函數再次使用第一個操作數。第二,%後的#副詞告訴Printf在用%o、%x或%X輸出時生成0、0x或0X前綴。

一個float32類型的浮點數可以提供大約6個十進制數的精度,而float64則可以提供約15個十進制數的精度;通常應該優先使用float64類型,因爲float32類型的累計計算誤差很容易擴散,並且float32能精確表示的正整數並不是很大(譯註:因爲float32的有效bit位只有23個,其它的bit位用於指數和符號;當整數大於23bit能表達的範圍時,float32的表示將出現誤差)

var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1)    // "true"!

用Printf函數的%g參數打印浮點數,將採用更緊湊的表示形式打印,並提供足夠的精度,但是對應表格的數據,使用%e(帶指數)或%f的形式打印可能更合適。所有的這三個打印形式都可以指定打印的寬度和控制打印精度。

fmt.Printf("%g, %[1]e, %[1]f", float64(0.11111111111111111111))
// 0.1111111111111111, 1.111111e-01, 0.111111

math包中除了提供大量常用的數學函數外,還提供了IEEE754浮點數標準中定義的特殊值的創建和測試:正無窮大和負無窮大,分別用於表示太大溢出的數字和除零的結果;還有NaN非數,一般用於表示無效的除法操作結果0/0或Sqrt(-1)

var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"

布爾值並不會隱式轉換爲數字值0或1,反之亦然。必須使用一個顯式的if語句輔助轉換

i := 0
if b {
    i = 1
}

如果需要經常做類似的轉換, 包裝成一個函數會更方便

// btoi returns 1 if b is true and 0 if false.
func btoi(b bool) int {
    if b {
        return 1
    }
    return 0
}
// itob reports whether i is non-zero.
func itob(i int) bool { return i != 0 }

因爲字符串是不可修改的,因此嘗試修改字符串內部數據的操作也是被禁止的
s[0] = 'L' // compile error: cannot assign to s[0]
不變性意味如果兩個字符串共享相同的底層數據的話也是安全的,這使得複製任何長度的字符串代價是低廉的。同樣,一個字符串s和對應的子字符串切片s[7:]的操作也可以安全地共享相同的內存,因此字符串切片操作代價也是低廉的。在這兩種情況下都沒有必要分配新的內存

原生字符串面值用於編寫正則表達式會很方便,因爲正則表達式往往會包含很多反斜槓。原生字符串面值同時被廣泛應用於HTML模板、JSON面值、命令行提示信息以及那些需要擴展到多行的場景。

UTF8編碼使用1到4個字節來表示每個Unicode碼點,ASCII部分字符只使用1個字節,常用字符部分使用2或3個字節表示。每個符號編碼後第一個字節的高端bit位用於表示總共有多少編碼個字節。如果第一個字節的高端bit爲0,則表示對應7bit的ASCII字符,ASCII字符每個字符依然是一個字節,和傳統的ASCII編碼兼容。如果第一個字節的高端bit是110,則說明需要2個字節;後續的每個高端bit都以10開頭。更大的Unicode碼點也是採用類似的策略處理

0xxxxxxx                             runes 0-127    (ASCII)
110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)

字符串包含13個字節,以UTF8形式編碼,但是隻對應9個Unicode字符

import "unicode/utf8"

s := "Hello, 世界"
fmt.Println(len(s))                    // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"
for i := 0; i < len(s); {
    r, size := utf8.DecodeRuneInString(s[i:])
    fmt.Printf("%d\t%c\n", i, r)
    i += size
}
等價
for i, r := range "Hello, 世界" {
    fmt.Printf("%d\t%q\t%d\n", i, r, r)
}

每一個UTF8字符解碼,不管是顯式地調用utf8.DecodeRuneInString解碼或是在range循環中隱式地解碼,如果遇到一個錯誤的UTF8編碼輸入,將生成一個特別的Unicode字符’\uFFFD’,在印刷中這個符號通常是一個黑色六角或鑽石形狀,裏面包含一個白色的問號(?)。當程序遇到這樣的一個字符,通常是一個危險信號,說明輸入並不是一個完美沒有錯誤的UTF8字符串

標準庫中有四個包對字符串處理尤爲重要:bytes、strings、strconv和unicode包。strings包提供了許多如字符串的查詢、替換、比較、截斷、拆分和合並等功能。

bytes包也提供了很多類似功能的函數,但是針對和字符串有着相同結構的[]byte類型。因爲字符串是隻讀的,因此逐步構建字符串會導致很多分配和複製。在這種情況下,使用bytes.Buffer類型將會更有效,稍後我們將展示。

strconv包提供了布爾型、整型數、浮點數和對應字符串的相互轉換,還提供了雙引號轉義相關的轉換。

unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等類似功能,它們用於給字符分類。每個函數有一個單一的rune類型的參數,然後返回一個布爾值。而像ToUpper和ToLower之類的轉換函數將用於rune字符的大小寫轉換。所有的這些函數都是遵循Unicode標準定義的字母、數字等分類規範。strings包也有類似的函數,它們是ToUpper和ToLower,將原始字符串的每個字符都做相應的轉換,然後返回新的字符串。

字符串和字節slice之間可以相互轉換

s := "abc"
b := []byte(s)
s2 := string(b)

從概念上講,一個[]byte(s)轉換是分配了一個新的字節數組用於保存字符串數據的拷貝,然後引用這個底層的字節數組。編譯器的優化可以避免在一些場景下分配和複製字符串數據,但總的來說需要確保在變量b被修改的情況下,原始的s字符串也不會改變。將一個字節slice轉到字符串的string(b)操作則是構造一個字符串拷貝,以確保s2字符串是隻讀的

bytes包還提供了Buffer類型用於字節slice的緩存。一個Buffer開始是空的,但是隨着string、byte或[]byte等類型數據的寫入可以動態增長,一個bytes.Buffer變量並不需要處理化,因爲零值也是有效的

如果在數組的長度位置出現的是“…”省略號,則表示數組的長度是根據初始化值的個數來計算

q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"

數組的長度是數組類型的一個組成部分,因此[3]int和[4]int是兩種不同的數組類型。數組的長度必須是常量表達式,因爲數組的長度需要在編譯階段確定。

要刪除slice中間的某個元素並保存原有的元素順序,可以通過內置的copy函數將後面的子slice向前依次移動一位完成

func remove(slice []int, i int) []int {
    copy(slice[i:], slice[i+1:])
    return slice[:len(slice)-1]
}

func main() {
    s := []int{5, 6, 7, 8, 9}
    fmt.Println(remove(s, 2)) // "[5 6 8 9]"
}

如果刪除元素後不用保持原來順序的話,我們可以簡單的用最後一個元素覆蓋被刪除的元素

func remove(slice []int, i int) []int {
    slice[i] = slice[len(slice)-1]
    return slice[:len(slice)-1]
}

func main() {
    s := []int{5, 6, 7, 8, 9}
    fmt.Println(remove(s, 2)) // "[5 6 9 8]
}

但是map中的元素並不是一個變量,因此我們不能對map的元素進行取址操作:
_ = &ages["bob"] // compile error: cannot take address of map element
禁止對map元素取址的原因是map可能隨着元素數量的增長而重新分配更大的內存空間,從而可能導致之前的地址無效。

Map的迭代順序是不確定的,並且不同的哈希函數實現可能導致不同的遍歷順序。在實踐中,遍歷的順序是隨機的,每一次遍歷的順序都不相同。這是故意的,每次都使用隨機的遍歷順序可以強制要求程序不會依賴具體的哈希函數實現。如果要按順序遍歷key/value對,我們必須顯式地對key進行排序,可以使用sort包的Strings函數對字符串slice進行排序。

var names []string
for name := range ages {
    names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
    fmt.Printf("%s\t%d\n", name, ages[name])
}

因爲我們一開始就知道names的最終大小,因此給slice分配一個合適的大小將會更有效。下面的代碼創建了一個空的slice,但是slice的容量剛好可以放下map中全部的key
names := make([]string, 0, len(ages))

一個命名爲S的結構體類型將不能再包含S類型的成員:因爲一個聚合的值不能包含它自身。(該限制同樣適應於數組。)但是S類型的結構體可以包含*S指針類型的成員,這可以讓我們創建遞歸的數據結構,比如鏈表和樹結構等

我們將看到如何使用Go語言提供的不同尋常的結構體嵌入機制讓一個命名的結構體包含另一個結構體類型的匿名成員,這樣就可以通過簡單的點運算符x.f來訪問匿名成員鏈中嵌套的x.d.e.f成員

type Point struct {
    X, Y int
}

type Circle struct {
    Point
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}

var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

結構體字面值必須遵循形狀類型聲明時的結構,所以我們只能用下面的兩種語法,它們彼此是等價的

w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

到目前爲止,我們看到匿名成員特性只是對訪問嵌套成員的點運算符提供了簡短的語法糖。稍後,我們將會看到匿名成員並不要求是結構體類型;其實任何命令的類型都可以作爲結構體的匿名成員。但是爲什麼要嵌入一個沒有任何子成員類型的匿名成員類型呢?

答案是匿名類型的方法集。簡短的點運算符語法可以用於選擇匿名成員嵌套的成員,也可以用於訪問它們的方法。實際上,外層的結構體不僅僅是獲得了匿名成員類型的所有成員,而且也獲得了該類型導出的全部的方法。這個機制可以用於將一個有簡單行爲的對象組合成有複雜行爲的對象。組合是Go語言中面向對象編程的核心

另一個json.MarshalIndent函數將產生整齊縮進的輸出。該函數有兩個額外的字符串參數用於表示每一行輸出的前綴和每一個層級的縮進
data, err := json.MarshalIndent(movies, "", " ")

結構體的成員Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:”value”鍵值對序列;因爲值中含有雙引號字符,因此成員Tag一般用原生字符串面值的形式書寫。json開頭鍵名對應的值用於控制encoding/json包的編碼和解碼的行爲,並且encoding/…下面其它的包也遵循這個約定。成員Tag中json對應值的第一部分用於指定JSON對象的名字,比如將Go語言中的TotalCount成員對應到JSON中的total_count對象。Color成員的Tag還帶了一個額外的omitempty選項,表示當Go語言結構體成員爲空或零值時不生成JSON對象(這裏false爲零值)

Year  int  `json:"released"`
Color bool `json:"color,omitempty"`

golang.org/x/… 目錄下存儲了一些由Go團隊設計、維護,對網絡編程、國際化文件處理、移動平臺、圖像處理、加密解密、開發者工具提供支持的擴展包。未將這些擴展包加入到標準庫原因有二,一是部分包仍在開發中,二是對大多數Go語言的開發者而言,擴展包提供的功能很少被使用

雖然Go的垃圾回收機制會回收不被使用的內存,但是這不包括操作系統層面的資源,比如打開的文件、網絡連接。因此我們必須顯式的釋放這些資源。

fmt.Errorf函數使用fmt.Sprintf格式化錯誤信息並返回。我們使用該函數前綴添加額外的上下文信息到原始錯誤信息。當錯誤最終由main函數處理時,錯誤信息應提供清晰的從原因到後果的因果鏈,就像美國宇航局事故調查時做的那樣
genesis: crashed: no parachute: G-switch failed: bad relay orientation
由於錯誤信息經常是以鏈式組合在一起的,所以錯誤信息中應避免大寫和換行符

處理錯誤的第二種策略。如果錯誤的發生是偶然性的,或由不可預知的問題導致的。一個明智的選擇是重新嘗試失敗的操作。在重試時,我們需要限制重試的時間間隔或重試的次數,防止無限制的重試

// WaitForServer attempts to contact the server of a URL.
// It tries for one minute using exponential back-off.
// It reports an error if all attempts fail.
func WaitForServer(url string) error {
    const timeout = 1 * time.Minute
    deadline := time.Now().Add(timeout)
    for tries := 0; time.Now().Before(deadline); tries++ {
        _, err := http.Head(url)
        if err == nil {
            return nil // success
        }
        log.Printf("server not responding (%s);retrying…", err)
        time.Sleep(time.Second << uint(tries)) // exponential back-off
    }
    return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

處理錯誤的第三種策略:輸出錯誤信息並結束程序。需要注意的是,這種策略只應在main中執行。對庫函數而言,應僅向上傳播錯誤,除非該錯誤意味着程序內部包含不一致性,即遇到了bug,才能在庫函數中結束程序

// (In function main.)
if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)
}
或
log.Fatalf("Site is down: %v\n", err)

我們應該在每次函數調用後,都養成考慮錯誤處理的習慣,當你決定忽略某個錯誤時,你應該在清晰的記錄下你的意圖

在Go中,錯誤處理有一套獨特的編碼風格。檢查某個子函數是否失敗後,我們通常將處理失敗的邏輯代碼放在處理成功的代碼之前。如果某個錯誤會導致函數返回,那麼成功時的邏輯代碼不應放在else語句塊中,而應直接放在函數體中。Go中大部分函數的代碼結構幾乎相同,首先是一系列的初始檢查,防止錯誤發生,之後是函數的實際邏輯

在Go中,函數被看作第一類值(first-class values):函數像其他值一樣,擁有類型,可以被賦值給其他變量,傳遞給函數,從函數返回。對函數值(function value)的調用類似函數調用

fmt.Printf的一個小技巧控制輸出的縮進。%*s中的*會在字符串之前填充一些空格。在例子中,每次輸出會先填充depth*2數量的空格,再輸出"",最後再輸出HTML標籤
fmt.Printf("%*s</%s>\n", depth*2, "", n.Data)

func squares() func() int {
    var x int
    return func() int {
        x++
        return x * x
    }
}
func main() {
    f := squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    f := squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
}

函數squares返回另一個類型爲 func() int 的函數。對squares的一次調用會生成一個局部變量x並返回一個匿名函數。每次調用時匿名函數時,該函數都會先使x的值加1,再返回x的平方。第二次調用squares時,會生成第二個x變量,並返回一個新的匿名函數。新匿名函數操作的是第二個x變量。

squares的例子證明,函數值不僅僅是一串代碼,還記錄了狀態。在squares中定義的匿名內部函數可以訪問和更新squares中的局部變量,這意味着匿名函數和squares中,存在變量引用。這就是函數值屬於引用類型和函數值不可比較的原因。Go使用閉包(closures)技術實現函數值,Go程序員也把函數值叫做閉包。

當匿名函數需要被遞歸調用時,我們必須首先聲明一個變量(在上面的例子中,我們首先聲明瞭 visitAll),再將匿名函數賦值給這個變量。如果不分成兩部,函數字面量無法與visitAll綁定,我們也無法遞歸調用該匿名函數。

visitAll := func(items []string) {
    // ...
    visitAll(m[item]) // compile error: undefined: visitAll
    // ...
}
而是
var visitAll func(items []string)
visitAll = func(items []string) {
    // ...
    visitAll(m[item])
    // ...
}
var rmdirs []func()

for _, d := range tempDirs() {
    dir := d // NOTE: necessary!
    os.MkdirAll(dir, 0755) // creates parent directories too
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir)
    })
}

for _, rmdir := range rmdirs {
    rmdir() // clean up
}

問題的原因在於循環變量的作用域。在上面的程序中,for循環語句引入了新的詞法塊,循環變量dir在這個詞法塊中被聲明。在該循環中生成的所有函數值都共享相同的循環變量。需要注意,函數值中記錄的是循環變量的內存地址,而不是循環變量某一時刻的值。以dir爲例,後續的迭代會不斷更新dir的值,當刪除操作執行時,for循環已完成,dir中存儲的值等於最後一次迭代的值。這意味着,每次對os.RemoveAll的調用刪除的都是相同的目錄。

通常,爲了解決這個問題,我們會引入一個與循環變量同名的局部變量,作爲循環變量的副本。比如下面的變量dir,雖然這看起來很奇怪,但卻很有用。

這個問題不僅存在基於range的循環,在下面的例子中,對循環變量i的使用也存在同樣的問題

var rmdirs []func()
dirs := tempDirs()
for i := 0; i < len(dirs); i++ {
    os.MkdirAll(dirs[i], 0755) // OK
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dirs[i]) // NOTE: incorrect!
    })
}

如果你使用go語句(第八章)或者defer語句(5.8節)會經常遇到此類問題。這不是go或defer本身導致的,而是因爲它們都會等待循環結束後,再執行函數值。

在聲明可變參數函數時,需要在參數列表的最後一個參數類型之前加上省略符號“…”,這表示該函數會接收任意數量的該類型參數。

func sum(vals...int) int {
    total := 0
    for _, val := range vals {
        total += val
    }
    return total
}

fmt.Println(sum())           // "0"
fmt.Println(sum(1, 2, 3, 4)) // "10"

在上面的代碼中,調用者隱式的創建一個數組,並將原始參數複製到數組中,再把數組的一個切片作爲參數傳給被調函數。如果原始參數已經是切片類型,我們該如何傳遞給sum?只需在最後一個參數後加上省略符

values := []int{1, 2, 3, 4}
fmt.Println(sum(values...)) // "10"

調試複雜程序時,defer機制也常被用於記錄何時進入和退出函數。下例中的bigSlowOperation函數,直接調用trace記錄函數的被調情況。bigSlowOperation被調時,trace會返回一個函數值,該函數值會在bigSlowOperation退出時被調用。通過這種方式, 我們可以只通過一條語句控制函數的入口和所有的出口,甚至可以記錄函數的運行時間,如例子中的start。需要注意一點:不要忘記defer語句後的圓括號,否則本該在進入時執行的操作會在退出時執行,而本該在退出時執行的,永遠不會被執行。

func bigSlowOperation() {
    defer trace("bigSlowOperation")() // don't forget the
    extra parentheses
    // ...lots of work…
    time.Sleep(10 * time.Second) // simulate slow
    operation by sleeping
}
func trace(msg string) func() {
    start := time.Now()
    log.Printf("enter %s", msg)
    return func() { 
        log.Printf("exit %s (%s)", msg,time.Since(start)) 
    }
}

我們知道,defer語句中的函數會在return語句更新返回值變量後再執行,又因爲在函數中定義的匿名函數可以訪問該函數包括返回值變量在內的所有變量,所以,對匿名函數採用defer機制,可以使其觀察函數的返回值。

func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d\n", x,result) }()
    return x + x
}
_ = double(4)
// Output:
// "double(4) = 8"

被延遲執行的匿名函數甚至可以修改函數返回給調用者的返回值:

func triple(x int) (result int) {
    defer func() { result += x }()
    return double(x)
}
fmt.Println(triple(4)) // "12"

在每一個合法的方法調用表達式中,也就是下面三種情況裏的任意一種情況都是可以的:
不論是接收器的實際參數和其接收器的形式參數相同,比如兩者都是類型T或者都是類型*T:

Point{1, 2}.Distance(q) //  Point
pptr.ScaleBy(2)         // *Point

或者接收器形參是類型T,但接收器實參是類型*T,這種情況下編譯器會隱式地爲我們取變量的地址:
p.ScaleBy(2) // implicit (&p)
或者接收器形參是類型*T,實參是類型T。編譯器會隱式地爲我們解引用,取到指針指向的實際變量:
pptr.Distance(q) // implicit (*pptr)

如果類型T的所有方法都是用T類型自己來做接收器(而不是*T),那麼拷貝這種類型的實例就是安全的;調用他的任何一個方法也就會產生一個值的拷貝。比如time.Duration的這個類型,在調用其方法時就會被全部拷貝一份,包括在作爲參數傳入函數的時候。但是如果一個方法使用指針作爲接收器,你需要避免對其進行拷貝,因爲這樣可能會破壞掉該類型內部的不變性。比如你對bytes.Buffer對象進行了拷貝,那麼可能會引起原始對象和拷貝對象只是別名而已,但實際上其指向的對象是一致的。緊接着對拷貝後的變量進行修改可能會有讓你意外的結果。
譯註: 作者這裏說的比較繞,其實有兩點:
不管你的method的receiver是指針類型還是非指針類型,都是可以通過指針/非指針類型進行調用的,編譯器會幫你做類型轉換。
在聲明一個method的receiver該是指針還是非指針類型時,你需要考慮兩方面的內部,第一方面是這個對象本身是不是特別大,如果聲明爲非指針變量時,調用會產生一次拷貝;第二方面是如果你用指針類型作爲receiver,那麼你一定要注意,這種指針類型指向的始終是一塊內存地址,就算你對其進行了拷貝。熟悉C或者C艹的人這裏應該很快能明白。

在類型中內嵌的匿名字段也可能是一個命名類型的指針,這種情況下字段和方法會被間接地引入到當前的類型中(譯註:訪問需要通過該指針指向的對象去取)。添加這一層間接關係讓我們可以共享通用的結構並動態地改變對象之間的關係

type ColoredPoint struct {
    *Point
    Color color.RGBA
}

p := ColoredPoint{&Point{1, 1}, red}
q := ColoredPoint{&Point{5, 4}, blue}
fmt.Println(p.Distance(*q.Point)) // "5"
q.Point = p.Point                 // p and q now share the same Point
p.ScaleBy(2)
fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"

一個struct類型也可能會有多個匿名字段

type ColoredPoint struct {
    Point
    color.RGBA
}

然後這種類型的值便會擁有Point和RGBA類型的所有方法,以及直接定義在ColoredPoint中的方法。當編譯器解析一個選擇器到方法時,比如p.ScaleBy,它會首先去找直接定義在這個類型裏的ScaleBy方法,然後找被ColoredPoint的內嵌字段們引入的方法,然後去找Point和RGBA的內嵌字段引入的方法,然後一直遞歸向下找。如果選擇器有二義性的話編譯器會報錯,比如你在同一級裏有兩個同名的方法

下面這個例子展示了簡單的cache,其使用兩個包級別的變量來實現,一個mutex互斥量和它所操作的cache:

var (
    mu sync.Mutex // guards mapping
    mapping = make(map[string]string)
)

func Lookup(key string) string {
    mu.Lock()
    v := mapping[key]
    mu.Unlock()
    return v
}
等價
var cache = struct {
    sync.Mutex
    mapping map[string]string
}{
    mapping: make(map[string]string),
}


func Lookup(key string) string {
    cache.Lock()
    v := cache.mapping[key]
    cache.Unlock()
    return v
}

我們經常選擇一個方法,並且在同一個表達式裏執行,比如常見的p.Distance()形式,實際上將其分成兩步來執行也是可能的。p.Distance叫作“選擇器”,選擇器會返回一個方法”值”->一個將方法(Point.Distance)綁定到特定接收器變量的函數。這個函數可以不通過指定其接收器即可被調用;即調用時不需要指定接收器(譯註:因爲已經在前文中指定過了),只要傳入函數的參數即可:

p := Point{1, 2}
q := Point{4, 6}

distanceFromP := p.Distance        // method value
fmt.Println(distanceFromP(q))      // "5"
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })
等價
time.AfterFunc(10 * time.Second, r.Launch)

當T是一個類型時,方法表達式可能會寫作T.f或者(*T).f,會返回一個函數”值”,這種函數會將其第一個參數用作接收器,所以可以用通常(譯註:不寫選擇器)的方式來對其進行調用:

p := Point{1, 2}
q := Point{4, 6}

distance := Point.Distance   // method expression
fmt.Println(distance(p, q))  // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章