Golang開發常見的57個錯誤

IT小馬發表於2022-02-22

1、不允許左大括號單獨一行

2、不允許出現未使用的變數

3、不允許出現未使用的import(使用 _ 包名 引入)

4、短的變數宣告(Short Variable Declarations)只能在函式內部使用

// myvar := 1   // error
var myvar = 1   // ok

5、不能使用短變數宣告(Short Variable Declarations)重複宣告

6、不能使用短變數宣告(Short Variable Declarations)這種方式來設定欄位值

data.result, err := work() //error

7、意外的變數幽靈(Accidental Variable Shadowing)
程式碼塊中同名短變數宣告從宣告開始到程式碼塊結束,對變數的修改將不會影響到外部變數!

8、不能使用nil初始化一個未指定型別的變數

9、不能直接使用nil值的Slice和Map

10、map使用make分配記憶體時可指定capicity,但是不能對map使用cap函式

在golang中,nil只能賦值給指標、channel、func、interface、map或slice型別的變數。

12、陣列用於函式傳參時是值複製

注意:方法或函式呼叫時,傳入引數都是值複製(跟賦值一致),除非是map、slice、channel、指標型別這些特殊型別是引用傳遞。

13、range關鍵字返回是鍵值對,而不是值

14、Slice和Array是一維的

15、從不存在key的map中取值時,返回的總是”0值”

16、字串是不可變的

17、字串與[]byte之間的轉換是複製(有記憶體損耗),可以用map[string] []byte建立字串與[]byte之間對映,也可range來避免記憶體分配來提高效能

//[]byte: 
for i,v := range []byte(str) {
}

18、string的索引操作返回的是byte(或uint8),如想獲取字元可使用for range,也可使用unicode/utf8包和golang.org/x/exp/utf8string包的At()方法。

19、字串並不總是UTF8的文字

20、len(str)返回的是字串的位元組數

str := "我"
fmt.Println(len(str)) //3

21、在Slice、Array、Map的多行書寫最後的逗號不可省略,單行書寫,最後一個元素的逗號可省略

22、內建資料結構的操作並不同步,但可把Go提供了併發的特性使用起來:goroutines和channels。

23、使用for range迭代String,是以rune來迭代的。
一個字元,也可以有多個rune組成。需要處理字元,儘量使用golang.org/x/text/unicode/norm包。

for range總是嘗試將字串解析成utf8的文字,對於它無法解析的位元組,它會返回oxfffd的rune字元。
因此,任何包含非utf8的文字,一定要先將其轉換成字元切片([]byte)。

24、使用for range迭代map時每次迭代的順序可能不一樣,因為map的迭代是隨機的。

25、switch的case預設匹配規則不同於其它語言的是,匹配case條件後預設退出,除非使用fallthrough繼續匹配;而其它語言是預設繼續匹配,除非使用break退出匹配。

26、只有後置自增(a++)、後置自減,不存在前置自增(++a)、前置自減

27、位運算的非操作是^(跟異或位運算一樣),有別於其它語言的~。

28、位運算(與、或、異或、取反)優先順序高於四則運算(加、減、乘、除、取餘),有別於C語言。

29、結構體在序列化時非匯出欄位(以小寫字母開頭的欄位名)不會被encode,因此在decode時這些非匯出欄位的值為”0值”

30、程式不等所有goroutine結束就會退出。可通過channel實現主協程(main goroutine)等待所有goroutine完成。

31、對於無快取區的channel,寫入channel的goroutine會阻塞直到被讀取,讀取channel的goroutine會阻塞直到有資料寫入。

32、從一個closed狀態的channel讀取資料是安全的,可通過返回狀態(第二個返回引數)判斷是否關閉;而向一個closed狀態的channel寫資料會導致panic。

33、向一個nil值(未用make分配空間)的channel傳送或讀取資料,會導致永遠阻塞。

34、方法接收者是型別(T),接收者只是原物件的值複製,在方法中修改接收者不會修改原始物件的值;如果方法接收者是指標型別(*T),是對原物件的引用,方法中對其修改當然是原物件修改。

35、log包中的log.Fatal和log.Panic不僅僅記錄日誌,還會中止程式。它不同於Logging庫。

36、使用defer語句關閉資源時要注意nil值,在defer語句之前要進行nil值判斷處理(否則會引發空引用的panic)

37、關閉HTTP連線,可使用

  1. req.Close=true,表示在http請求完成時關閉連線
  2. 新增Connection: close的連線請求頭。http服務端也會傳送Connection: close的響應頭,http庫處理響應時會關閉連線。
  3. 全域性關閉http連線重用。

    package main
    
    import (  
     "fmt"
     "net/http"
     "io/ioutil"
    )
    
    func main() {  
     //全域性關閉http連線重用
     //tr := &http.Transport{DisableKeepAlives: true}
     //client := &http.Client{Transport: tr}
    
     req, err := http.NewRequest("GET","http://golang.org",nil)
     if err != nil {
         fmt.Println(err)
         return
     }
    
     req.Close = true
     //or do this:
     //req.Header.Add("Connection", "close")
    
     resp, err := http.DefaultClient.Do(req)
     if resp != nil {
         defer resp.Body.Close()
     }
    
     if err != nil {
         fmt.Println(err)
         return
     }
    
     body, err := ioutil.ReadAll(resp.Body)
     if err != nil {
         fmt.Println(err)
         return
     }
    
     fmt.Println(len(string(body)))
    }

37、Json反序列化數字到interface{}型別的值中,預設解析為float64型別

38.Struct、Array、Slice、Map的比較
如果struct結構體的所有欄位都能夠使用==操作比較,那麼結構體變數也能夠使用==比較。
但是,如果struct欄位不能使用==比較,那麼結構體變數使用==比較會導致編譯錯誤。

同樣,array只有在它的每個元素能夠使用==比較時,array變數才能夠比較。

Go提供了一些用於比較不能直接使用==比較的函式,其中最常用的是reflect.DeepEqual()函式。

DeepEqual()函式對於nil值的slice與空元素的slice是不相等的,這點不同於bytes.Equal()函式。

var b1 []byte = nil
b2 := []byte{}
fmt.Println("b1 == b2:",reflect.DeepEqual(b1, b2)) //prints: b1 == b2: false

var b3 []byte = nil
b4 := []byte{}
fmt.Println("b3 == b4:",bytes.Equal(b3, b4)) //prints: b3 == b4: true

如果要忽略大小寫來比較包含文字資料的位元組切片(byte slice),
不建議使用bytes包和strings包裡的ToUpper()、ToLower()這些函式轉換後再用==、byte.Equal()、bytes.Compare()等比較,ToUpper()、ToLower()只能處理英文文字,對其它語言無效。因此建議使用strings.EqualFold()和bytes.EqualFold()

如果要比較用於驗證使用者資料金鑰資訊的位元組切片時,使用reflact.DeepEqual()、bytes.Equal()、
bytes.Compare()會使應用程式遭受計時攻擊(Timing Attack),可使用crypto/subtle.ConstantTimeCompare()避免洩漏時間資訊。

39、recover()函式能夠捕獲或攔截panic,但必須在defer函式或語句中直接呼叫,否則無效。

40、在slice、array、map的for range獲取的資料項是從集合元素的複製過來的,並非引用原始資料,但使用索引能訪問原始資料。

data := []int{1,2,3}
for _,v := range data {
    v *= 10          // original item is not changed
}

data2 := []int{1,2,3}
for i,v := range data2 {
    data2[i] *= 10       // change original item
}

// 元素是指標型別就不一樣了
data3 := []*struct{num int} {{1}, {2}, {3}}
for _,v := range data {
    v.num *= 10
}

fmt.Println("data:", data)              //prints data: [1 2 3]
fmt.Println("data:", data2)             //prints data: [10 20 30]
fmt.Println(data3[0],data3[1],data3[2])    //prints &{10} &{20} &{30}

41、從一個slice上再生成一個切片slice,新的slice將直接引用原始slice的那個陣列,兩個slice對同一陣列的操作,會相互影響。

可通過copy()為新切片slice重新分配空間,從slice中copy部分的資料來避免相互之間的影響。

42.從已存在的切片slice中繼續切片時,新切片的capicity等於原capicity減去新切片之前部分的數量,新切片與原切片都指向同一陣列空間。

新生成切片之間capicity區域是重疊的,因此在新增資料時易造成資料覆蓋問題。

slice使用append新增的內容時超出capicity時,會重新分配空間。
利用這一點,將要修改的切片指定capicity為切片當前length,可避免切片之間的超範圍覆蓋影響。

    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')
    dir1 := path[:sepIndex]
    // 解決方法
    // dir1 := path[:sepIndex:sepIndex] //full slice expression
    dir2 := path[sepIndex+1:]
    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1,"suffix"...)
    path = bytes.Join([][]byte{dir1,dir2},[]byte{'/'})

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB (not ok)

    fmt.Println("new path =>",string(path))

43、slice在新增元素前,與其它切片共享同一資料區域,修改會相互影響;但新增元素導致記憶體重新分配之後,不再指向原來的資料區域,修改元素,不再影響其它切片。

    s1 := []int{1,2,3}
    fmt.Println(len(s1),cap(s1),s1) //prints 3 3 [1 2 3]

    s2 := s1[1:]
    fmt.Println(len(s2),cap(s2),s2) //prints 2 2 [2 3]

    for i := range s2 { s2[i] += 20 }

    //still referencing the same array
    fmt.Println(s1) //prints [1 22 23]
    fmt.Println(s2) //prints [22 23]

    s2 = append(s2,4)

    for i := range s2 { s2[i] += 10 }

    //s1 is now "stale"
    fmt.Println(s1) //prints [1 22 23]
    fmt.Println(s2) //prints [32 33 14]

44、型別重定義與方法繼承

從一個已存在的(non-interface)非介面型別重新定義一個新型別時,不會繼承原型別的任何方法。
可以通過定義一個組合匿名變數的型別,來實現對此匿名變數型別的繼承。

但是從一個已存在介面重新定義一個新介面時,新介面會繼承原介面所有方法。

45、從”for switch”和”for select”程式碼塊中跳出。

無label的break只會跳出最內層的switch/select程式碼塊。
如需要從switch/select程式碼塊中跳出外層的for迴圈,可以在for迴圈外部定義label,供break跳出。

return當然也是可以的,如果在這裡可以用的話。

46、在for語句的閉包中使用迭代變數會有問題

在for迭代過程中,迭代變數會一直保留,只是每次迭代值不一樣。
因此在for迴圈中在閉包裡直接引用迭代變數,在執行時直接取迭代變數的值,而不是閉包所在迭代的變數值。

如果閉包要取所在迭代變數的值,就需要for中定義一個變數來儲存所在迭代的值,或者通過閉包函式傳參。

47、defer函式呼叫引數

defer後面必須是函式或方法的呼叫語句。defer後面不論是函式還是方法,輸入引數的值是在defer宣告時已計算好,
而不是呼叫開始計算。

要特別注意的是,defer後面是方法呼叫語句時,方法的接受者是在defer語句執行時傳遞的,而不是defer宣告時傳入的。

48、defer語句呼叫是在當前函式結束之後呼叫,而不是變數的作用範圍。

49、失敗的型別斷言:var.(T)型別斷言失敗時會返回T型別的“0值”,而不是變數原始值。

func main() {  
      var data interface{} = "great"
      res, ok := data.(int); 
    fmt.Println("res =>",res, ",ok =>",ok)//res => 0 ,ok => false
}

50、阻塞的goroutine與資源洩漏

func First(query string, replicas ...Search) Result {  
    c := make(chan Result)
    // 解決1:使用緩衝的channel: c := make(chan Result,len(replicas))
    searchReplica := func(i int) { c <- replicas[i](query) }
    // 解決2:使用select-default,防止阻塞
    // searchReplica := func(i int) { 
    //     select {
    //     case c <- replicas[i](query):
    //     default:
    //     }
    // }
    // 解決3:使用特殊的channel來中斷原有工作
    // done := make(chan struct{})
    // defer close(done)
    // searchReplica := func(i int) { 
    //     select {
    //     case c <- replicas[i](query):
    //     case <- done:
    //     }
    // }

    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

51、用值例項上呼叫接收者為指標的方法

對於可定址(addressable)的值變數(而不是指標),可以直接呼叫接受物件為指標型別的方法。
換句話說,就不需要為可定址值變數定義以接受物件為值型別的方法了。

但是,並不是所有變數都是可定址的,像Map的元素就是不可定址的。

package main

import "fmt"

type data struct {  
    name string
}

func (p *data) print() {  
    fmt.Println("name:",p.name)
}

type printer interface {  
    print()
}

func main() {  
    d1 := data{"one"}
    d1.print() //ok

    // var in printer = data{"two"} //error
    var in printer = &data{"two"}
    in.print()

    m := map[string]data {"x":data{"three"}}
    //m["x"].print() //error
    d2 = m["x"]
    d2.print()      // ok
}

52、更新map值的欄位

如果map的值型別是結構體型別,那麼不能更新從map中取出的結構體的欄位值。
但是對於結構體型別的slice卻是可以的。

package main

type data struct {  
    name string
}

func main() {  
    m := map[string]data {"x":{"one"}}
    //m["x"].name = "two" //error
    r := m["x"]
    r.name = "two"
    m["x"] = r
    fmt.Println(s)       // prints: map[x:{two}]

    mp := map[string]*data {"x": {"one"}}
    mp["x"].name = "two" // ok

    s := []data{{"one"}}
    s[0].name = "two"    // ok
    fmt.Println(s)       // prints: [{two}]
}

53、nil值的interface{}不等於nil
在golang中,nil只能賦值給指標、channel、func、interface、map或slice型別的變數。

interface{}表示任意型別,可以接收任意型別的值。interface{}變數在底是由型別和值兩部分組成,表示為(T,V),interface{}變數比較特殊,判斷它是nil時,要求它的型別和值都是nil,即(nil, nil)。
其它型別變數,只要值是nil,那麼此變數就是nil(為什麼?變數型別不是nil,那當然只能用值來判斷了)

宣告變數interface{},它預設就是nil,底層型別與值表示是(nil, nil)。
當任何型別T的變數值V給interface{}變數賦值時,interface{}變數的底層表示是(T, V)。只要T非nil,即使V是nil,interface{}變數也不是nil。

    var data *byte
    var in interface{}

    fmt.Println(data,data == nil) //prints: <nil> true
    fmt.Println(in,in == nil)     //prints: <nil> true

    in = data
    fmt.Println(in,in == nil)     //prints: <nil> false
    //'data' is 'nil', but 'in' is not 'nil'

    doit := func(arg int) interface{} {
        var result *struct{} = nil
        if(arg > 0) {
            result = &struct{}{}
        }
        return result
    }
    if res := doit(-1); res != nil {
        fmt.Println("good result:",res) //prints: good result: <nil>
        //'res' is not 'nil', but its value is 'nil'
    }

    doit = func(arg int) interface{} {
        var result *struct{} = nil
        if(arg > 0) {
            result = &struct{}{}
        } else {
            return nil //return an explicit 'nil'
        }
        return result
    }

    if res := doit(-1); res != nil {
        fmt.Println("good result:",res)
    } else {
        fmt.Println("bad result (res is nil)") //here as expected
    }

54、變數記憶體的分配

在C++中使用new操作符總是在heap上分配變數。Go編譯器使用new()和make()分配記憶體的位置到底是stack還是heap,
取決於變數的大小(size)和逃逸分析的結果(result of “escape analysis”)。這意味著Go語言中,返回本地變數的引用也不會有問題。

要想知道變數記憶體分配的位置,可以在go build、go run命令指定-gcflags -m即可:
go run -gcflags -m app.go

55、GOMAXPROCS、Concurrency和Parallelism

Go 1.4及以下版本每個作業系統執行緒只使用一個執行上下文execution context)。這意味著每個時間片,只有一個goroutine執行。
從Go 1.5開始可以設定執行上下文的數量為CUP核心數量runtime.NumCPU(),也可以通過GOMAXPROCS環境變數來設定,
還可呼叫runtime.GOMAXPROCS()函式來設定。

注意,GOMAXPROCS並不代表Go執行時能夠使用的CPU數量,它是一個小256的數值,可以設定比實際的CPU數量更大的數字。

56、讀寫操作排序

Go可能會對一些操作排序,但它保證在goroutine的所有行為保持不變。
但是,它無法保證在跨多個goroutine時的執行順序。

package main

import (  
    "runtime"
    "time"
)

var _ = runtime.GOMAXPROCS(3)

var a, b int

func u1() {  
    a = 1
    b = 2
}

func u2() {  
    a = 3
    b = 4
}

func p() {  
    println(a)
    println(b)
}

func main() {  
    go u1()
    go u2()
    go p()
    time.Sleep(1 * time.Second)
    // 多次執行可顯示以下以幾種列印結果
    // 1   2
    // 3   4
    // 0   2 (奇怪嗎?)
    // 0   0    
    // 1   4 (奇怪嗎?)
}

57、優先排程

有一些比較流氓的goroutine會阻止其它goroutine的執行。
例如for迴圈可能就不允許排程器(scheduler)執行。

scheduler會在GC、go語句、阻塞channel的操作、阻塞系統呼叫、lock操作等語句執行之後立即執行。
也可以顯示地執行runtime.Gosched()(讓出時間片)使scheduler執行排程工作。

package main

import (  
    "fmt"
    "runtime"
)

func main() {  
    done := false
    go func(){
        done = true
    }()

    for !done {
        // ... 
        //runtime.Gosched() // 讓scheduler執行排程,讓出執行時間片
    }
    fmt.Println("done!")
}

參考資料:https://blog.csdn.net/gezhong...

相關文章