Go語言程式設計快速入門

huihui_teresa發表於2024-06-02

Go語言程式設計快速入門

這個是學習B站楊旭影片做的記錄,地址

安裝

  1. https://studygolang.com/
  2. VsCode 安裝 Go 外掛
  3. ctrl+shift+p: 輸入go查詢,選擇 Install/Update Tools,全部勾選,點選OK
  4. Go代理(執行命令後重啟vscode)
    #控制檯執行命令
    go env -w GO111MODULE=on
    go env -w GOPROXY=https://goproxy.cn,direct
    

switch

還有一個fallthrough關鍵字,它用來執行下一個case的body部分。(區別c#不需要關鍵字)

變數和作用域

短宣告

  • 在Go裡,可以使用var來宣告變數

  • 但是也可以使用短宣告

    count :=10
    
  • 不僅宣告語句更短,而且可以在無法使用var的地方使用

    for count := 10; count > 0; count-- {
    	fmt.Println(count)
    }
    

整數型別

uint8

  • uint8可以用來表示8位的顏色(紅綠藍:0-255)

    var red, green, blue uint8 = 0, 141, 213
    

十六進位制表示法

  • Go語言裡,在數前面加上0x字首,就可以用十六進位制的形式來表示數值

    var red, green, blue uint8 = 0, 141, 213
    var red, green, blue uint8 = 0x00, 0x8d, 0xd5
    

整數環繞

  • 所有的整數型別都有一個取值範圍,超出這個範圍,就會發生“環繞”

整數型別的最大值、最小值

  • math包裡,為與架構無關的整數型別,定義了最大、最小值常量:

    math.MaxInt16
    math.MinInt64
    
  • 而int和uint,可能是32或64位的。

比較大的數

數太大了怎辦?

  • 浮點型別可以儲存非常大的數值,但是精度不高
  • 整型很精確,但是取值範圍有限
  • 如果你需要很大的數,而且要求很精確,那麼怎麼辦
    • nt64可以容納很大的數,如果還不行,那麼:
    • int64可以容納更大的正數,如果還不行,那麼:
    • 也可以湊合用浮點型別,但是還有另外一種方法:
  • 使用big包。

big包

  • 對於較大的整數(超過1018):big.lnt
  • 對於任意精度的浮點型別,big.Float
  • 對於分數,big.Rat
  • 缺點:用起來繁瑣,速度比較慢

多語言文字

字串字面值/原始字串字面值

  • 字串字面值可以包含跳脫字元,例如 \n
  • 但如果你確實想得到\n而不是換行的話,可以使用`來代替“,這叫做原始字串字面值。

字元,code points,runes,bytes

  • Unicode聯盟為超過100萬個字元分配了相應的數值,這個數叫做code point.
    • 例如:65代表A
  • 為了表示這樣的unicode code point,Go語言提供了rune這個型別,它是int32的一個型別別名。
  • 而byte是uint8型別的別名,目的是用於二進位制資料。
    • byte 倒是可以表示由ASCIl 定義的英語字元,它是Unicode 的一個子集(共128個字元)

型別別名

  • 型別別名就是同一個型別的另一個名字。

    • 所以,rune和int32可以互換使用。
  • 也可以自定義型別別名,語法如下:

    type byte = uint8
    type rune = int32
    

列印

  • 如果想列印字元而不是數值,使用%c格式化動詞
  • 任何整數型別都可以使用%c列印,但是rune意味著該數值表示了一個字元

Go的內建函式

  • len是Go語言的一個內建函式。

    str := "hello word"
    fmt.Println(len(str))
    
  • 本例中 len 返回 message所佔的byte 數。

  • Go有很多內建函式,它們不需要import

  • 使用utf-8包,它提供可以按rune計算字串長度的方法。

  • ecodeRunelnString函式會返回第一個字元,以及字元所佔的位元組數。

    str := "hello word,你好呀"
    fmt.Println(len(str), "bytes")
    fmt.Println(utf8.RuneCountInString(str), "runes")
    c, size := utf8.DecodeRuneInString(str)
    fmt.Printf("First runs: %c %v bytes", c, size)
    

range

  • ·使用range關鍵字,可以遍歷各種集合

    str := "hello word,你好呀"
    for i, c := range str {
        fmt.Printf("%v %c\n", i, c)
    }
    

型別轉換

型別轉換時需謹慎

  • 環繞行為
  • 可以透過math包提供的max、min 常量,來判斷是否超過最大最小值

字串轉換

  • 想把數值轉化為string,它的值必須能轉化為codepoint。string(65)

  • trconv包的Itoa函式:

    num := 10
    fmt.Printf("數字是:" + strconv.Itoa(num))
    
  • 另外一種把數值轉化為string的方式是使用Sprintf函式,和Printf略類似,但是會返回一個string:

    countdown := 9
    str := fmt.Sprintf("Launch in T minus %v seconds.", countdown)
    fmt.Println(str)
    
  • strconv包裡面還有個Atoi(ASCll to Integer)函式。

  • 由於字串裡面可能包含任意字元,或者要轉換的數字字串太大,所以Atoi函式可能會發生錯誤:

    countdown,err:=strconv.Atoi("10")
    if err!=nil{
    // oh no,something went wrong
    }
    fmt.Println(countdown)
    
  • 如果err的值為nil,那麼就代表沒發生錯誤。

函式

函式宣告

  • 使用func關鍵字宣告函式

    func Intn(n int) int
    
  • 在Go裡,大寫字母開頭的函式、變數或其它識別符號都會被匯出,對其它包可用。

  • 小寫字母開頭的就不行(私有函式吧)

函式宣告-多個引數

  • 函式的引數可以是多個:

    • func Unix(sec int64,nsec int64) Time
    • 呼叫:future := time.Unix(12622780800,0)
  • 函式宣告時,如果多個形參型別相同,那麼該型別只寫一次即可:

    • func Unix(sec int64,nsec int64)Time
    • func Unix(sec,nsec int64)Time
    • 這種簡化是可選的

函式宣告-返回多個值

  • Go的函式可以返回多個值:
    • cuntdown, err := strconv.Atoi(10")
  • 該函式的宣告如下:
    • func Atoi(s string) (i int, err error)
  • 函式的多個返回值需要用括號括起來,每個返回值名字在前,型別在後。宣告函式時可以把名字去掉,只保留型別:
    • func Atoi(s string) (int, error)

函式宣告-可變引數函式

  • Println是一個特殊的函式,它可以接收一個、二個甚至多個引數,引數型別還可以不同:

    fmt.Println("Hello,playground")
    fmt.Println(186,"seconds")
    
  • Println的宣告是這樣的:

    • func Println(a ...interface) (n int, err error)
    • ….表示函式的引數的數量是可變的。
    • 引數a的型別為interface{},是一個空介面。
  • ... 和空介面組合到一起就可以接受任意數量、型別的引數了

方法

宣告新型別

  • 關鍵字type可以用來宣告新型別:

    type celsis float64
    var temperature celsis = 20
    
  • 雖然Celsius是一種全新的型別,但是由於它和float64具有相同的行為和表示,所以賦值操作能順利執行。

  • 為什麼要宣告新型別:極大的提高程式碼可讀性和可靠性

  • 不同的型別是無法混用的

透過方法新增行為

  • 在C#、Java裡,方法屬於類

  • 在Go裡,它提供了方法,但是沒提供類和物件

  • Go比其他語言的方法要靈活

  • 可以將方法與同包中宣告的任何型別相關聯,但不可以是int、float64等預宣告的型別進行關聯。

    type celsius float64
    type kelvin float64
    // 函式
    func kelvinToCelsius(k kelvin) celsius {
    	return celsius(k - 273.15)
    }
    // 方法
    func (k kelvin) celsius() celsius {
    	return celsius(k - 273.15)
    }
    
  • 上例中,celsius方法雖然沒有引數。但它前面卻又一個型別引數的接收者。

  • 每個方法可以有多個引數,但只能有一個接收者。

  • 在方法體中,接收者的行為和其它引數一樣。

一等函式

  • 在Go裡,函式是頭等的,它可以用在整數、字串或其它型別能用的地方:
    • 將函式賦給變數
    • 將函式作為引數傳遞給函式
    • 將函式作為函式的返回型別

將函式賦給變數

type kelvin float64

func fakeSensor() kelvin {
	return kelvin(rand.Intn(151) + 150)
}

func realSensor() kelvin {
	return 0
}

func main() {
	sensor := fakeSensor
	fmt.Println(sensor())

	sensor = realSensor
	fmt.Println(sensor())
}

將函式傳遞給其他函式

import (
	"fmt"
	"math/rand"
	"time"
)
type kelvin float64
func measureTemperature(samples int, sensor func() kelvin) {
	for i := 0; i < samples; i++ {
		k := sensor()
		fmt.Printf("%v K\n", k)
		time.Sleep(time.Second)
	}
}
func fakeSensor() kelvin {
	return kelvin(rand.Intn(151) + 150)
}
func main() {
	measureTemperature(3, fakeSensor)
}

宣告函式型別

  • 為函式宣告型別有助於精簡和明確呼叫者的程式碼。

    • 例如:type sensor func() kelvin

    • 所以: func measureTemperature(samples int,s func() kelvin)

    • 可以精簡為:func measureTemperature(samples int,s sensor)

閉包和匿名函式

  • 匿名函式就是沒有名字的函式,在Go裡也稱作函式字面值。

  • 因為函式字面值需要保留外部作用域的變數引用,所以函式字面值都是閉包的。‘

    import "fmt"
    
    type kelvin float64
    
    // sensor function type
    type sensor func() kelvin
    
    func realSensor() kelvin {
    	return 0
    }
    
    func calibrate(s sensor, offset kelvin) sensor {
    	return func() kelvin {
    		return s() + offset
    	}
    }
    
    func main() {
    	sensor := calibrate(realSensor, 5)
    	//這裡,雖然已經返回函式。但是引數已經被封在裡面,下面執行仍然可以訪問
    	fmt.Println(sensor())
    }
    
    
  • 閉包(closure)就是由於匿名函式封閉幷包圍作用域中的變數而得名的

陣列

  • 陣列是一種固定長度且有序的元素集合。

    var planets [8]string
    
  • 陣列中的每個元素都可以透過1和一個從0開始的索引進行訪問。

  • 陣列的長度可由內建函式len來確定。

  • 在宣告陣列時,未被賦值元素的值是對應型別的零值

陣列越界

  • Go編譯器在檢測到對越界元素的訪問時會報錯
  • 如果Go編譯器在編譯時未能發現越界錯誤,那麼程式在執行時會出現panic
  • Panic會導致程式崩潰

使用複合字面值初始化陣列

  • 複合字面值(compositeliteral)是一種給複合型別初始化的緊語法。

  • go的複合字面值語法允許我們只用一步就完成陣列宣告和陣列初始化兩步操作:

    dwarfs := [5]string{"1", "2", "3", "4", "5"}
    
  • 可以在複合字面值裡使用…作為陣列的長度,這樣Go編譯器會為你算出陣列的元素數量

    planets := [...]string{	"1", "2",}
    
  • 無論哪種方式,陣列的長度都是固定的。

遍歷陣列

  • for迴圈
  • range
dwarfs := [5]string{"1", "2", "3", "4", "5"}

for i := 0; i < len(dwarfs); i++ {
    fmt.Println(i, dwarfs[i])
}

for i, dwarf := range dwarfs {
    fmt.Println(i, dwarf)
}

陣列的複製

  • 無論陣列賦值給新的變數還是將它傳遞給函式,都會產生一個完整的陣列副本。(c#的值型別?
  • 陣列也是一種值,函式透過值傳遞來接受引數。所以陣列作為函式的引數就非常低效
  • 陣列的長度也是陣列型別的一部分
    • 嘗試將長度不符的陣列作為引數傳遞,將會報錯。
  • 函式一般使用slice而不是陣列作為引數。

Slice(切片)

Slice指向陣列的視窗

  • 假設planets是一個陣列,那麼planets[0:4]就是一個切片,它切分出了陣列裡前4個元素。
  • 切分陣列不會導致陣列被修改,它只是建立了指向陣列的一個視窗或檢視,這種檢視就是slice型別。
  • Slice使用的是半開區間
    • 例如planets[0:4],包含索引0、1、2、3對應的元素,不包括索引4對應的元素。

Slice的預設索引

  • 忽略掉slice的起始索引,表示從陣列的起始位置進行切分;
  • 忽略掉slice的結束索引,相當於使用陣列的長度作為結束索引。
  • 注意:slice的索引不能是負數。
  • 如果同時省略掉起始和結束索引,那就是包含陣列所有元素的一個slice.
  • 切分陣列的語法也可以用於切分字串
    • 切分字串時,索引代表的是位元組數而非rune的數。

Slice的複合字面值

  • Go裡面很多函式都傾向於使用slice而不是陣列作為引數。

  • 想要獲得與底層陣列相同元素的slice,那麼可以使用[:]進行切分

  • 切分陣列並不是建立slice唯一的方法,可以直接宣告slice:

    slices := []string{"1", "2"}
    

更大的slice

append函式

  • append函式也是內建函式,它可以將元素新增到slice裡面。

    slices := []string{"1", "2"}
    slices = append(slices, "3","4")
    

長度和容量(length & capacity)

  • slice中元素的個數決定了slice的長度。
  • 如果slice的底層陣列比slice還大,那麼就說該slice還有容量可供增長。

三個索引的切分操作

  • G01.2中引入了能夠限制新建切片容量的三索引切分操作。

    planets := []string{"1", "2", "3", "4", "5", "6"}
    terrestrial := planets[0:4]
    words := append(terrestrial, "100")
    fmt.Println(planets, terrestrial, words)
    
  • 上面執行append會修改planets。如果不想這樣,可以指定長度,append後會複製到一個新的資料。terrestrial := planets[0:44]

使用make 函式對slice進行預分配

  • 當slice的容量不足以執行append操作時,Go必須建立新陣列並複製舊陣列中的內容。
  • 但透過內建的make函式,可以對slice進行預分配策略。
    • 儘量避免額外的記憶體分配和陣列複製操作。
dwarfts:=make([]string, 0,10)
dwarfts:=make([]string, 10)

宣告可變引數的函式

  • 宣告Printf、append這樣的可變引數函式,需要在函式的最後一個引數前面加上符號。

    import "fmt"
    
    func terraform(prefix string, wolds ...string) []string {
    	newWorlds := make([]string, len(wolds))
    
    	for i := range wolds {
    		newWorlds[i] = prefix + " " + wolds[i]
    	}
    
    	return newWorlds
    }
    
    func main() {
    	twoWorlds := terraform("New", "Venus", "Mars")
    	fmt.Println(twoWorlds)
    
    	planets := []string{"Venus", "Mars", "Jupiter"}
    	newPlanets := terraform("new", planets...)
    	fmt.Println(newPlanets)
    }
    

Map(c#的字典)

  • Map是Go提供的另外一種集合:
    • 它可以將key對映到value。
    • 可快速透過key找到對應的value
    • 它的key幾乎可以是任何型別

宣告Map

  • 宣告map,必須指定key和value的型別:

    temperature := map[string]int{
        "Earth": 15,
        "Mars":  -65,
    }
    

逗號與ok寫法

temperature := map[string]int{
    "Earth": 15,
    "Mars":  -65,
}

//沒有的話,這樣會新增一個key,value
temperature["Venus"] = 100

if moon, ok := temperature["Moon"]; ok {
	fmt.Println("on average the moon is %v C.\n", moon)
} else {
	fmt.Println("Where is the moon?")
}

map 不會被複制

  • 陣列、int、float64等型別在賦值給新變數或傳遞至函式l方法的時候會建立相應的副本
  • 但map不會(c#的引用型別)

使用 make 函式對 map 進行預分配

  • 除非你使用複合字面值來初始化map,否則必須使用內建的make函式來為map分配空間。

    dic := make(map[string]int, 8)
    
  • 建立map時,make函式可接受一個或兩個引數

    • 第二個引數用於為指定數量的key預先分配空間
  • 使用make函式建立的map的初始長度為0

使用 map 和 slice 實現資料分組

temperature := []float64{
	-28.0, 32.0, -31.0, -29.0, -23.0, -29.0, -28.0, -33.0,
}

groups := make(map[float64][]float64)

for _, t := range temperature {
	g := math.Trunc(t/10) * 10
	groups[g] = append(groups[g], t)
}

for g, temperature := range groups {
	fmt.Println("%v: %v\n", g, temperature)
}

結構型別(struct)

  • 為了將分散的零件組成一個完整的結構體,Go提供了struct型別。
  • struct允許你將不同型別的東西組合在一起。
var curiosity struct {
	lat  float64
	long float64
}

透過型別複用結構體

type location struct {
	lat  float64
	long float64
} 
var spirit location
spirit.lat = 1
spirit.long = 2

透過複合字面值初始化struct

  • 透過成對的欄位和值進行初始化

    type location struct {
    	lat, long float64
    }
    
    opportunitu := location{lat: 1, long: 2}
    
  • 按欄位定義的順序進行初始

    spirit = location{1, 2}
    

由結構體組成的slice

type location struct {
	name      string
	lat, long float64
}
locations := []location{
	{name: "aa", lat: 1, long: 2},
	{name: "bb", lat: 1, long: 2},
}

將struct編碼為JSON

  • Json包的Marshal函式可以將struct中的資料轉化為JsoN格式

    type location struct {
    	Lat, Long float64
    }
    
    curiosity := location{1, 2}
    bytes, _ := json.Marshal(curiosity)
    fmt.Println(string(bytes))
    
  • Marshal函式只會對struct中被匯出的欄位進行編碼。

    使用struct標籤來自定義JSON

  • go語言中的json 包要求struct 中的欄位必須以大寫字母開頭,類似CamelCase駝峰型命名規範。

  • 可以為欄位註明標籤,使得ison包在進行編碼的時候能夠按照標籤裡的樣式修改欄位名

    type location struct {
    	Lat float32 `json:"latitude"`
    	Long float64 `json:longitude`
    }
    

Go語言沒有class

  • Go和其它經典語言不同,它沒有class,沒有物件,也沒有繼承。
  • 但是Go提供了struct和方法。

建構函式

  • 可以使用struct複合字面值來初始化你所要的資料。

  • 但如果 struct初始化的時候還要做很多事情,那就可以考慮寫一個構造用的函式

  • Go語言沒有專用的建構函式,但以new或者New開頭的函式,通常是用來構造資料的。例如newPerson(),NewPerson()

    type location struct{
    	lat,long float64
    }
    //初始化使用
    func newLocation(lat,long float64) location {
    	return location{lat,long}
    }
    

New函式

  • 有一些用於構造的函式的名稱就是New(例如errors包裡面的New函式)。
  • 這是因為函式呼叫時使用包名.函式名的形式。
  • 如果該函式叫NewError,那麼呼叫的時候就是errors.NewError(),這就不如errors.New()簡潔

組合與轉發

組合

  • 在物件導向的世界中,物件由更小的物件組合而成。
  • 術語:物件組合或組合
  • Go透過結構體實現組合(composition)。
  • Go提供了“嵌入”(embedding)特性,它可以實現方法的轉發(forwarding)
  • 組合是一種更簡單、靈活的方式。

組合機構體

type report struct {
	sol         int
	temperature temperature
	location    location
}

type temperature struct {
	high, low celsius
}

type location struct {
	lat, long float64
}

type celsius float64

轉發方法

  • Go可以透過struct嵌入來實現方法的轉發。
  • 在struct中只給定欄位型別,不給定欄位名即可
  • 在struct中,可以轉發任意型別
type report struct {
	sol int
	temperature
	location
}

type temperature struct {
	high, low celsius
}

type location struct {
	lat, long float64
}

type celsius float64

func (t temperature) average() celsius {
	return (t.high + t.low) / 2
}

func main() {
	bradbury := location{-4.5895, 137.4417}
	t := temperature{high: -1.0, low: -78}
	report := report{
		sol:         15,
		temperature: t,
		location:    bradbury,
	}
	//report可以直接呼叫temperature的方法
	fmt.Print(report.average())
	//也可以直接訪問欄位
	fmt.Print(report.high)
}

繼承 還是 組合

  • 優先使用物件組合而不是類的繼承。
  • 對傳統的繼承不是必需的;所有使用繼承解決的問題都可以透過其它方法解決。

介面

  • 介面關注於型別可以做什麼,而不是儲存了什麼。
  • 介面透過列舉型別必須滿足的一組方法來進行宣告。
  • 在Go語言中,不需要顯式宣告介面。

介面型別

  • 為了複用,通常會把介面宣告為型別。
  • 按約定,介面名稱通常以er結尾。
  • 介面可以與struct嵌入特性一同使用
  • 同時使用組合和介面將構成非常強大的設計工具。
import "fmt"

type talker interface {
	talk() string
}
type martain struct{}

func (m martain) talk() string {
	return "nack nack"
}

func shout(t talker) {
	fmt.Println(t.talk())
}

type starship struct {
	martain
}

func main() {
	shout(martain{})

	s := starship{martain{}}
	fmt.Println(s.talk())
}

滿足介面

  • go標準庫匯出了很多隻有單個方法的介面。

  • go透過簡單的、通常只有單個方法的介面….來鼓勵組合而不是繼承,這些介面在各個元件之間形成了簡明易懂的界限。

  • 例如fmt包宣告的Stringer介面:

    type Stringer interface{
        String() string
    }
    
  • 標準庫中常用介面還包括:io.Reader,io.Writer,json.Marshaler...

指標

什麼是指標

  • 指標是指向另一個變數地址的變數。
  • Go語言的指標同時也強調安全性,不會出現迷途指標(danglingpointers)

& 和 * 符號

  • 變數會將它們的值儲存在計算機的RAM裡,儲存位置就是該變數的記憶體地址。
  • & 表示地址運算子,透過 & 可以獲得變數的記憶體地址
  • & 作符無法獲得字串/數值/布林字面值的地址。
    • &42,&“hello”這些都會導致編譯器報錯
    • 運算子與 & 的作用相反,它用來解引用,提供記憶體地址指向的值。

注意

  • C語言中的記憶體地址可以透過例如 address++ 這樣的指標運算進行操作,但是在Go裡面不允許這種不安全操作。

記憶體型別

  • 指標儲存的是記憶體地址
  • 指標型別和其它普通型別一樣,出現在所有需要用到型別的地方,如變數宣告、函式形參、返回值型別、結構體欄位等
  • 將*放在型別前面表示宣告指標型別
  • 將*放在變數前面表示解引用操作

指標就是用來指向的

  • 兩個指標變數持有相同的記憶體地址,那麼它們就是相等。
func main() {
	var administrator *string

	scolese := "huihui scolese"
	administrator = &scolese
	fmt.Println(*administrator)

	bolden := "huihui bolden"
	administrator = &bolden
	fmt.Println(*administrator)

	bolden = "huihui bolden change"
	fmt.Println(*administrator)

	*administrator = "Maj. Gen. Charles Frank Bolden Jr."
	fmt.Println(bolden)

	major := administrator
	*major = "hahaha"
	fmt.Print(bolden)

	fmt.Println(administrator == major)

}

指向結構的指標

  • 與字串和數值不一樣,複合字面量的前面可以放置&
  • 訪問欄位時,對結構體進行解引用並不是必須
func main() {

	type person struct {
		name, superpower string
		age              int
	}

	timmy := &person{
		name: "huihui",
		age:  10,
	}

	(*timmy).superpower = "aaa"
	timmy.superpower = "bbb"

	fmt.Print(timmy)

}

指向陣列的指標

  • 和結構體一樣,可以把&放在陣列的複合字面值前面來建立指向陣列的指標。

    func main() {
    	superpowers := &[3]string{"1", "2", "3"}
    
    	fmt.Println(superpowers[0])
    	fmt.Println(superpowers[1:2])
    }
    
  • 陣列在執行索引或切片操作時會自動解引用。沒有必要寫(*superpower)[o]這種形式。

  • 與C語言不一樣,Go裡面陣列和指標式兩種完全獨立的型別。

  • Slice和map的複合字面值前面也可以放置&運算子,但是Go並沒有為它們提供自動解引用的功能。

實現修改

  • Go語言的函式和方法都是按值傳遞引數的,這意味著函式總是操作於被傳遞引數的副本。
  • 當指標被傳遞到函式時,函式將接收傳入的記憶體地址的副本。之後函式可以透過解引用記憶體地址來修改指標指向的值。

指標接收者

  • 方法的接收者和方法的引數在處理指標方面是很相似的。
  • Go語言在變數透過點標記法進行呼叫的時候,自動使用&取得變數的記憶體地址。
    • 所以不用寫(&anathan).birthday()這種形式也可以正常執行。
type person struct {
	name string
	age  int
}

func (p *person) birthday() {
	p.age++
}

func main() {
	terry := &person{
		name: "terry",
		age:  15,
	}

	terry.birthday()
	fmt.Println(terry)

	mathan := person{
		name: "Nathan",
		age:  7,
	}

	mathan.birthday()
	fmt.Println(mathan)
}

注意

  • 使用指標作為接收者的策略應該始終如一:
  • 如果一種型別的某些方法需要用到指標作為接收者,就應該為這種型別的所有方法都是用指標作為接收者。

內部指標

  • Go語言提供了內部指標這種特性。
  • 它用於確定結構體中指定欄位的記憶體地址。
  • & 運算子不僅可以獲得結構體的記憶體地址,還可以獲得結構體中指定欄位的記憶體地址。&animal.dog

修改陣列

  • 函式透過指標對陣列的元素進行修改。

    func reset(board *[8][8]rune) {
    	board[0][0] = 'r'
    }
    
    func main() {
    	var board [8][8]rune
    	reset(&board)
    	fmt.Println(board[0][0])
    }
    

隱式的指標

  • Go語言裡一些內建的集合型別就在暗中使用指標。
  • map在被賦值或者唄作為引數傳遞的時候不會被複制。
    • map就是一種隱式指標。
    • 這種寫法就是多此一舉:func demolish(planets *map[string]string)
  • map的鍵和值都可以是指標型別
  • 需要將指標指向map的情況並不多見

slice 指向陣列

  • 之前說過 slice是指向陣列的視窗,實際上slice在指向陣列元素的時候也使用了指標。

  • 每個slice內部都會被表示為一個包含3個元素的結構,它們分別指向:

    • 陣列的指標
    • slice 的容量
    • slice的長度
  • 當slice被直接傳遞至函式或方法時,slice的內部指標就可以對底層資料進行修改。

  • 指向 slice 的顯式指標的唯一作用就是修改slice 本身:slice的長度、容量以及起始偏移量。

    func reclassify(planets *[]string) {
    	*planets = (*planets)[0:2]
    }
    
    func main() {
    	planets := []string{
    		"Mercury", "Venus", "Earth", "Mars", "Pluto",
    	}
    	reclassify(&planets)
    	fmt.Println(planets)
    }
    
  • 注意:slice超長後,會複製出一個新的

指標和介面

  • 本例中,無論martian還是指向martian的指標,都可以滿足talker介面。
  • 如果方法使用的是指標接收者,那麼情況會有所不同。
import (
	"fmt"
	"strings"
)

type talker interface {
	talk() string
}

func shout(t talker) {
	louder := strings.ToUpper(t.talk())
	fmt.Println(louder)
}

type martian struct{}

func (m martian) talk() string {
	return "nack nack"
}

func main() {
	shout(martian{})
	shout(&martian{})
}

明智的使用指標

  • 應合理使用指標,不要過度使用指標。

nil

  • Ni是一個名詞,表示“無”或者“零”
  • 在Go裡,nil是一個零值。
  • 如果一個指標沒有明確的指向,那麼它的值就是nil
  • 除了指標,nil還是slice、map和介面的零值。
  • Go語言的nil,比以往語言中的null 更為友好,並且用的沒那麼頻繁,但是仍需謹慎使用。

nil 會導致 panic

  • 如果指標沒有明確的指向,那麼程式將無法對其實施的解引用。
  • 嘗試解引用一個nil指標將導致程式崩潰。

保護你的方法

  • 避免nil引發panic
  • 因為值為nil的接收者和值為nil的引數在行為上並沒有區別,所以Go語言即使在接收者為nil的情況下,也會繼續呼叫方法。

nil 函式值

  • 當變數被宣告為函式型別時,它的預設值是nil。
  • 檢查函式值是否為nil,並在有需要時提供預設行為。

nil slice

  • 如果slice在宣告之後沒有使用複合字面值或內建的make函式進行初始化,那麼它的值就是nil。
  • 幸運的是,range、len、append等內建函式都可以正常處理值為nil的slice。
  • 雖然空slice和值為nil的slice並不相等,但它們通常可以替換使用。

nil map

  • 和slice一樣,如果map在宣告後沒有使用複合字面值或內建的make函式進行初始化,那麼它的值將會是預設的nil

nil 介面

  • 宣告為介面型別的變數在未被賦值時,它的零值是nil。
  • 對於一個未被賦值的介面變數來說,它的介面型別和值都是nil,並且變數本身也等於nil。
  • 當介面型別的變數被賦值後,介面就會在內部指向該變數的型別和值。
  • 在Go中,介面型別的變數只有在型別和值都為nil時才等於nil。
    • 即使介面變數的值仍為nil,但只要它的型別不是nil,那麼該變數就不等於nil。
  • 檢驗介面變數的內部表示

錯誤

錯誤處理

  • go語言允許函式和方法同時返回多個值
  • 按照慣例,函式在返回錯誤時,最後邊的返回值應用來表示錯誤。
  • 呼叫函式後,應立即檢查是否發生錯誤。
    • 如果沒有錯誤發生,那麼返回的錯誤值為nil。
import (
	"fmt"
	"io/ioutil"
	"os"
)

func main() {
	files, err := ioutil.ReadDir(".")
	if err == nil {
		fmt.Println(err)
		os.Exit(1)
	}

	for _, file := range files {
		fmt.Println(file.Name())
	}
}

優雅的錯誤處理

  • 減少錯誤處理程式碼的一種策略是:將程式中不會出錯的部分和包含潛在錯誤隱患的部分隔離開來。
  • 對於不得不返回錯誤的程式碼,應盡力簡化相應的錯誤處理程式碼。

檔案寫入

  • 寫入檔案的時候可能出錯:
    • 路徑不正確
    • 許可權不夠
    • 磁碟空間不足
  • 檔案寫入完畢後,必須被關閉,確保檔案被刷到磁碟上,避免資源的洩露。
import (
	"fmt"
	"os"
)

func proverbs(name string) error {
	f, err := os.Create(name)
	if err != nil {
		return err
	}

	_, err = fmt.Fprintln(f, "Errors are values")
	if err != nil {
		f.Close()
		return err
	}

	_, err = fmt.Fprintln(f, "Don't just check errors,handle them gracefully.")
	f.Close()
	return err

}

func main() {
	error := proverbs("proverbs.txt")
	if error != nil {
		fmt.Println(error)
		os.Exit(1)
	}
}

內建型別 error

  • 內建型別error用來表示錯誤。

defer 關鍵字

  • 使用defer關鍵字,Go可以確保所有deferred的動作可以在函式返回前執行。
  • 可以defer任意的函式和方法。
  • defer並不是專門做錯誤處理的。
  • defer可以消除必須時刻惦記執行資源釋放的負擔
func proverbs(name string) error {
	f, err := os.Create(name)
	if err != nil {
		return err
	}
	
	//不用寫多遍close
	defer f.Close()

	_, err = fmt.Fprintln(f, "Errors are values")
	if err != nil {
		return err
	}

	_, err = fmt.Fprintln(f, "Don't just check errors,handle them gracefully.")
	return err
}

有創意的錯誤處理

type safeWriter struct {
	w   io.Writer
	err error
}

func (sw *safeWriter) writeln(s string) {
	if sw.err != nil {
		return
	}

	_, sw.err = fmt.Fprintln(sw.w, s)
}

New Error

  • errors包裡有一個構造用New函式,它接收string作為引數用來表示錯誤資訊。該函式返回error型別。

按需返回錯誤

  • 按照慣例,包含錯誤資訊的變數名應以Err開頭。
  • errors.New這個建構函式是使用指標實現的,所以上例中的switch語句比較的是記憶體地址,而不是錯誤包含的文字資訊。

自定義錯誤型別

  • error型別是一個內建的介面:任何型別只要實現了返回string的Erroro)方法就滿足了該介面。
  • 可以建立新的錯誤型別。
  • 按照慣例,自定義錯誤型別的名字應以Error結尾。
    • 有時候名字就是Error,例如 url.Error

型別斷言

  • 上例中,我們可以使用型別斷言來訪問每一種錯誤。
  • 使用型別斷言,你可以把介面型別轉化成底層的具體型別。
    • 例如:err.(SudokuError)
  • 如果型別滿足多個介面,那麼型別斷言可使它從一個介面型別轉化為另一個介面型別。

不要恐慌(don't panic)

  • Go沒有異常,它有個類似機制panic
  • 當panic發生,那麼程式就會崩潰。

其它語言的異常vs Go 的錯誤值

  • 其它語言的異常在行為和實現上與Go語言的錯誤值有很大的不同:
    • 如果函式丟擲異常,並且附近沒人捕獲它,那麼它就會“冒泡”到函式的呼叫者那裡,如果還沒有人進行捕獲,那麼就繼續“冒泡”到更上層的呼叫者..直到達到棧(Stack)的頂部(例如main函式)。
    • 常這種錯誤處理方式可被看作是可選的:
      • 不處理異常,就不需要加入其它程式碼。
      • 想要處理異常,就需要加入相當數量的專用程式碼。
    • Go語言中的錯誤值更簡單靈活:
      • 忽略錯誤是有意識的決定,從程式碼上看也是顯而易見的。

如何 panic

  • Go裡有一個和其他語言異常類似的機制:panic。
  • 實際上,panic很少出現。
  • 建立panic
    • panic("I forgot my towel")
      • panic 的引數可以是任意型別

錯誤值、panic、os.Exit?

  • 通常,更推薦使用錯誤值,其次才是panic。
  • panic比os.Exit更好:panic後會執行所有defer的動作,而os.Exit則不會。
  • 有時候Go程式會panic而不是返回錯誤值(除以0)

保持冷靜並繼續

  • 為了防止panic導致程式崩潰,Go提供了recover函式。
  • defer的動作會在函式返回前執行,即使發生了panic。
  • 但如果defer的函式呼叫了recover,panic就會停止,程式將繼續執行。

goroutine 和併發(concurrnet)

goroutine

  • 在Go中,獨立的任務叫做goroutine
    • 雖然goroutine與其它語言中的協程、程序、執行緒都有相似之處,但goroutine和它們並不完全相同
    • goroutine建立效率非常高
    • Go能直截了當的協同多個併發(concurrent)操作
  • 在某些語言中,將順序式程式碼轉化為併發式程式碼需要做大量修改
  • 在Go裡,無需修改現有順序式的程式碼,就可以透過goroutine以併發的方式執行任意數量的任務。

啟動goroutine

  • 只需在呼叫前面加一個go關鍵字。

    func sleepyGopher() {
    	time.Sleep(3 * time.Second)
    	fmt.Println("...snore...")
    }
    
    func main() {
    	go sleepyGopher()           //主線路
    	time.Sleep(2 * time.Second) //分支線路
    }
    
  • 如果main函式返回了,goroutine沒執行完也不會執行了

不止一個 goroutine

  • 每次使用go關鍵字都會產生一個新的goroutine。
  • 表面上看,goroutine似乎在同時執行,但由於計算機處理單元有限,其實技術上來說,這些goroutine不是真的在同時執行。
    • 計算機處理器會使用“分時”技術,在多個goroutine上輪流花費一些時間
    • 在使用goroutine時,各個goroutine的執行順序無法確定。

gorputine 的引數

  • 向goroutine傳遞引數就跟向函式傳遞引數一樣,引數都是按值傳遞的(傳入的是副本)

通道 channel

  • 通道(channel)可以在多個goroutine之間安全的傳值。
  • 通道可以用作變數、函式引數、結構體欄位…
  • 建立通道用make函式,並指定其傳輸資料的型別
    • c:= make(chan int)

通道 channel 傳送、接收

  • 使用左箭頭運算子 <- 向通道傳送值或從通道接收值
    • 向通道傳送值:c <- 99
    • 從通道接收值:r := <- c
  • 傳送操作會等待直到另一個goroutine嘗試對該通道進行接收操作為止。
    • 執行傳送操作的goroutine在等待期間將無法執行其它操作
    • 未在等待通道操作的goroutine讓然可以繼續自由的執行
  • 執行接收操作的goroutine將等待直到另一個goroutine嘗試向該通道進行傳送操作為止。
func sleepyGopher(id int, c chan int) {
	time.Sleep(3 * time.Second)
	fmt.Println("...", id, " snore ... ")
	c <- id
}

func main() {
	c := make(chan int)
	for i := 0; i < 5; i++ {
		go sleepyGopher(i, c)
	}

	for i := 0; i < 5; i++ {
		gopherID := <-c
		fmt.Println("gopher", gopherID, " has finished sleeping")
	}
}

使用 select 處理多個通道

  • 等待不同型別的值。

  • time.After函式,返回一個通道,該通道在指定時間後會接收到一個值(傳送該值的goroutine是Go執行時的一部分)。

  • select和switch有點像。

    • 該語句包含的每個case都持有一個通道,用來傳送或接收資料。
    • select會等待直到某個case分支的操作就緒,然後就會執行該case分支。
    import (
    	"fmt"
    	"math/rand"
    	"time"
    )
    
    func sleepyGopher(id int, c chan int) {
    	time.Sleep(time.Duration(rand.Intn(4000)) * time.Millisecond)
    	c <- id
    }
    
    func main() {
    	c := make(chan int)
    	for i := 0; i < 5; i++ {
    		go sleepyGopher(i, c)
    	}
    
    	timeout := time.After(1 * time.Second)
    	for i := 0; i < 5; i++ {
    		select {
    		case gopherID := <-c:
    			fmt.Println("gopher", gopherID, " has finished sleeping")
    		case <-timeout:
    			fmt.Println("my patience ran out")
    			return
    		}
    	}
    }
    
  • 注意:即使已經停止等待goroutine,但只要main函式還沒返回,仍在執行的goroutine將會繼續佔用記憶體。

  • select語句在不包含任何case的情況下將永遠等下去。

nil 通道

  • 如果不使用make初始化通道,那麼通道變數的值就是nil(零值)
  • 對nil通道進行傳送或接收不會引起panic,但會導致永久阻塞。
  • 對nil通道執行close函式,那麼會引起panic
  • nil通道的用處:
    • 對於包含select語句的迴圈,如果不希望每次迴圈都等待select所涉及的所有通道,那麼可以先將某些通道設為nil,等到傳送值準備就緒之後,再將通道變成一個非nil值並執行傳送操作。

阻塞和死鎖

  • 當goroutine在等待通道的傳送或接收時,我們就說它被阻塞了。
  • 除了goroutine本身佔用少量的記憶體外,被阻塞的goroutine並不消耗任何其它資源。
    • goroutine靜靜的停在那裡,等待導致其阻塞的事情來解除阻塞。
  • 當一個或多個goroutine因為某些永遠無法發生的事情被阻塞時,我們稱這種情況為死鎖。而出現死鎖的程式通常會崩潰或掛起。

地鼠裝配線

  • Go允許在沒有值可供傳送的情況下透過close函式關閉通道
    • 例如 close(c)
  • 通道被關閉後無法寫入任何值,如果嘗試寫入將引發panic。
  • 嘗試讀取被關閉的通道會獲得與通道型別對應的零值。
  • 注意:如果迴圈裡讀取一個已關閉的通道,並沒檢查通道是否關閉,那麼該迴圈可能會一直運轉下去,耗費大量CPU時間
  • 執行以下程式碼可得知通道是否被關閉:
    • v,ok:=-c
import (
	"fmt"
	"strings"
)

func sourceGopher(downstream chan string) {
	for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
		downstream <- v
	}
	close(downstream)
}

func filterGopher(upstream, downstream chan string) {
	for {
		item := <-upstream
		if item == "" {
			close(upstream)
			return
		}
		if !strings.Contains(item, "bad") {
			downstream <- item
		}
	}
}

func printGopher(upstream chan string) {
	for {
		v := <-upstream
		if v == "" {
			return
		}
		fmt.Println(v)
	}
}

func main() {
	c0 := make(chan string)
	c1 := make(chan string)
	go sourceGopher(c0)
	go filterGopher(c0, c1)
	printGopher(c1)
}

常用模式

  • 從通道里面讀取值,直到它關閉為止。
    • 可以使用range關鍵字達到該目的
import (
	"fmt"
	"strings"
)

func sourceGopher(downstream chan string) {
	for _, v := range []string{"hello world", "a bad apple", "goodbye all"} {
		downstream <- v
	}
	close(downstream)
}

func filterGopher(upstream, downstream chan string) {
	for item := range upstream {
		if !strings.Contains(item, "bad") {
			downstream <- item
		}
	}
	close(downstream)
}

func printGopher(upstream chan string) {
	for v := range upstream {
		fmt.Println(v)
	}
}

func main() {
	c0 := make(chan string)
	c1 := make(chan string)
	go sourceGopher(c0)
	go filterGopher(c0, c1)
	printGopher(c1)
}

併發狀態

  • 共享值
  • 競爭條件(race condition)

Go 的互斥鎖(mutex)

  • mutex = mutex exclusive
  • Lock(), Unlock()
  • sync包
  • 互斥鎖定義在被保護的變數之上
import (
	"sync"
)

var mu sync.Mutex

func main() {
	mu.Lock()
	defer mu.Lock()
}

互斥鎖的隱患

  • 死鎖
  • 為保證互廳鎖的安全使用,我們須遵守以下規則:
    • 1.儘可能的簡化互廳鎖保護的程式碼
    • 2、對每一份共享狀態只使用一個互廳鎖

長時間執行的工作程序

  • 工作程序(worker)

    • 通常會被寫成包含select語句的for迴圈。

      func worker() {
      	for {
      		select {}
      	}
      }
      
      func main() {
      	go worker()
      }
      

時間迴圈和goroutine

  • 事件迴圈(event loop)
  • 中心迴圈(central loop)'
  • go透過提供goroutine作為核心概念,消除了對中心迴圈的需求。

相關文章