上面兩次課我講解了編程方面的基礎(chǔ)知識,這次開始,我使用Go語言來做一些編程實(shí)踐方面的講解。
今天先來說下Go語言中的一些我認(rèn)為比較重要的知識點(diǎn)。
關(guān)于Go的基礎(chǔ)使用,這里不做過多介紹,可以閱讀:
- How to Write Go Code: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
重要的數(shù)據(jù)結(jié)構(gòu)
slice
基礎(chǔ)知識
slice是go中最常用的數(shù)據(jù)結(jié)構(gòu)之一,它相當(dāng)于動(dòng)態(tài)數(shù)組,了解下它的內(nèi)部實(shí)現(xiàn),對我們是用來說有很大的好處:
slice的數(shù)據(jù)結(jié)構(gòu)示例為:
type slice struct {
ptr *array //底層存儲數(shù)組
len int //當(dāng)前存儲了多少個(gè)元素
cap int //底層數(shù)組可以存儲多少個(gè)元素(從ptr指向的位置開始)
}
用張圖來表示:

我們常用的slice有個(gè)len和cap的概念,他們就是取len和cap這兩個(gè)字段的值。
slice我們通常都用它做為動(dòng)態(tài)數(shù)組使用,但slice翻譯過來是切片的意思,為什么呢?
我們來看個(gè)例子:
首先,我們創(chuàng)建一個(gè)slice:
s := make([]int, 5)
對應(yīng)的數(shù)據(jù)結(jié)構(gòu)為:

之后,我們再調(diào)用:
ss := s[2:4]
我們得到:

所以兩個(gè)slice,相當(dāng)于是在底層array上的兩個(gè)切片。大家請注意下第二個(gè)slice的cap是3。
使用注意
slice在使用中有幾個(gè)很容易出錯(cuò)的地方,需要大家注意下。
這里先總結(jié)下最容易出錯(cuò)的原因,就是多個(gè)slice在使用同樣的底層存儲時(shí),修改一個(gè)slice會(huì)導(dǎo)致其它slice中的數(shù)據(jù)變化。
示例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]
大家可以看到,由于兩個(gè)slice都是用同樣的底層array,所以修改其中一個(gè)就會(huì)導(dǎo)致另外一個(gè)的變化
示例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]
這個(gè)和上面同樣的原因
示例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操作改變了其中一個(gè)slice的底層array,所以對其中一個(gè)slice的修改不會(huì)影響到另外一個(gè)。
map
關(guān)于map,有如下幾個(gè)地方需要注意:
- 使用先要初始化
var m map[string]int
m["a"] = 1
會(huì)導(dǎo)致:
panic: assignment to entry in nil map
正確使用:
m := make(map[string]int)
m["a"] = 1
fmt.Println(m)
輸出:
map[a:1]
- map作為函數(shù)形參時(shí),函數(shù)中對map的修改會(huì)影響實(shí)參中的值
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做并發(fā)讀寫會(huì)導(dǎo)致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
}
}
運(yùn)行結(jié)果:
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做并發(fā)讀寫時(shí)需要加鎖
類型轉(zhuǎn)換
我們開發(fā)強(qiáng)類型語言程序時(shí)通常需要做類型轉(zhuǎn)換,Go中的類型轉(zhuǎn)換有兩種最常用的形式:
原生類型轉(zhuǎn)換
- 同一大類型下(如整數(shù)的int、int64,浮點(diǎn)數(shù)的float32、float64等),可以用類型加括號的形式,如:
int -> int64:
var a int = 1
b := int64(a)
- 不同大類型下的轉(zhuǎn)換,使用strconv包中的方法
復(fù)雜類型轉(zhuǎn)換,通常是interface轉(zhuǎn)指定類型
這個(gè)要使用類型斷言:
var a interface{} = 1
b := a.(int)
請注意這里如果類型斷言失敗的話,程序會(huì)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
函數(shù)傳參時(shí)的指針和結(jié)構(gòu)體
這里只需要記住一點(diǎn),就是結(jié)構(gòu)體作為函數(shù)形參時(shí),會(huì)做值拷貝,所以拷貝的那部分值的修改,不會(huì)反映到實(shí)參值
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}
指針就不同了,會(huì)修改實(shí)參中的原值,這里就不舉例了。
防止棧溢出,遞歸轉(zhuǎn)循環(huán)
我們編程時(shí)有時(shí)會(huì)寫遞歸函數(shù),遞歸雖然簡單,但是會(huì)有棧溢出的風(fēng)險(xiǎn),解決方法是把遞歸轉(zhuǎn)循環(huán),將存儲從??臻g轉(zhuǎn)移到堆空間上。
我們這里舉個(gè)實(shí)際的例子,linux中有個(gè)tree命令,它能列出一個(gè)給定根目錄下所有的文件,包括子目錄:
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
讀取目錄下的包括子目錄的所有文件,最先想到的就是遞歸了,但是如果目錄層級過深,顯然會(huì)導(dǎo)致棧溢出,所以這是一個(gè)非常好的例子
實(shí)現(xiàn)代碼如下:
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這種動(dòng)態(tài)存儲結(jié)構(gòu)使用的是在堆上的空間,所以我們將遞歸轉(zhuǎn)循環(huán)解決這個(gè)問題。
參考
Go Slices: usage and internals:https://blog.golang.org/go-slices-usage-and-internals