Go程式設計基礎

HULK一線技術雜談發表於2018-11-05

女主宣言

Go 是一個開源的程式語言,它能讓構造簡單、可靠且高效的軟體變得容易。現在Go的開發已經是完全開放的,並且擁有一個活躍的社群。本節課開始,李鋼老師將使用Go語言來做一些程式設計實踐方面的講解。

PS:豐富的一線技術、多元化的表現形式,盡在“HULK一線技術雜談”,點關注哦!

Go程式設計基礎

Splashdown! Apollo 7 Returns Home

by NASA IOTD


上面兩次課我講解了程式設計方面的基礎知識,這次開始,我使用Go語言來做一些程式設計實踐方面的講解。


今天先來說下Go語言中的一些我認為比較重要的知識點。


關於Go的基礎使用,這裡不做過多介紹,可以閱讀:

  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

重要的資料結構

slice

基礎知識

slice是go中最常用的資料結構之一,它相當於動態陣列,瞭解下它的內部實現,對我們使用來說有很大的好處:


slice的資料結構示例為:

type slice struct {    

    ptr *array  //底層儲存陣列    

    len int     //當前儲存了多少個元素    

    cap int     //底層陣列可以儲存多少個元素(從ptr指向的位置開始)}

用張圖來表示:

Go程式設計基礎

我們常用的slice有個len和cap的概念,他們就是取len和cap這兩個欄位的值。

slice我們通常都用它做為動態陣列使用,但slice翻譯過來是切片的意思,為什麼呢?

我們來看個例子:

首先,我們建立一個slice:

s := make([]int, 5)

對應的資料結構為:

Go程式設計基礎

之後,我們再呼叫:

ss := s[2:4]

我們得到:

Go程式設計基礎

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

執行結果:

Go程式設計基礎

所以對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命令,它能列出一個給定根目錄下所有的檔案,包括子目錄:

讀取目錄下的包括子目錄的所有檔案,最先想到的就是遞迴了,但是如果目錄層級過深,顯然會導致棧溢位,所以這是一個非常好的例子

實現程式碼如下:

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這種動態儲存結構使用的是在堆上的空間,所以我們將遞迴轉迴圈解決這個問題。

參考

  1. Go Slices: usage and internals:

    https://blog.golang.org/go-slices-usage-and-internals


HULK一線技術雜談

由360雲平臺團隊打造的技術分享公眾號,內容涉及雲端計算資料庫大資料監控泛前端自動化測試等眾多技術領域,通過夯實的技術積累和豐富的一線實戰經驗,為你帶來最有料的技術分享

原文連結:https://mp.weixin.qq.com/s/AJyb1ZBBgfo13VubiX36TQ

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31555491/viewspace-2218730/,如需轉載,請註明出處,否則將追究法律責任。

相關文章