工作中用Go: Go基礎(chǔ)

背景介紹

工作中用Go: 工具篇 - 簡書 (jianshu.com) 介紹了相關(guān)工具的使用, 這篇聚集 Go基礎(chǔ).

Go is simple but not easy.

Go 很簡單齿坷,但不容易掌握

type: 類型系統(tǒng)

先說結(jié)論:

  • 用法: 類型聲明 declare; 類型轉(zhuǎn)換 trans; 類型別名 alias; 類型斷言 assert
  • 值類型 vs 指針類型
  • 0值可用

Go內(nèi)置關(guān)鍵字, 大部分可以直接從源碼中查看 <Go>/src/builtin/builtin.go 中查看, 其中大部分都是Go內(nèi)置類型

怎么快速查看Go源碼, 可以訪問上一篇blog: 工作中用Go: 工具篇 - 簡書 (jianshu.com)

var 變量

變量的本質(zhì): 特定名字 <-> 特定內(nèi)存塊

  • 靜態(tài)語言: 變量所綁定的********內(nèi)存********區(qū)域是要有一個明確的邊界的 -> 知道類型才能知道大小
    • 指針: 指針雖然大小固定(32bit/64bit, 依賴平臺), 但是其指向的內(nèi)存, 必須知道類型, 才能知道大小

變量聲明的3種方式:

  • := 推薦, 支持類型自動推導(dǎo), 常用分支控制中的局部變量
  • var
  • 函數(shù)的命名返回值
// 申明且顯式初始化
a := int(10) 

// 默認(rèn)為0值
var a int

func A() (a int) // 命名返回值相當(dāng)于 var a int

變量的作用域(scope)

  • 包級別變量: 大寫可導(dǎo)出
  • 局部變量: 代碼庫(block{}) 控制語句(for if switch)

變量常見問題 - 變量遮蔽(variable shadowing): 定義了同名變量, 容易導(dǎo)致變量混淆, 產(chǎn)生隱藏bug且難以定位

a, err := A()
// do something
b, err := B() // 再次定義同名 err 變量

type alias 類型別名

類型別名(type alias)的存在踩窖,是 漸進(jìn)式代碼修復(fù)(Gradual code repair) 的關(guān)鍵

// <Go>/src/builtin/builtin.go

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

類型別名其實(shí)是對現(xiàn)實(shí)世界的一種映射, 同一個事物擁有不同的名字的場景太多, 比如 apple蘋果, 再比如 土豆馬鈴薯 , 更有意思的一個例子:

你們抓周樹人匆赃,關(guān)我魯迅什么事坞生? -- 《樓外樓》

0值

Go中基礎(chǔ)類型和0值對照表:

type 0值
int byte rune 0
float 0.0
bool false
string ""
struct 字段都為0值
slice map pointer interface func nil

關(guān)于 nil, 可以從源碼中獲取到詳細(xì)信息:

// <Go>/src/builtin/builtin.go

// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

func 也只是類型的一種:

t := T{}
f := func(){} // 函數(shù)字面值.FunctionLiteral

type HandlerFunc func(ResponseWriter, *Request)
http.HandlerFunc(hello) // hello 和 HandlerFunc 出入?yún)⑾嗤? 所以才能進(jìn)行類型轉(zhuǎn)換
func hello(writer http.ResponseWriter, request *http.Request) {
 // fmt.Fprintln(writer, "<h1>hello world</h1>")
 fmt.Fprintf(writer, "<h1>hello world %v</h1>", request.FormValue("name"))
}

值類型 vs 指針類型

結(jié)合上面變量的本質(zhì)來理解:

變量的本質(zhì): 特定名字 <-> 特定內(nèi)存塊

那么值類型和指針類型就很容易理解: 值類型在函數(shù)調(diào)用過程中會發(fā)生復(fù)制, 指向新的內(nèi)存塊, 而指針則指向同一塊內(nèi)存

再結(jié)合上面的0值, 有一個簡單的規(guī)則:

0值的為 nil 的類型, 函數(shù)調(diào)用時不會發(fā)生復(fù)制

當(dāng)然, 這條規(guī)則還需要打上不少補(bǔ)丁, 我們在后面繼續(xù)聊

還有一個經(jīng)典問題: 值類型 vs 指針類型, 怎么選 / 用哪個?

其實(shí)回答這個問題, 只需要列舉幾個 Must 的 case 即可:

  • noCopy: 不應(yīng)該復(fù)制的場景, 這種情況必須使用指針類型, 尤其要注意 struct, 默認(rèn)是值類型T, 如果有 noCopy 字段, 必須使用指針類型*T
// 源碼中 sync.Mutex 上的說明
// A Mutex must not be copied after first use.

// Go中還有特殊 noCopy 類型
// noCopy may be added to structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
//
// Note that it must not be embedded, due to the Lock and Unlock methods.
type noCopy struct{}

// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

  • 不應(yīng)當(dāng)復(fù)制的場景: 比如結(jié)構(gòu)體使用 []byte 字段, 如果使用值類型T 導(dǎo)致 []byte 在調(diào)用過程中產(chǎn)生復(fù)制, 會大大影響性能, 這種情況就要使用*T, 更多細(xì)節(jié), 可以參考這個地址: 03 Decisions | Google Style Guides (gocn.github.io)
// Good:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}

func New() *Record {...}

func (r *Record) Process(...) {...}

func Consumer(r *Record) {...}

// Bad:
type Record struct {
  buf bytes.Buffer
  // other fields omitted
}

func (r Record) Process(...) {...} // Makes a copy of r.buf

func Consumer(r Record) {...} // Makes a copy of r.buf

0值可用

大部分情況下, Go中的類型都是滿足 0值可用 的, 需要注意幾個點(diǎn):

  • map 不是 0值可用 , 必須進(jìn)行初始化
    • 使用 make, 如果知道大小也可以預(yù)先指定
    • 初始化對應(yīng)值
    • 函數(shù)命名返回值中的map (m map[int]int, 需要顯式初始化一次
m := make(map[int]int, 10) // 推薦

var m = map[int]int{1:1} // 初始化對應(yīng)值

  • 0值可用的特殊類型: sync.Mutex sync.Once ...
// 以 sync.Mutex 的使用舉例
var mu sync.Mutex // 零值不需要額外初始化

type Counter struct {
 Type int
 Name string

 mu  sync.Mutex // 1.放在要控制的字段上面并空行 2.內(nèi)嵌字段
 cnt uint64
}

// 1.封裝成方法 
// 2.讀寫都需要
func (c *Counter) Incr() {
 c.mu.Lock()
 c.cnt++
 c.mu.Unlock()
}
func (c *Counter) Cnt() uint64 {
 c.mu.Lock()
 defer c.mu.Unlock()
 return c.cnt
}

  • 在具體實(shí)踐過程中, 類型在申明時沒有賦值會自動賦0值, 就需要注意0值什么滿足業(yè)務(wù)需求, 比如:
type ReqListReq struct {
 Month     string     `form:"month"`      // 期間, 格式: 202212
 Status    pay.Status `form:"status"`     // 審批狀態(tài)
}

type Status int // 審批狀態(tài)

const (
 StatusNone    Status = iota // 0值
 StatusInit                  // 未開始
 StatusIng                   // 審批中
 StatusDone                  // 已通過
 StatusReject                // 已拒絕
 StatusCancel                // 已撤回
)

如果請求帶了 status 查詢條件, 則一定非0值

語法和易錯點(diǎn)

byte / rune / string

type rune = int32: Go中用 rune 表示一個 utf8 編碼, 在 utf8 中, 一個字符由 1-4字節(jié) 來編碼

len("漢") // 3
utf8.RuneCountInString("漢") // 1

[]byte("漢") // []byte{0xE6, 0xB1, 0x89}
[]rune("漢")

遍歷string:

  • for-i / s[0] -> byte
  • for-range -> rune

字符串拼接:

  • + +=
  • fmt
  • strings 或者 bytes 包中的方法: strings.Builder

性能上的注意點(diǎn):

  • string 和 []byte 的轉(zhuǎn)換會分配內(nèi)存, 一個推薦的做法是 []byte 使用 bytes 包中的方法, 基本 strings 包有的功能, bytes 包都有
  • 使用 strings.Builder 時一定要使用 Grow(), 底層是使用的 slice

slice

  • 預(yù)先指定大小, 減少內(nèi)存分配
    • len 需要指定為 0, len不為0時會將 len 個元素全部設(shè)為0值, appendlen 后的元素開始
s := make([]int, 0, 10)
s = append(s, 10)

  • 切片的切片: slice 底層使用的 array , 由于 slice 會自動擴(kuò)容, 在使用切片的切片時, 就一定要小心: 發(fā)生寫操作時, 是否會影響到原來的切片?

map

  • map不是0值可用, 上面????已經(jīng)講到

  • map是無序的, 而且是開發(fā)組特意加的, 原因可以參考官方blog, 這一條說起來簡單, 但是實(shí)踐上卻非常容易犯錯, 特別是使用 map 返回 keys / values 集合的情況

    • 查詢使用的下拉框
    • 查詢多行數(shù)據(jù)后使用 map 拼接數(shù)據(jù), 然后使用map返回 values
  • 解決map無序通常2個方法

    • 使用slice保證順序: 比如上面的例子, 申明了個 slice 就好了, 因?yàn)樵囟际侵羔? 讓map去拼數(shù)據(jù), 后續(xù)返回的 slice 就是最終結(jié)果了
    • 使用sort.Slice 排序
  • map無序還會影響一個騷操作: for-range 遍歷map的時候新增key, 新增的key不一定會被遍歷到

sort.Slice(resp, func(i, j int) bool {
 return resp[i].MonthNumber < resp[j].MonthNumber
})

  • map沒有使用 ok 進(jìn)行判斷, 尤其是 map[k]*T 的場景, 極易導(dǎo)致 runtime error: invalid memory address or nil pointer dereference
  • map不是并發(fā)安全, 真這樣寫了, 編譯也不會通過的??
  • map實(shí)現(xiàn)set, 推薦使用 struct{} 明確表示不需要value
type Set[K comparable] map[K]struct{}

struct

  • 最重要的其實(shí)上面已經(jīng)介紹過的: T是值類型, *T是指針類型
    • T在初始化時默認(rèn)會把所有字段設(shè)為為0值
    • *T 默認(rèn)是nil, 其實(shí)是不可用狀態(tài), 必須要初始化后才能使用
    • 值類型T會產(chǎn)生復(fù)制, 要注意 noCopy 的場景
var t T // T的所有字段設(shè)置為0值進(jìn)行初始化

var t *T // nil, 不推薦, t必須初始化才能使用
(t *T) // 函數(shù)的命名返回值也會踩這個坑

t := &T{} // 等價的, 都是使用 0 值來初始化T并返回指針, 推薦使用 &T{}
t := new(T)

  • 還有2個奇淫巧技
  1. struct{} 是 0 內(nèi)存占用, 可以在一些優(yōu)化一些場景, 不需要分配內(nèi)存, 比如
  • 上面的 type Set[K comparable] map[K]struct{}
  • chan: chan struct{}
  1. struct 內(nèi)存對齊(aligned)
// 查看內(nèi)存占用: unsafe.Sizeof
i := int32(10)
s := struct {}{}
fmt.Println(unsafe.Sizeof(i)) // 4
fmt.Println(unsafe.Sizeof(s)) // 0

// 查看內(nèi)存對齊后的內(nèi)存占用
unsafe.Alignof()

for

  • for-range 循環(huán), 循環(huán)的 v 始終指向同一個內(nèi)存, 每次都講遍歷的元素, 進(jìn)行值復(fù)制給 v
// bad
var a []T
var b []*T
for _, v := range a {
 b = append(b, &v) // &V 都是指向同一個地址, 最后導(dǎo)致 b 中都是相同的元素
}

  • for+go 外部變量 vs 傳參
// bad
for i := 0; i < 10; i++ {
 go func() {
  println(i) // 協(xié)程被調(diào)度時, i 的值并不確定
 }()
}

// good
for i := 0; i < 10; i++ {
 go func(i int) {
  println(i)
 }(i)
}

break

break + for/select/switch 只能跳出一層循環(huán), 如果要跳出多層循環(huán), 使用 break label

switch

Go中的switch和以前的語言有很大的不同, break只能退出當(dāng)前switch, 而 Go 中 switch 執(zhí)行完當(dāng)前 case 就會退出, 所以大部分情況下, break 都可以省略

func

  • 出入?yún)? 還是上面的內(nèi)容, 值類型 vs 指針類型, 需要注意的是: string/slice/map 作為入?yún)? 只是傳了一個描述符進(jìn)來, 并不會發(fā)生全部數(shù)據(jù)的拷貝
  • 變長參數(shù): func(a ...int) 相當(dāng)于 a []int
  • 具名返回值(a int) 相當(dāng)于 var a int, 考慮到 0值可用, 一定要注意是否要對變量進(jìn)行初始化
    • 適用場景: 相同類型的值進(jìn)行區(qū)分, 比如返回經(jīng)緯度; 簡短函數(shù)簡化0值申明, 是函數(shù)更簡潔
  • func也是一種類型, var f func()func f() 函數(shù)簽名相同(出入?yún)⑾嗤?時可以進(jìn)行類型轉(zhuǎn)換

err

  • 慣例
    • 如果有 err, 作為函數(shù)最后一個返回值
    • 預(yù)期內(nèi)err, 使用 value; 非預(yù)期err, 使用 type
// 初始化
err := errors.New("xxx")
err := fmt.Errorf("%v", xxx)

// wrap
err := fmt.Errorf("wrap err: %w", err)

// 預(yù)期內(nèi)err
var ErrFoo = errors.New("foo")

// 非預(yù)期err, 比如 net.Error
// An Error represents a network error.
type Error interface {
 error
 Timeout() bool // Is the error a timeout?

 // Deprecated: Temporary errors are not well-defined.
 // Most "temporary" errors are timeouts, and the few exceptions are surprising.
 // Do not use this method.
 Temporary() bool
}

  • 推薦使用 pkg/errors, 使用 %v 可以查看err信息, 使用 %+v 可以查看調(diào)用棧
    • 原理是實(shí)現(xiàn)了 type Formatter interface
// Format formats the frame according to the fmt.Formatter interface.
//
//    %s    source file
//    %d    source line
//    %n    function name
//    %v    equivalent to %s:%d
//
// Format accepts flags that alter the printing of some verbs, as follows:
//
//    %+s   function name and path of source file relative to the compile time
//          GOPATH separated by \n\t (<funcname>\n\t<path>)
//    %+v   equivalent to %+s:%d
func (f Frame) Format(s fmt.State, verb rune) {
 switch verb {
 case 's':
  switch {
  case s.Flag('+'):
   io.WriteString(s, f.name())
   io.WriteString(s, "\n\t")
   io.WriteString(s, f.file())
  default:
   io.WriteString(s, path.Base(f.file()))
  }
 case 'd':
  io.WriteString(s, strconv.Itoa(f.line()))
 case 'n':
  io.WriteString(s, funcname(f.name()))
 case 'v':
  f.Format(s, 's')
  io.WriteString(s, ":")
  f.Format(s, 'd')
 }
}

defer

  • 性能: Go1.17 優(yōu)化過, 性能損失<5% -> 放心使用
  • 場景
    • 關(guān)閉資源: defer conn.Close()
    • 配套使用的函數(shù): defer mu.Unlock()
    • recover()
    • 上面場景以外的騷操作, 需謹(jǐn)慎編碼

panic

運(yùn)行時 / panic() 產(chǎn)生, panic 會一直出棧, 直到程序退出或者 recover, 而 defer 一定會在函數(shù)運(yùn)行后執(zhí)行, 所以:

  • recover() 必須放在 defer 中執(zhí)行, 保證能捕捉到 panic
  • 當(dāng)前協(xié)程的 panic 只能被當(dāng)前協(xié)程的 recover 捕獲, 一定要小心 野生goroutine, 詳細(xì)參考這篇blog:

Go源碼中還有一種用法: 提示潛在bug

// json/encode.go resolve()
func (w *reflectWithString) resolve() error {
 if w.k.Kind() == reflect.String {
  w.ks = w.k.String()
  return nil
 }
 if tm, ok := w.k.Interface().(encoding.TextMarshaler); ok {
  if w.k.Kind() == reflect.Pointer && w.k.IsNil() {
   return nil
  }
  buf, err := tm.MarshalText()
  w.ks = string(buf)
  return err
 }
 switch w.k.Kind() {
 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
  w.ks = strconv.FormatInt(w.k.Int(), 10)
  return nil
 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
  w.ks = strconv.FormatUint(w.k.Uint(), 10)
  return nil
 }
 panic("unexpected map key type") // 正常情況不會走到這里, 如果走到了, 就有潛在bug
}

方法(method)

  • 方法的本質(zhì), 是將 receiver 作為函數(shù)的第一個參數(shù): func (t *T) xxx(){} -> func xxx(t *T, ){}
  • Go有一個語法糖, 無論使用 T 還是 *T 的方法, 都可以調(diào)用, 但是需要注意細(xì)微的差別:
    • 最重要的: 值類型 vs 指針類型, 尤其是只能使用 *T 的場景
    • 實(shí)現(xiàn) interface 時, *T 可以使用所有方法, 而 T 只能使用 T 定義的方法

interface

  • 最重要的一點(diǎn): interface = type + value, 下面有個很好的例子
func TestInterface(t *testing.T) {
 var a any
 var b error
 var c *error
 d := &b
 t.Log(a == b)   // true
 t.Log(a == c)   // false
 t.Log(c == nil) // true
 t.Log(d == nil) // false
}

使用 debug 查看:

debug: interface & nil
  • 盡量使用小接口(1-3個方法): 抽象程度更高 + 易于實(shí)現(xiàn)和測試 + 單一職責(zé)易于組合復(fù)用
  • 接口 in, 接口體 out

Go 社區(qū)流傳一個經(jīng)驗(yàn)法則:“接受接口煮纵,返回結(jié)構(gòu)體(Accept interfaces, return structs)

  • 盡量不要使用 any

Go 語言之父 Rob Pike 曾說過:空接口不提供任何信息(The empty interface says nothing)

寫在最后

得益于Go的核心理念和設(shè)計哲學(xué):

核心理念:簡單匪凉、詩意式矫、簡潔(Simple, Poetic, Pithy)

設(shè)計哲學(xué):簡單、顯式预明、組合缩赛、并發(fā)和面向工程

Go 擁有編程語言中相對最少的關(guān)鍵字和類型系統(tǒng), 讓Go入門變得極易學(xué)習(xí)和上手, 希望這blog能幫助入門的gopher, 更快掌握Go, 同時避免一些易錯點(diǎn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市撰糠,隨后出現(xiàn)的幾起案子酥馍,更是在濱河造成了極大的恐慌,老刑警劉巖阅酪,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旨袒,死亡現(xiàn)場離奇詭異,居然都是意外死亡术辐,警方通過查閱死者的電腦和手機(jī)砚尽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辉词,“玉大人尉辑,你說我怎么就攤上這事〗嫌欤” “怎么了隧魄?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵卓练,是天一觀的道長。 經(jīng)常有香客問我购啄,道長襟企,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任狮含,我火速辦了婚禮顽悼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘几迄。我一直安慰自己蔚龙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布映胁。 她就那樣靜靜地躺著木羹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪解孙。 梳的紋絲不亂的頭發(fā)上坑填,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音弛姜,去河邊找鬼脐瑰。 笑死,一個胖子當(dāng)著我的面吹牛廷臼,可吹牛的內(nèi)容都是我干的苍在。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼荠商,長吁一口氣:“原來是場噩夢啊……” “哼寂恬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起结啼,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤掠剑,失蹤者是張志新(化名)和其女友劉穎屈芜,沒想到半個月后郊愧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡井佑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年属铁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躬翁。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡焦蘑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出盒发,到底是詐尸還是另有隱情例嘱,我是刑警寧澤狡逢,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站拼卵,受9級特大地震影響奢浑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腋腮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一雀彼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧即寡,春花似錦徊哑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至善涨,卻和暖如春窒盐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背钢拧。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工蟹漓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人源内。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓葡粒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親膜钓。 傳聞我的和親對象是個殘疾皇子嗽交,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容