Go語言的那些坑

RyuGou發表於2018-07-25

Golang是我最喜歡的一門語言,它簡潔、高效、易學習、開發效率高、還可以編譯成機器碼... 雖然它一出世,就飽受關注,而且現在在市面上逐漸流行開來,但是,它畢竟是一門新興語言,還有很多讓人不太習慣的地方(即坑,(^__^)),我作為新手,一邊學習,一邊踩坑,希望對其他人有借鑑作用。

檔名字不要輕易以__test.go為結尾

Golang的source檔案的命名和其他語言本無差別,但是Golang自帶Unit test,它的unit test有個小規範:所有unit test檔案都要以__test.go為結尾! 所以,當你命名一個非unit test檔案為XXX_test.go,而且執意要編譯時,就會報錯:no buildable Go source files in XXXXXX(你的檔案路徑)。 所以,切記,以__test.go為結尾的都是unit test的檔案,且切記不要把unit test檔案和普通Go檔案放到一起,一定要把unit test檔案集體放到一個目錄中,否則會編譯不過的。

語句fmt.Println("這裡是漢字:" + 字串變數) 字串變數的值列印不出來的問題

現有如下程式:

package main

import "fmt"

func main()  {
    m1 := getString()

    fmt.Println("現在是:" + m1)
}

func getString()string{
    return "abd"
}
複製程式碼

執行指令go run test.go

Go語言的那些坑

但是單獨列印變數m1卻可以正常顯示

import "fmt"

func main()  {
    m1 := getString()

    fmt.Println(m1)

    fmt.Println("現在是:" + m1)
}

func getString()string{
    return "abd"
}
複製程式碼

Go語言的那些坑

這是為什麼呢?很奇怪啊! 其實這要怪IDE,我的IDE是phpstorm + Golang外掛包,IDE自帶的console對中文的支援很不友好,帶中文的字串列印出來後,容易顯示不全,其實通過terminal列印出來,是正確的!

Go語言的那些坑

多個defer出現的時候,多個defer之間按照LIFO(後進先出)的順序執行

package main

import "fmt"

func main(){
    defer func(){
        fmt.Println("1")
    }()

    defer func(){
        fmt.Println("2")
    }()

    defer func(){
        fmt.Println("3")
    }()


}
複製程式碼

對應的輸出是:

3
2
1
複製程式碼

panic中可以傳任何值,不僅僅可以傳string

package main

import "fmt"

func main(){

    defer func(){
        if r := recover();r != nil{
            fmt.Println(r)
        }
    }()

    panic([]int{12312})
}
複製程式碼

輸出:

[12312]
複製程式碼

for range來遍歷陣列或者map的時候,被遍歷的指標是不變的,每次遍歷僅執行struct值的拷貝

import "fmt"

type student struct{
    Name string
    Age  int
}

func main(){
    var stus []student

    stus = []student{
        {Name:"one", Age: 18},
        {Name:"two", Age: 19},
    }

    data := make(map[int]*student)

    for i, v := range stus{
        data[i] = &v   //應該改為:data[i] = &stus[i]
    }

    for i, v := range data{
        fmt.Printf("key=%d, value=%v \n", i,v)
    }
}
複製程式碼

所以,結果輸出為:

key=0, value=&{two 19} 
key=1, value=&{two 19}
複製程式碼

Go中沒有繼承!沒有繼承!Go中是叫組合!是組合!

import "fmt"

type student struct{
    Name string
    Age  int
}

func (p *student) love(){
    fmt.Println("love")

}

func (p *student) like(){
    fmt.Println("like first")
    p.love()
}

type boy struct {
    student
}

func (b * boy) love(){
    fmt.Println("hate")
}

func main(){

    b := boy{}

    b.like()
}
複製程式碼

輸出:

like first
love
複製程式碼

不管執行順序如何,當引數為函式的時候,要先計算引數的值

func main(){
    a := 1
    defer print(function(a))
    a = 2;
}

func function(num int) int{
    return num
}
func print(num int){
    fmt.Println(num)
}
複製程式碼

輸出:

1
複製程式碼

注意是struct的函式,還是* struct的函式

import "fmt"

type people interface {
    speak()
}

type student struct{
    name string
    age int
}
func (stu *student) speak(){
    fmt.Println("I am a student, I am ", stu.age)
}


func main(){
    var p people
    p = student{name:"RyuGou", age:12} //應該改為 p = &student{name:"RyuGou", age:12}
    p.speak()
}

複製程式碼

輸出:

cannot use student literal (type student) as type people in assignment:
student does not implement people (speak method has pointer receiver)
複製程式碼

make(chan int)make(chan int, 1)是不一樣的

chan一旦被寫入資料後,當前goruntine就會被阻塞,知道有人接收才可以(即 " <- ch"),如果沒人接收,它就會一直阻塞著。而如果chan帶一個緩衝,就會把資料放到緩衝區中,直到緩衝區滿了,才會阻塞

import "fmt"


func main(){
    ch := make(chan int) //改為 ch := make(chan int, 1) 就好了

    ch <- 1

    fmt.Println("success")
}
複製程式碼

輸出:

fatal error: all goroutines are asleep - deadlock!
複製程式碼

golang 的 select 的功能和 select, poll, epoll 相似, 就是監聽 IO 操作,當 IO 操作發生時,觸發相應的動作。

select 的程式碼形式和 switch 非常相似, 不過 select 的 case 裡的操作語句只能是"IO操作"(不僅僅是取值<-channel,賦值channel<-也可以), select 會一直等待等到某個 case 語句完成,也就是等到成功從channel中讀到資料。 則 select 語句結束

 import "fmt"


func main(){
    ch := make(chan int, 1)

    ch <- 1

    select {
    case msg :=<-ch:
        fmt.Println(msg)
    default:
        fmt.Println("default")
    }

    fmt.Println("success")
}
複製程式碼

輸出:

1
success
複製程式碼

default可以判斷chan是否已經滿了

import "fmt"


func main(){
    ch := make(chan int, 1)

    select {
    case msg :=<-ch:
        fmt.Println(msg)
    default:
        fmt.Println("default")
    }

    fmt.Println("success")
}
複製程式碼

輸出:

default
success
複製程式碼

此時因為ch中沒有寫入資料,為空,所以 case不會讀取成功。 則 select 執行 default 語句。

Go語言中不存在未初始化的變數

變數定義基本方式為:

var 髮量名字 型別 = 表示式
複製程式碼

其中型別和表示式均可省略,如果初始化表示式被省略,將用零值初始化該變數。

  • 數值變數對應的是0值
  • 布林變數對應的是false
  • 字串對應的零值是空字串
  • 介面或者引用型別(包括slice,map,chan)變數對應的是nil
  • 陣列或者結構體等聚合型別對應的零值是每個元素或欄位對應該型別的零值。
 var s string 
 fmt.Println(s) // ""
複製程式碼

:=注意的問題

  • 使用:=定義的變數,僅能使用在函式內部。
  • 在定義多個變數的時候:=周圍不一定是全部都是剛剛宣告的,有些可能只是賦值,例如下面的err變數
    in, err := os.Open(infile)
    // TODO
    out, err := os.Create(outfile)
    複製程式碼

new在Go語言中只是一個預定義的函式,它並不是一個關鍵字,我們可以將new作為變數或者其他

例如:

func delta(old, new int) int { 
    return new - old 
}
複製程式碼

以上是正確的。

並不是使用new就一定會在堆上分配記憶體

編譯器會自動選擇在棧上還是在堆上分配儲存空間,但可能令人驚訝的是,這個選擇並不是由用var還是new宣告變數的方式決定的。

請看例子:


var global *int 

func f() {
    var x int x=1 
    global = &x
}

func g() {
    y := new(int)
    *y = 1 
}

複製程式碼

f()函式中的x就是在堆上分配記憶體,而g()函式中的y就是分配在棧上。

init函式在同一個檔案中可以包含多個

在同一個包檔案中,可以包含有多個init函式,多個init函式的執行順序和定義順序一致。

Golang中沒有“物件”

package main

import (
    "fmt"
)
type test struct {
    name string
}
func (t *test) getName(){
    fmt.Println("hello world")
}
func main() {
    var t *test
    t = nil
    t.getName()
}
複製程式碼

能正常輸出嗎?會報錯嗎?

輸出為:

hello world
複製程式碼

可以正常輸出。Go本質上不是物件導向的語言,Go中是不存在object的含義的,Go語言書籍中的物件也和Java、PHP中的物件有區別,不是真正的”物件”,是Go中struct的實體。

呼叫getName方法,在Go中還可以轉換,轉換為:Type.method(t Type, arguments) 所以,以上程式碼main函式中還可以寫成:

func main() {
    (*test).getName(nil)
}
複製程式碼

Go中的指標*符號的含義

&的意思大家都明白的,取地址,假如你想獲得一個變數的地址,只需在變數前加上&即可。

例如:

a := 1
b := &a
複製程式碼

現在,我拿到a的地址了,但是我想取得a指標指向的值,該如何操作呢?用*號,*b即可。 *的意思是對指標取值。

下面對a的值加一

a := 1
b := &a
*b++
複製程式碼

*&可以相互抵消,同時注意,*&可以抵消,但是&*不可以;所以a*&a是一樣的,和*&*&*&a也是一樣的。

os.Args獲取命令列指令引數,應該從陣列的1座標開始

os.Args的第一個元素,os.Args[0], 是命令本身的名字

package main
import (
    "fmt"
    "os"
)
func main() {
    fmt.Println(os.Args[0])
}
複製程式碼

以上程式碼,經過go build之後,打包成一個可執行檔案main,然後執行指令./main 123

輸出:./main

陣列切片slice的容量問題帶來的bug

請看下列程式碼:

import (
    "fmt"
)
func main(){
    array := [4]int{10, 20, 30, 40}
    slice := array[0:2]
    newSlice := append(slice, 50)
    newSlice[1] += 1
    fmt.Println(slice)
}
複製程式碼

請問輸出什麼? 答案是:

[10 21]
複製程式碼

如果稍作修改,將以上newSlice改為擴容三次,newSlice := append(append(append(slice, 50), 100), 150)如下:

import (
    "fmt"
)
func main(){
    array := [4]int{10, 20, 30, 40}
    slice := array[0:2]
    newSlice := append(append(append(slice, 50), 100), 150)
    newSlice[1] += 1
    fmt.Println(slice)
}
複製程式碼

輸出為:

[10 20]
複製程式碼

這特麼是什麼鬼? 這就要從Golang切片的擴容說起了;切片的擴容,就是當切片新增元素時,切片容量不夠了,就會擴容,擴容的大小遵循下面的原則:(如果切片的容量小於1024個元素,那麼擴容的時候slice的cap就翻番,乘以2;一旦元素個數超過1024個元素,增長因子就變成1.25,即每次增加原來容量的四分之一。)如果擴容之後,還沒有觸及原陣列的容量,那麼,切片中的指標指向的位置,就還是原陣列(這就是產生bug的原因);如果擴容之後,超過了原陣列的容量,那麼,Go就會開闢一塊新的記憶體,把原來的值拷貝過來,這種情況絲毫不會影響到原陣列。 建議儘量避免bug的產生。

map引用不存在的key,不報錯

請問下面的例子輸出什麼,會報錯嗎?

import (
    "fmt"
)

func main(){
    newMap := make(map[string]int)
    fmt.Println(newMap["a"])
}
複製程式碼

答案是:

0
複製程式碼

不報錯。不同於PHP,Golang的map和Java的HashMap類似,Java引用不存在的會返回null,而Golang會返回初始值

map使用range遍歷順序問題,並不是錄入的順序,而是隨機順序

請看下面的例子:


import (
    "fmt"
)

func main(){
    newMap := make(map[int]int)
    for i := 0; i < 10; i++{
        newMap[i] = i
    }
    for key, value := range newMap{
        fmt.Printf("key is %d, value is %d\n", key, value)
    }
}
複製程式碼

輸出:

key is 1, value is 1
key is 3, value is 3
key is 5, value is 5
key is 7, value is 7
key is 9, value is 9
key is 0, value is 0
key is 2, value is 2
key is 4, value is 4
key is 6, value is 6
key is 8, value is 8
複製程式碼

是雜亂無章的順序。map的遍歷順序不固定,這種設計是有意為之的,能為能防止程式依賴特定遍歷順序。

channel作為函式引數傳遞,可以宣告為只取(<- chan)或者只傳送(chan <-)

一個函式在將channel作為一個型別的引數來宣告的時候,可以將channl宣告為只可以取值(<- chan)或者只可以傳送值(chan <-),不特殊說明,則既可以取值,也可以傳送值。

例如:只可以傳送值

func setData(ch chan <- string){
    //TODO
}
複製程式碼

如果在以上函式中存在<-ch則會編譯不通過。

如下是隻可以取值:

func setData(ch <- chan  string){
    //TODO
}
複製程式碼

如果以上函式中存在ch<-則在編譯期會報錯

使用channel時,注意goroutine之間的執行流程問題

package main
import (
    "fmt"
)
func main(){
    ch := make(chan string)
    go setData(ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
func setData(ch  chan  string){
    ch <- "test"
    ch <- "hello wolrd"
    ch <- "123"
    ch <- "456"
    ch <- "789"
}
複製程式碼

以上程式碼的執行流程是怎樣的呢? 一個基於無快取channel的傳送或者取值操作,會導致當前goroutine阻塞,一直等待到另外的一個goroutine做相反的取值或者傳送操作以後,才會正常跑。 以上例子中的流程是這樣的:

主goroutine等待接收,另外的那一個goroutine傳送了“test”並等待處理;完成通訊後,列印出”test”;兩個goroutine各自繼續跑自己的。 主goroutine等待接收,另外的那一個goroutine傳送了“hello world”並等待處理;完成通訊後,列印出”hello world”;兩個goroutine各自繼續跑自己的。 主goroutine等待接收,另外的那一個goroutine傳送了“123”並等待處理;完成通訊後,列印出”123”;兩個goroutine各自繼續跑自己的。 主goroutine等待接收,另外的那一個goroutine傳送了“456”並等待處理;完成通訊後,列印出”456”;兩個goroutine各自繼續跑自己的。 主goroutine等待接收,另外的那一個goroutine傳送了“789”並等待處理;完成通訊後,列印出”789”;兩個goroutine各自繼續跑自己的。

記住:Golang的channel是用來goroutine之間通訊的,且通訊過程中會阻塞。

Go語言的那些坑二

Go語言的那些坑三

更多精彩內容,請關注我的微信公眾號 網際網路技術窩 或者加微信共同探討交流:

Go語言的那些坑

相關文章