背景介紹
工作中用Go: 工具篇 - 簡書 (jianshu.com) 介紹了相關工具的使用, 這篇聚集 Go基礎.
Go is simple but not easy.
Go 很簡單,但不容易掌握
type: 類型系統
先說結論:
- 用法: 類型聲明 declare; 類型轉換 trans; 類型別名 alias; 類型斷言 assert
- 值類型 vs 指針類型
- 0值可用
Go內置關鍵字, 大部分可以直接從源碼中查看 <Go>/src/builtin/builtin.go
中查看, 其中大部分都是Go內置類型
怎麼快速查看Go源碼, 可以訪問上一篇blog: 工作中用Go: 工具篇 - 簡書 (jianshu.com)
var 變量
變量的本質: 特定名字 <-> 特定內存塊
- 靜態語言: 變量所綁定的********內存********區域是要有一個明確的邊界的 -> 知道類型才能知道大小
- 指針: 指針雖然大小固定(32bit/64bit, 依賴平臺), 但是其指向的內存, 必須知道類型, 才能知道大小
變量聲明的3種方式:
-
:=
推薦, 支持類型自動推導, 常用分支控制中的局部變量 var
- 函數的命名返回值
// 申明且顯式初始化
a := int(10)
// 默認爲0值
var a int
func A() (a int) // 命名返回值相當於 var a int
變量的作用域(scope)
- 包級別變量: 大寫可導出
- 局部變量: 代碼庫(block
{}
) 控制語句(for if switch)
變量常見問題 - 變量遮蔽(variable shadowing): 定義了同名變量, 容易導致變量混淆, 產生隱藏bug且難以定位
a, err := A()
// do something
b, err := B() // 再次定義同名 err 變量
type alias 類型別名
類型別名(type alias)的存在,是 漸進式代碼修復(Gradual code repair) 的關鍵
// <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
類型別名其實是對現實世界的一種映射, 同一個事物擁有不同的名字的場景太多, 比如 apple
和 蘋果
, 再比如 土豆
和 馬鈴薯
, 更有意思的一個例子:
你們抓周樹人,關我魯迅什麼事? -- 《樓外樓》
0值
Go中基礎類型和0值對照表:
type | 0值 |
---|---|
int byte rune | 0 |
float | 0.0 |
bool | false |
string | "" |
struct | 字段都爲0值 |
slice map pointer interface func | nil |
關於 nil, 可以從源碼中獲取到詳細信息:
// <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(){} // 函數字面值.FunctionLiteral
type HandlerFunc func(ResponseWriter, *Request)
http.HandlerFunc(hello) // hello 和 HandlerFunc 出入參相同, 所以才能進行類型轉換
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 指針類型
結合上面變量的本質來理解:
變量的本質: 特定名字 <-> 特定內存塊
那麼值類型和指針類型就很容易理解: 值類型在函數調用過程中會發生複製, 指向新的內存塊, 而指針則指向同一塊內存
再結合上面的0值, 有一個簡單的規則:
0值的爲 nil 的類型, 函數調用時不會發生複製
當然, 這條規則還需要打上不少補丁, 我們在後面繼續聊
還有一個經典問題: 值類型 vs 指針類型, 怎麼選 / 用哪個?
其實回答這個問題, 只需要列舉幾個 Must
的 case 即可:
-
noCopy
: 不應該複製的場景, 這種情況必須使用指針類型, 尤其要注意 struct, 默認是值類型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() {}
- 不應當複製的場景: 比如結構體使用
[]byte
字段, 如果使用值類型T
導致[]byte
在調用過程中產生複製, 會大大影響性能, 這種情況就要使用*T
, 更多細節, 可以參考這個地址: 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值可用
的, 需要注意幾個點:
-
map
不是0值可用
, 必須進行初始化- 使用 make, 如果知道大小也可以預先指定
- 初始化對應值
- 函數命名返回值中的map
(m map[int]int
, 需要顯式初始化一次
m := make(map[int]int, 10) // 推薦
var m = map[int]int{1:1} // 初始化對應值
- 0值可用的特殊類型:
sync.Mutex
sync.Once
...
// 以 sync.Mutex 的使用舉例
var mu sync.Mutex // 零值不需要額外初始化
type Counter struct {
Type int
Name string
mu sync.Mutex // 1.放在要控制的字段上面並空行 2.內嵌字段
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
}
- 在具體實踐過程中, 類型在申明時沒有賦值會自動賦0值, 就需要注意0值什麼滿足業務需求, 比如:
type ReqListReq struct {
Month string `form:"month"` // 期間, 格式: 202212
Status pay.Status `form:"status"` // 審批狀態
}
type Status int // 審批狀態
const (
StatusNone Status = iota // 0值
StatusInit // 未開始
StatusIng // 審批中
StatusDone // 已通過
StatusReject // 已拒絕
StatusCancel // 已撤回
)
如果請求帶了 status
查詢條件, 則一定非0值
語法和易錯點
byte / rune / string
type rune = int32
: Go中用 rune 表示一個 utf8 編碼, 在 utf8 中, 一個字符由 1-4字節
來編碼
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
性能上的注意點:
-
string 和 []byte
的轉換會分配內存, 一個推薦的做法是[]byte
使用 bytes 包中的方法, 基本 strings 包有的功能, bytes 包都有 - 使用 strings.Builder 時一定要使用
Grow()
, 底層是使用的 slice
slice
- 預先指定大小, 減少內存分配
-
len
需要指定爲 0, len不爲0時會將len
個元素全部設爲0值,append
從len
後的元素開始
-
s := make([]int, 0, 10)
s = append(s, 10)
- 切片的切片: slice 底層使用的
array
, 由於 slice 會自動擴容, 在使用切片的切片時, 就一定要小心: 發生寫操作時, 是否會影響到原來的切片?
map
map不是0值可用, 上面👆🏻已經講到
-
map是無序的, 而且是開發組特意加的, 原因可以參考官方blog, 這一條說起來簡單, 但是實踐上卻非常容易犯錯, 特別是使用 map 返回 keys / values 集合的情況
- 查詢使用的下拉框
- 查詢多行數據後使用 map 拼接數據, 然後使用map返回 values
-
解決map無序通常2個方法
- 使用slice保證順序: 比如上面的例子, 申明瞭個 slice 就好了, 因爲元素都是指針, 讓map去拼數據, 後續返回的 slice 就是最終結果了
- 使用
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
進行判斷, 尤其是map[k]*T
的場景, 極易導致runtime error: invalid memory address or nil pointer dereference
- map不是併發安全, 真這樣寫了, 編譯也不會通過的😅
- map實現
set
, 推薦使用struct{}
明確表示不需要value
type Set[K comparable] map[K]struct{}
struct
- 最重要的其實上面已經介紹過的:
T
是值類型,*T
是指針類型-
T
在初始化時默認會把所有字段設爲爲0值 -
*T
默認是nil, 其實是不可用狀態, 必須要初始化後才能使用 - 值類型
T
會產生複製, 要注意noCopy
的場景
-
var t T // T的所有字段設置爲0值進行初始化
var t *T // nil, 不推薦, t必須初始化才能使用
(t *T) // 函數的命名返回值也會踩這個坑
t := &T{} // 等價的, 都是使用 0 值來初始化T並返回指針, 推薦使用 &T{}
t := new(T)
還有2個奇淫巧技
-
struct{}
是 0 內存佔用, 可以在一些優化一些場景, 不需要分配內存, 比如
- 上面的
type Set[K comparable] map[K]struct{}
- chan:
chan struct{}
- struct 內存對齊(aligned)
// 查看內存佔用: unsafe.Sizeof
i := int32(10)
s := struct {}{}
fmt.Println(unsafe.Sizeof(i)) // 4
fmt.Println(unsafe.Sizeof(s)) // 0
// 查看內存對齊後的內存佔用
unsafe.Alignof()
for
-
for-range
循環, 循環的 v 始終指向同一個內存, 每次都講遍歷的元素, 進行值複製給 v
// bad
var a []T
var b []*T
for _, v := range a {
b = append(b, &v) // &V 都是指向同一個地址, 最後導致 b 中都是相同的元素
}
-
for+go
外部變量 vs 傳參
// bad
for i := 0; i < 10; i++ {
go func() {
println(i) // 協程被調度時, i 的值並不確定
}()
}
// good
for i := 0; i < 10; i++ {
go func(i int) {
println(i)
}(i)
}
break
break + for/select/switch 只能跳出一層循環, 如果要跳出多層循環, 使用 break label
switch
Go中的switch和以前的語言有很大的不同, break只能退出當前switch, 而 Go 中 switch 執行完當前 case 就會退出, 所以大部分情況下, break 都可以省略
func
- 出入參: 還是上面的內容,
值類型 vs 指針類型
, 需要注意的是:string/slice/map
作爲入參, 只是傳了一個描述符進來, 並不會發生全部數據的拷貝 - 變長參數:
func(a ...int)
相當於a []int
- 具名返回值
(a int)
相當於var a int
, 考慮到 0值可用, 一定要注意是否要對變量進行初始化- 適用場景: 相同類型的值進行區分, 比如返回經緯度; 簡短函數簡化0值申明, 是函數更簡潔
- func也是一種類型,
var f func()
和func f()
函數簽名相同(出入參相同)時可以進行類型轉換
err
- 慣例
- 如果有 err, 作爲函數最後一個返回值
- 預期內err, 使用 value; 非預期err, 使用 type
// 初始化
err := errors.New("xxx")
err := fmt.Errorf("%v", xxx)
// wrap
err := fmt.Errorf("wrap err: %w", err)
// 預期內err
var ErrFoo = errors.New("foo")
// 非預期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
可以查看調用棧- 原理是實現了
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 優化過, 性能損失<5% -> 放心使用
- 場景
- 關閉資源:
defer conn.Close()
- 配套使用的函數:
defer mu.Unlock()
recover()
- 上面場景以外的騷操作, 需謹慎編碼
- 關閉資源:
panic
運行時 / panic()
產生, panic 會一直出棧, 直到程序退出或者 recover, 而 defer 一定會在函數運行後執行, 所以:
-
recover()
必須放在 defer 中執行, 保證能捕捉到 panic - 當前協程的 panic 只能被當前協程的 recover 捕獲, 一定要小心 野生goroutine, 詳細參考這篇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)
- 方法的本質, 是將
receiver
作爲函數的第一個參數:func (t *T) xxx(){}
->func xxx(t *T, ){}
- Go有一個語法糖, 無論使用
T
還是*T
的方法, 都可以調用, 但是需要注意細微的差別:- 最重要的:
值類型 vs 指針類型
, 尤其是隻能使用*T
的場景 - 實現 interface 時,
*T
可以使用所有方法, 而T
只能使用T
定義的方法
- 最重要的:
interface
- 最重要的一點:
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 查看:
- 儘量使用小接口(1-3個方法): 抽象程度更高 + 易於實現和測試 + 單一職責易於組合複用
- 接口 in, 接口體 out
Go 社區流傳一個經驗法則:“接受接口,返回結構體(Accept interfaces, return structs)
- 儘量不要使用 any
Go 語言之父 Rob Pike 曾說過:空接口不提供任何信息(The empty interface says nothing)
寫在最後
得益於Go的核心理念和設計哲學:
核心理念:簡單、詩意、簡潔(Simple, Poetic, Pithy)
設計哲學:簡單、顯式、組合、併發和麪向工程
Go 擁有編程語言中相對最少的關鍵字和類型系統, 讓Go入門變得極易學習和上手, 希望這blog能幫助入門的gopher, 更快掌握Go, 同時避免一些易錯點