應用程式設計基礎課第三講:Go程式設計基礎
上面兩次課我講解了程式設計方面的基礎知識,這次開始,我使用Go語言來做一些程式設計實踐方面的講解。
今天先來說下Go語言中的一些我認為比較重要的知識點。
關於Go的基礎使用,這裡不做過多介紹,可以閱讀:
- How to Write Go Code: ofollow,noindex">https://golang.org/doc/code.html
- Effective Go: https://golang.org/doc/effective_go.html
- The Way to Go: https://github.com/Unknwon/the-way-to-go_ZH_CN
重要的資料結構
slice
基礎知識
slice是go中最常用的資料結構之一,它相當於動態陣列,瞭解下它的內部實現,對我們是用來說有很大的好處:
slice的資料結構示例為:
type slice struct { ptr *array//底層儲存陣列 len int//當前儲存了多少個元素 cap int//底層陣列可以儲存多少個元素(從ptr指向的位置開始) }
用張圖來表示:

go-slices-usage-and-internals_slice-struct.png
我們常用的slice有個len和cap的概念,他們就是取len和cap這兩個欄位的值。
slice我們通常都用它做為動態陣列使用,但slice翻譯過來是切片的意思,為什麼呢?
我們來看個例子:
首先,我們建立一個slice:
s := make([]int, 5)
對應的資料結構為:

go-slices-usage-and-internals_slice-1.png
之後,我們再呼叫:
ss := s[2:4]
我們得到:

go-slices-usage-and-internals_slice-2.png
所以兩個slice,相當於是在底層array上的兩個切片。大家請注意下第二個slice的cap是3。
使用注意
slice在使用中有幾個很容易出錯的地方,需要大家注意下。
這裡先總結下最容易出錯的原因,就是多個slice在使用同樣的底層儲存時,修改一個slice會導致其它slice中的資料變化。
示例1:
s := []int{1, 2, 3} fmt.Println(s) ss := s[1:3] ss[0] = 0 fmt.Println(s, ss) s[1] = 11 fmt.Println(s, ss)
輸出:
[1 2 3] [1 0 3] [0 3] [1 11 3] [11 3]
大家可以看到,由於兩個slice都是用同樣的底層array,所以修改其中一個就會導致另外一個的變化
示例2:
func main() { s := []int{1, 2, 3} fmt.Println(s) foo(s) or foo(s[1:3]) fmt.Println(s) } func foo(ss []int) { ss[0] = 0 }
輸出:
[1 2 3] [1 0 3]
這個和上面同樣的原因
示例3:
s := []int{1, 2, 3} fmt.Println(s) ss := s[1:3] ss = append(ss, 4) fmt.Println(s, ss)
輸出:
[1 2 3] [1 2 3] [2 3 4]
這裡大家可以看到,由於append操作改變了其中一個slice的底層array,所以對其中一個slice的修改不會影響到另外一個。
map
關於map,有如下幾個地方需要注意:
- 使用先要初始化
var m map[string]int m["a"] = 1
會導致:
panic: assignment to entry in nil map
正確使用:
m := make(map[string]int) m["a"] = 1 fmt.Println(m)
輸出:
map[a:1]
- map作為函式形參時,函式中對map的修改會影響實參中的值
func main() { m := make(map[string]int) m["a"] = 1 fmt.Println(m) foo(m) fmt.Println(m) } func foo(fm map[string]int) { fm["a"] = 11 }
輸出:
map[a:1] map[a:11]
- 對map做併發讀寫會導致panic
var gm map[int]int func main() { gm = make(map[int]int) for i := 0; i < 10; i++ { go foo(i) } time.Sleep(time.Second * 10) } func foo(i int) { for j := 0; j < 100; j++ { gm[i] = j } }
執行結果:
fatal error: concurrent map writes fatal error: concurrent map writes goroutine 17 [running]: runtime.throw(0x46ff50, 0x15) /usr/local/go/src/runtime/panic.go:616 +0x81 fp=0xc420028758 sp=0xc420028738 pc=0x422711 runtime.mapassign_fast64(0x45e4e0, 0xc42007a060, 0x0, 0x0) /usr/local/go/src/runtime/hashmap_fast.go:531 +0x2f6 fp=0xc4200287a0 sp=0xc420028758 pc=0x408306 main.foo(0x0) /home/ligang/tmp/go/main.go:22 +0x4c fp=0xc4200287d8 sp=0xc4200287a0 pc=0x44f4dc runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:2361 +0x1 fp=0xc4200287e0 sp=0xc4200287d8 pc=0x448a51 created by main.main /home/ligang/tmp/go/main.go:14 +0x61
所以對map做併發讀寫時需要加鎖
型別轉換
我們開發強型別語言程式時通常需要做型別轉換,Go中的型別轉換有兩種最常用的形式:
原生型別轉換
- 同一大型別下(如整數的int、int64,浮點數的float32、float64等),可以用型別加括號的形式,如:
int -> int64:
var a int = 1 b := int64(a)
- 不同大型別下的轉換,使用strconv包中的方法
複雜型別轉換,通常是interface轉指定型別
這個要使用型別斷言:
var a interface{} = 1 b := a.(int)
請注意這裡如果型別斷言失敗的話,程式會panic,可以使用recover防止:
defer func() { if r := recover(); r != nil { fmt.Println(r) } }() var a interface{} = 1 b := a.(string)
輸出:
interface conversion: interface {} is int, not string
函式傳參時的指標和結構體
這裡只需要記住一點,就是結構體作為函式形參時,會做值拷貝,所以拷貝的那部分值的修改,不會反映到實參值
type ta struct { i int } func main() { var a ta a.i = 1 foo(a) fmt.Println(a) } func foo(t ta) { t.i = 11 }
輸出:
{1}
同樣的:
type ta struct { i int } func main() { var a ta a.i = 1 a.foo() fmt.Println(a) } func (t ta) foo() { t.i = 11 }
輸出:
{1}
指標就不同了,會修改實參中的原值,這裡就不舉例了。
防止棧溢位,遞迴轉迴圈
我們程式設計時有時會寫遞迴函式,遞迴雖然簡單,但是會有棧溢位的風險,解決方法是把遞迴轉迴圈,將儲存從棧空間轉移到堆空間上。
我們這裡舉個實際的例子,linux中有個 tree
命令,它能列出一個給定根目錄下所有的檔案,包括子目錄:
ligang@vm-xubuntu ~/devspace/hogwarts $ tree cppsimple/ cppsimple/ ├── cmake-build-debug │├── CMakeCache.txt │├── CMakeFiles ││├── 3.12.2 │││├── CMakeCCompiler.cmake │││├── CMakeCXXCompiler.cmake │││├── CMakeDetermineCompilerABI_C.bin │││├── CMakeDetermineCompilerABI_CXX.bin │││├── CMakeSystem.cmake │││├── CompilerIdC ││││├── a.out ││││├── CMakeCCompilerId.c ││││└── tmp │││└── CompilerIdCXX │││├── a.out │││├── CMakeCXXCompilerId.cpp
讀取目錄下的包括子目錄的所有檔案,最先想到的就是遞迴了,但是如果目錄層級過深,顯然會導致棧溢位,所以這是一個非常好的例子
實現程式碼如下:
func ListFilesInDir(rootDir string) ([]string, error) { rootDir = strings.TrimRight(rootDir, "/") if !DirExist(rootDir) { return nil, errors.New("Dir not exists") } var fileList []string dirList := []string{rootDir} for i := 0; i < len(dirList); i++ { curDir := dirList[i] file, err := os.Open(dirList[i]) if err != nil { return nil, err } fis, err := file.Readdir(-1) if err != nil { return nil, err } for _, fi := range fis { path := curDir + "/" + fi.Name() if fi.IsDir() { dirList = append(dirList, path) } else { fileList = append(fileList, path) } } } return fileList, nil }
由於slice這種動態儲存結構使用的是在堆上的空間,所以我們將遞迴轉迴圈解決這個問題。
參考
Go Slices: usage and internals: https://blog.golang.org/go-slices-usage-and-internals