應(yīng)用編程基礎(chǔ)課第三講:Go編程基礎(chǔ)

上面兩次課我講解了編程方面的基礎(chǔ)知識,這次開始,我使用Go語言來做一些編程實(shí)踐方面的講解。

今天先來說下Go語言中的一些我認(rèn)為比較重要的知識點(diǎn)。

關(guān)于Go的基礎(chǔ)使用,這里不做過多介紹,可以閱讀:

  1. How to Write Go Code:https://golang.org/doc/code.html
  2. Effective Go:https://golang.org/doc/effective_go.html
  3. 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指向的位置開始)
}

用張圖來表示:

go-slices-usage-and-internals_slice-struct.png

我們常用的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)為:

go-slices-usage-and-internals_slice-1.png

之后,我們再調(diào)用:

ss := s[2:4]

我們得到:

go-slices-usage-and-internals_slice-2.png

所以兩個(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容