Go 小白的十萬個為什麼

Remember發表於2021-01-26

上篇文章我提出 channel 在什麼樣的操作下會引發 panic。這篇文章就讓我們來總結一下小白在 go 中經常會問的十萬個為什麼。

string

假設我們要修改型別為字串變數的某個字元,如果是之前世界上最偉大的語言,那麼可以直接這樣(差點忘本不會寫php了):

<?php

$name = "test";
$name[0] = "T";
var_dump($name);
// string(4) "Test"

在 go 中是不允許使用索引下標操作字串變數中的字元的,會直接編譯錯誤。

// 不被允許
  x := "test"
  x[0] = 'T'
  //cannot assign to x[0]

一般要修改我會轉換成 byte。

package main
import "fmt"

func main() {
  s := "test"
  bytes := []byte(s)
  bytes[0] = 'T'
  fmt.Println("s的值:", string(bytes))
  // s的值: Test
}

array

我們來看這樣一段程式,

import "fmt"
func main() {
  a := [3]int{10, 20, 30}
  changeArray(a)
  fmt.Println("a的值:", a)
}

func changeArray(items [3]int) {
  items[2] = 50
}
// a的值: [10 20 30]

答案並不會是 [10,20,30]。因為上面把陣列傳遞給函式時,陣列將會被複制,是一個值傳遞,那麼之後的修改當然和之前的沒有關係。

當然你可以傳遞陣列指標,這樣他們指向的就是同一個地址了。

func main() {
  a := [3]int{10, 20, 30}
  changeArray(&a)
  fmt.Println("a的值:", a)
}

//陣列是值傳遞
func changeArray(items *[3]int) {
  items[2] = 50
}
//a的值: [10 20 50]

或者可以使用 slice。

package main

import "fmt"

func main() {
  s := []int{10, 20, 30}
  changeSlice(s)
  fmt.Println("s的值是:",s)
}
func changeSlice(items []int) {
  items[2] = 50
}
// s的值是: [10 20 50]

slice 本質上不儲存任何資料,它只是描述基礎陣列的一部分。slice 在底層的結構是,

type slice struct {
  array unsafe.Pointer // 底層陣列的指標位置
  len   int // 切片當前長度
  cap   int //容量,當容量不夠時,會觸發動態擴容的機制
}

當傳遞的是 slice,並且切片的底層結構 array 的值還是指向同一個陣列指標地址時,對陣列元素的修改會相互影響。

slice

看看下面的程式碼,

package main

import "fmt"

func main() {
  data := cutting()
  fmt.Printf("data's len:%v,cap:%v\n", len(data), cap(data))
}

func cutting() []int {
  val := make([]int, 1000)
  fmt.Printf("val's len:%v,cap:%v\n ", len(val), cap(val))
  return val[0:10]
}
// val's len:1000,cap:1000
// data's len:10,cap:1000

就像上面說的,當在原有 slice 的基礎上擷取出新的 sliceslice 將會引用原切片的底層陣列。如果是一個大的切片,會導致記憶體的浪費。

我們可以通過額外定義一個容量大小合適的變數,然後通過 copy 操作。

package main

import "fmt"

func main() {
  data := cutting()
  fmt.Printf("data's len:%v,cap:%v\n", len(data), cap(data))
}
func cutting() []int {
  val := make([]int, 1000)
  fmt.Printf("val's len:%v,cap:%v\n ", len(val), cap(val))
  res := make([]int, 10)
  copy(res, val)
  return res
}

//val's len:1000,cap:1000
// data's len:10,cap:10

copy

既然上面出現了 copy,那麼我們來看看 copy

package main

import "fmt"

func main() {
  var test1, test2 []int
  test1 = []int{1, 2, 3}
  copy(test2, test1)
  fmt.Println("test2 的值:", test2)
}
// test2 的值: []

為什麼會這樣?

copy 複製的元素數目是兩個切片中最小的長度。當前 test1test2 的最小長度為 test2 的0,因此最終返回空切片,我們可以為 test2 分配長度。

package main

import "fmt"

func main() {
  var test1, test2 []int
  test1 = []int{1, 2, 3}
  test2=make([]int,len(test1))
  copy(test2,test1)
  fmt.Println("test2 的值:",test2)
}
// test2 的值: [1 2 3]

range

我們來看下面的程式碼,

package main

import "fmt"

func main() {
  res := []int{1, 2, 3}
  for _, item := range res {
    item *= 10
  }
  fmt.Println("res:", res)
}
// res: [1 2 3]

最終的值並沒有想象中的 [10,20,30]。為什麼?

因為在 go 中 range 第二個返回值實際上是一個值拷貝。

這也告訴我們當遍歷切片型別為結構體時,需要避免這樣的操作,此操作會消耗大量的記憶體,我們可以通過索引下標搞定。

package main

import "fmt"

func main() {
  res := []int{1, 2, 3}
  for index, _ := range res {
    res[index] *= 10
  }
  fmt.Println("res:", res)
  // res: [10 20 30]
}

struct

我們經常會使用 “==” 去判斷兩個結構體是否相等,比如,

package main

import "fmt"

type User struct {
  Name string
  Age  int

}
func main() {
  user1 := User{}
  user2 := User{}
  fmt.Println(user1 == user2)
  // true
}

這樣是沒問題的,如果我往結構體加一個這樣的欄位呢?

package main

import "fmt"

type User struct {
  Name string
  Age  int
  IsChild func(age int) bool
}

func main() {
  user1 := User{}
  user2 := User{}
  fmt.Println(user1 == user2)
  // invalid operation: user1 == user2 (struct containing func(int) bool cannot be compared)
}

直接報編譯錯誤,為什麼?

如果結構體中的任一欄位不具有可比性,那麼使用 “==” 運算子會導致編譯出錯。上面我加的 IsChild 欄位型別為閉包函式顯然不具備可比性。

groutine

package main

import (
  "fmt"
)

func main() {
  var hi string
  go func() {
    hi = "golang"
  }()
  fmt.Println(hi)
}

以上輸出什麼?

大概率啥都沒有,因為在子協程給 hi 變數賦值前,主協程大概率先列印,然後執行結束,接著整個程式結束了。

用最愚蠢的方法讓主協程停一下。

package main

import (
  "fmt"
  "time"
)

func main() {
  var hi string
  go func() {
    hi = "golang"
  }()
  time.Sleep(10 * time.Millisecond)
  fmt.Println(hi)
  //golang
}

再來看這題,

package main

import (
  "fmt"
  "sync"
)

func main() {
  var wg sync.WaitGroup
  for i := 0; i < 5; i++ {
    wg.Add(1)
    go func() {
      fmt.Println("值是:", i)
      wg.Done()
    }()
  }
  wg.Wait()
}

每次執行,答案都不相同,但是大概率都是5。這裡的操作存在資料競爭,即 data race。這種情況發生的條件是,當兩個或兩個以上 groutine 併發地訪問同一個變數並且有一個訪問是寫入時,就會引發 data race

可以通過命令列新增引數 -race 執行檢測。
圖片

修復的方式也會簡單,在啟動 groutine 時使用區域性變數並將數字作為引數傳遞。

package main

import (
  "fmt"
  "sync"
)
func main() {
  var wg sync.WaitGroup
  for i := 0; i < 5; i++ {
    wg.Add(1)
    go func(item int) {
      fmt.Println("值是:", item)
      wg.Done()
    }(i)
  }
  wg.Wait()
}

recover()

在 go 中可以使用 recover() 函式捕獲 panic,但是我們也需要注意它的用法,以下使用姿勢都是錯誤的。

package main

import "fmt"

func main() {
  recover()
  panic("make error")
}
// 錯誤姿勢
package main

import "fmt"


func main() {
  doRecover()
  panic("make error")
}

func doRecover() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println("出錯了")
    }
  }()
}
// 錯誤姿勢
package main

import "fmt"


func main() {
  defer func() {
    defer func() {
      if err := recover(); err != nil {
        fmt.Println(err)
      }
    }()
  }()
  panic("make error")
}
// 錯誤姿勢

它只有在延遲函式中直接呼叫才能生效。

package main

import "fmt"

func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println(err)
    }
  }()
  panic("make error")
}
// 正確姿勢

還有好多錯誤姿勢沒有列舉,你有不一樣的錯誤操作嘛?歡迎下方留言一起討論。

如果文章對你有所幫助,點贊、轉發、留言都是一種支援!
圖片

本作品採用《CC 協議》,轉載必須註明作者和本文連結
吳親庫裡

相關文章