Go 學習筆記

Undefined443發表於2024-08-18

變數

var a int
var b, c bool = true, false

// factored style
var (
  ToBe   bool       = false
  MaxInt uint64     = 1<<64 - 1
  z      complex128 = complx.Sqrt(-5 + 12i)
)

func main() {
  k := 3  // 函式內可以使用短宣告
}

注意,:= 是宣告符,不是賦值符

陣列和切片

var a [10]int  // 陣列,型別為 [10]int
a[0] = 1

primes := [6]int{2, 3, 5, 7, 11, 13}  // 陣列字面量

var s []int = primes[1:4]  // 切片類似陣列引用,前閉後開。型別為 []T
s := []int{2, 3}  // 切片字面量
s = append(s, 5)  // 向切片新增元素

a := make([]int, 5)  // 使用 make 構造切片

函式

func add(x, y int) int {
  return x + y
}

// 返回多個值
func swap(x, y string) (string, string) {
  return y, x
}
a, b := swap("world", "hello")

指標

var p *int  // int 的指標
p = &a  // 取地址
*p = 2  // 解引用

迴圈

// go 只有 for 迴圈
for i := 0; i < 10; i++ {
  sum += i
}

// for in range
for i, v := range arr {
  fmt.Printf("index: %d, value: v\n", i, v)
}

// omit value
for i := range arr {
  fmt.Printf("index: %d\n", i)
}

// omit index
for _, v := range arr {
  fmt.Printf("value: %d\n", v)
}

// "for" is go's "while"
for sum < 1000 {
  sum += sum
}

// forever
for {
  sum += 1
}

if 和 switch

if x < 0 {
  sum += 1
} else {
  sum -= 1
}

// if 可以帶一個短表示式
if v := math.Pow(x, n); v < lim {  // 只有首字母大寫的名字會被匯出,因此是 Pow
  return v
}

// switch,不需要 break
switch os := runtime.GOOS; os {
  case "darwin":
    fmt.Println("OS X.")
  case "linux":
    fmt.Println("Linux.")
  default:
    // freebsd, openbsd,
    // plan9, windows...
    fmt.Printf("%s.\n", os)
}

函式閉包

func adder() func(int) int {
  sum := 0
  return func(x int) int {
    sum += x
    return sum
  }
}

每次呼叫 addr() 時都返回一個閉包,這個閉包和本次呼叫的 addr() 內的 sum 繫結

map

var m map[string]int  // string: int

m = make(map[string]int)
m["math_score"] = 100

// map 字面量
var m = map[string]int{
	"math_score": 150,
	"phy_score": 110,
}

// 如果頂級型別僅是一個型別名稱,您可以從字面量的元素中省略它。
var m = map[string]Vertex{
  "Bell Labs": {40.68433, -74.39967},
  "Google":    {37.42202, -122.08408},
}

m[key]  // 插入/取出/更新
delete(m, key)  // 刪除元素
elem, ok = m[key]  // 檢測 key 是否在 map 中。如果在,則 ok == true
elem, ok := m[key]  // 如果 elem, ok 還沒有被宣告,你可以使用這種短宣告形式

結構體

type Vertex struct {
	X int
	Y int
}

func main() {
  v := Vertex{1, 2}  // 結構體字面量
  v.X = 4  // 使用結構體

  p := &v  // p 是結構體的指標
  p.X = 1e9  // 隱式解引用

  v2 := Vertex{X: 1}  // 只為指定成員變數賦值
}

關於 type 關鍵字的更多用法:你可以將基本資料型別包裝為自己的型別。

type MyFloat float64
type IPAddr [4]byte  // 定義 IP 地址型別

方法

Go 語言沒有類,但是你可以為型別指定方法。

// 透過在函式名前面加上 receiver 引數列表來將函式變為方法
func (v Vertex) Abs() float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 由於方法經常用來改變 receiver 內部的值,所以 receiver 指標更為常用
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

var v Vertex
v.Scale  // 這裡本來需要提供一個指標,但是 go 進行了指標間接訪問,把 v.Scale 解釋為了 (&v).Scale
p = &v
p.Abs()  // 反過來也是,這裡本來需要一個 value,但是卻提供了指標。go 將 p.Abs 解釋為了 (*p).Abs

介面

介面型別是方法簽名的一個集合

type Abser interface {
  Abs() float64
}

// 任何實現了介面方法的型別都隱式實現了介面
func (f MyFloat) Abs() float64 {
	...
}

func (v *Vertex) Abs() float64 {
  // 可以這樣優雅地處理“空指標”引用
	if t == nil {
	  fmt.Println("<nil>")
	  return
}

// 介面變數可以儲存任何實現了介面方法的值
var a Abser  // nil,一個零值介面既不持有值也不持有具體型別
a = MyFloat(-math.Sqrt2)
a = &Vertex{3, 4}

// 空介面被用於處理未知型別值的程式碼。例如,fmt.Print 接受任意數量的 interface{} 型別引數。
func describe(i interface{}) {
  fmt.Printf("(%v, %T)\n", i, i)
}
// 在 Go 1.18 中,interface{} 被 any 型別代替

// 型別斷言
f, ok := i.(float64)  // 如果介面 i 的底層是 float64,那麼 f = float64,否則 ok = false

// type switches
switch v := i.(type) {
case T:
    // here v has type T
case S:
    // here v has type S
default:
    // no match; here v has the same type as i
}

// 最常見的介面:fmt 包定義的 Stringer 介面
type Stringer interface {
  String() string
}

func (p Person) String() string {
  return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

錯誤

Go程式用 error 值表示錯誤狀態。

error 型別是一個內建介面,類似於 fmt.Stringer

type error interface {
  Error() string
}

函式通常會返回一個 error 值,呼叫程式碼應透過測試 error 是否等於 nil 來處理錯誤。

i, err := strconv.Atoi("42")
if err != nil {
  fmt.Printf("couldn't convert number: %v\n", err)
  return
}
fmt.Println("Converted integer:", i)
// 實現一個 error 結構體
type MyError struct {
  When time.Time
  What string
}

// 實現 error 介面
func (e *MyError) Error() string {
  return fmt.Sprintf("at %v, %s", e.When, e.What)
}

協程

func foo() {
  ...
}

go foo()  // 啟動協程

通道

通道是一種型別化的管道,透過它你可以使用通道運算子 <- 傳送和接收值。

預設情況下,傳送和接收操作會阻塞,直到另一方準備就緒。這使得 goroutines 可以在沒有顯式鎖或條件變數的情況下進行同步。

ch := make(chan int)  // 與 map 和 slice 一樣,通道必須在使用之前建立
ch <- v    // 將 v 傳送到通道 ch
v := <-ch  // 從 ch 接收,並將值賦給 v

// 建立接收通道引數的函式
func sum(s []int, c chan int) {
  sum := 0
  for _, v := range s {
    sum += v
  }
  c <- sum  // 將 sum 傳送給 c
}

func main() {
  s := []int{7, 2, 8, -9, 4, 0}

  c := make(chan int)
  sum(s[:len(s)/2], c)
  x := <-c // 從 c 接收

  fmt.Println(x)
}

帶有緩衝區的通道:

ch := make(chan int, 100)  // 建立一個緩衝區長度為 100 的通道

當緩衝區已滿時,傳送到緩衝通道會被阻塞。當緩衝區為空時,接收會被阻塞。

close(ch)  // 傳送方可以關閉通道以指示不會再傳送更多值
v, ok := <-ch  // 接收方可以接收第二個引數來測試通道是否已關閉
func fibonacci(n int, c chan int) {
  x, y := 0, 1
  for i := 0; i < n; i++ {
    c <- x
    x, y = y, x+y
  }
  close(c)
}

func main() {
  c := make(chan int, 10)
  go fibonacci(cap(c), c)
  // 迴圈會重複地從通道接收值,直到通道被關閉。
  for i := range c {
    fmt.Println(i)
  }
}

select 語句允許一個 goroutine 等待多個通訊操作。

一個 select 阻塞,直到其中一個 case 可以執行,然後執行該 case。如果有多個 case 準備就緒,則隨機選擇一個。

func fibonacci(c, quit chan int) {
  x, y := 0, 1
  for {
    select {
    case c <- x:  // 向 c 傳送
      x, y = y, x+y
    case <-quit:  // 從 quit 接收
      fmt.Println("quit")
      return
    }
  }
}

func main() {
  c := make(chan int)  // 建立通道
  quit := make(chan int)
  go func() {  // 為字面量函式啟動協程
    for i := 0; i < 10; i++ {
      fmt.Println(<-c)  // 從 c 取資料
    }
      quit <- 0  // 停止阻塞 quit
  }()
  fibonacci(c, quit)
}

如果沒有其他 case 準備就緒,那麼執行 default

select {
case i := <-c:
    // use i
default:
    // receiving from c would block
}

互斥鎖

package main

import (
	"fmt"
	"sync"
	"time"
)

// SafeCounter 可以安全地併發使用。
type SafeCounter struct {
  mu sync.Mutex
  v  map[string]int
}

// Inc 對給定 key 的計數器加 1
func (c *SafeCounter) Inc(key string) {
  c.mu.Lock()
  // 獲取鎖,以便一次只有一個 goroutine 可以訪問 map c.v
  c.v[key]++
  c.mu.Unlock()
}

// Value 返回給定 key 的計數器的當前值
func (c *SafeCounter) Value(key string) int {
  c.mu.Lock()
  // 獲取鎖,以便一次只有一個 goroutine 可以訪問 map c.v
  defer c.mu.Unlock()  // 使用 defer 確保在 return 之後再釋放鎖
  return c.v[key]
}

func main() {
  c := SafeCounter{v: make(map[string]int)}  // 這裡只對 map v 賦值
  for i := 0; i < 1000; i++ {
    go c.Inc("somekey")
  }

  time.Sleep(time.Second)
  fmt.Println(c.Value("somekey"))
}

同步鎖

可以使用 sync.WaitGroup 來管理並等待所有的協程完成。

var wg sync.WaitGroup  // 建立一個新的同步鎖
for _, u := range urls {
  wg.Add(1)  // 同步鎖訊號量 +1
  go Crawl(u, depth-1, fetcher, c, &wg)  // 啟動協程
}
subWg.Wait()  // 等待所有協程結束

defer 關鍵字

一個 defer 語句會延遲函式的執行,直到包圍它的函式返回。

func main() {
  defer fmt.Println("world")
  fmt.Println("hello")
}

註釋

註釋方式和 C 一樣

匯入包

import "fmt"  // 匯入單個包

import (  // 匯入多個包
    "fmt"
    "math"
)

import f "fmt"  // 給匯入的包起一個別名

import _ "fmt"  // 匯入包但不使用(僅用於初始化包)

import "myproject/mypackage"  // 匯入本地包

import "github.com/user/package"  // 匯入遠端包

一些注意事項:

  • 匯入的包路徑要使用雙引號括起來。
  • 匯入的包名通常與包的目錄名相同,但也可以在包內使用package關鍵字指定不同的包名。
  • 如果匯入了包但未使用,編譯器會報錯。可以使用空白識別符號_來匯入包但不使用其內容,僅用於初始化該包。
  • 包的匯入路徑可以是相對路徑(本地包)或絕對路徑(遠端包)。
  • 一個 Go 原始檔必須在其 package 宣告之後立即匯入所需的包。

相關文章