背景
公司最近出了 golang 語(yǔ)言規(guī)范,大部分參考 uber 的 go 語(yǔ)言規(guī)范(原版和翻譯),以及官方的 Effective Go。這里分享一下自己之前沒(méi)注意的點(diǎn),查漏補(bǔ)缺
方法和函數(shù)
defer 和返回值賦值的執(zhí)行順序
對(duì)應(yīng)知識(shí)點(diǎn)為方法返回值是有名還是無(wú)名的時(shí)候,defer 的順序的差異
package main
func deferWithAnonymous() int {
ret := 1
defer func() {
ret++
}()
return ret
}
func deferWithNamed() (ret int) {
ret = 1
defer func() {
ret++
}()
return
}
func main() {
println(deferWithAnonymous()) // 1
println(deferWithNamed()) // 2
}
defer 和返回值之間的關(guān)系: 設(shè)置函數(shù)返回值 -> 執(zhí)行 defer -> 最終返回給調(diào)用方
關(guān)鍵在第一步,匿名返回值函數(shù)中,設(shè)置的返回值就是具體的值,而在有名返回值函數(shù),設(shè)置的是返回值的引用(即 ret 的引用)
所以有名返回值函數(shù)的 defer 會(huì)影響最后的返回值
對(duì) defer 的字節(jié)碼解析可以參考這篇文章
sync.Mutex 作為傳參的時(shí)候,需要傳指針,否則可能導(dǎo)致死鎖
因?yàn)?Mutex 的加鎖和釋放鎖邏輯是通過(guò)內(nèi)部的state和sema兩個(gè)整數(shù)對(duì)象控制的,直接拷貝 Mutex 只是復(fù)制了鎖的狀態(tài),但和原來(lái)的鎖并不是同一個(gè),所以釋放復(fù)制后的 Mutex 并不能解鎖原來(lái)的 Mutex
一個(gè)復(fù)現(xiàn)這個(gè)問(wèn)題的示例,是通過(guò) pointer receiver 占鎖,通過(guò) value receiver 釋放鎖,由于 value receiver 會(huì)拷貝調(diào)用者對(duì)象,所以釋放的鎖對(duì)象和外面的不同,導(dǎo)致死鎖
參考-Detect locks passed by value in Go
package main
import "sync"
type T struct {
lock sync.Mutex
}
func (t *T) Lock() {
t.lock.Lock()
}
func (t T) Unlock() {
t.lock.Unlock()
}
func main() {
t := T{lock: sync.Mutex{}}
t.Lock()
t.Unlock()
t.Lock() // 死鎖
}
基本類(lèi)型
interface 的判空
Go 面試題:Go interface 的一個(gè) “坑” 及原理分析
interface 表示 golang 的接口類(lèi)型,它和其他語(yǔ)言的“基類(lèi)”(如 Java 的 interface)相比,在空對(duì)象上的表現(xiàn)不太一樣
示例代碼: 思考以下代碼會(huì)輸出什么
type MyError struct {
msg string
}
func (err *MyError) Error() string {
return err.msg
}
func workWithBalance() bool {
return true
}
func workTooHard() bool {
return false
}
func getError(f func() bool) error {
var err *MyError
if !f() {
err = &MyError{
msg: "need relax",
}
}
return err
}
func main() {
if err := getError(workTooHard); err != nil {
println("work too hard caused " + err.Error())
}
if getError(workWithBalance) == nil {
println("work with balance")
}
}
以上代碼對(duì)自定義錯(cuò)誤 MyError 進(jìn)行了判空,預(yù)期是通過(guò) getError(workWithBalance) 獲取到的 error 為空,結(jié)果卻不為空(work with balance 不會(huì)打?。?/p>
那么為什么 var err *MyError 聲明,但沒(méi)有賦值的 err 判空得到的是 false 呢?我們可以從 interface 的內(nèi)部結(jié)構(gòu) iface、eface 可以了解到端倪
// runtime/runtime2.go
type eface struct {
_type *_type
data unsafe.Pointer
}
type iface struct {
tab *itab
data unsafe.Pointer
}
其中,iface 包含了接口的類(lèi)型、方法和數(shù)據(jù),iface 的 tab 描述了接口的類(lèi)型和方法,data 則指向?qū)嶋H的接口數(shù)據(jù)
itab 的結(jié)構(gòu)如下:
type itab struct {
inter *interfacetype // abi.InterfaceType(abi: application binary interface 二進(jìn)制接口),包含接口類(lèi)型,pkg path(import 的路徑)和接口方法(Imethod)
_type *_type // abi.Type,實(shí)體類(lèi)型
hash uint32 // _type.hash 拷貝而來(lái)
_ [4]byte // 占位,留給以后可能用到的對(duì)象
fun [1]uintptr // 接口方法對(duì)應(yīng)的地址,多個(gè)方法則在這個(gè)數(shù)組后面繼續(xù)添加,fun[0] == 0 表示未實(shí)現(xiàn)接口的方法
}
而 eface 的數(shù)據(jù)結(jié)構(gòu)就簡(jiǎn)單很多了,只包含實(shí)體類(lèi)型 _type 和數(shù)據(jù)指針 data,不包含方法信息
不包含方法的 eface 對(duì)應(yīng) var i interface{} 這種對(duì)象聲明,主要用于 傳參、序列化和泛型場(chǎng)景
那么 go 是如何判斷一個(gè) interface 類(lèi)型對(duì)象是否為空呢?需要兩個(gè)條件:data 對(duì)應(yīng)的值為空,且 _type 類(lèi)型也為空
通過(guò) getError(workWithBalance) 獲取的 error,雖然沒(méi)有被初始化,但它有具體實(shí)現(xiàn)類(lèi)型(MyError)而不是純接口類(lèi)型(error),所以 err == nil 為 false
想要判斷 interface 背后的對(duì)象的值確實(shí)為空,有兩種辦法:先強(qiáng)轉(zhuǎn)成具體的類(lèi)型指針再判斷,或者是通過(guò)反射方法 reflact.ValueOf 獲取到內(nèi)部的值來(lái)判斷
e := getError(workWithBalance)
v := reflect.ValueOf(e)
if e.(*MyError) == nil {
println("err is nil")
}
// 注意: IsNil 對(duì)一些無(wú)法判斷空值的類(lèi)型,或者未初始化的 interface 會(huì)直接 panic,所以需要先判斷 value 的 kind
if v.Kind() == reflect.Pointer {
if v.IsNil() {
println("err is nil")
}
}
擴(kuò)展: 空接口對(duì)象,是否可以調(diào)用接口方法呢?
type MyError struct {
msg string
}
func (err *MyError) Error() string {
if err == nil {
return "empty error"
}
return err.msg
}
func main() {
var emptyErr *MyError
println(emptyErr.Error()) // 不會(huì) panic
}
結(jié)論是可以調(diào)用,這一點(diǎn)和其他語(yǔ)言很不同。一個(gè)指針是否可以調(diào)用方法,取決于它的類(lèi)型而不是實(shí)際值是否為空,空接口對(duì)象調(diào)用 pointer receiver 不會(huì)報(bào)空指針,但注意只是能調(diào)用,如果 pointer receiver 內(nèi)部有獲取對(duì)象屬性的操作,還是會(huì)報(bào)空指針錯(cuò)誤
參考-nil receiver in GoLang
參考-Calling a method on a nil struct pointer doesn’t panic. Why not?
nil channel 的使用場(chǎng)景
在公司規(guī)范中,說(shuō)明“禁止對(duì) nil 或已關(guān)閉的 channel 進(jìn)行讀寫(xiě)關(guān)閉操作”,這一句算是規(guī)范中為數(shù)不多需要指正的一點(diǎn):nil channel 在特定場(chǎng)景是有用的
先了解一下各種特殊情況下使用 channel 會(huì)出現(xiàn)什么情況
closed channel: 讀不阻塞(會(huì)讀完剩下的數(shù)據(jù),之后返回零值)、寫(xiě) panic、再次 close panic
nil channel: 讀阻塞、寫(xiě)阻塞、close panic
對(duì)于 nil channel 讀寫(xiě)都會(huì)阻塞的特性,有一個(gè)使用場(chǎng)景是 合并多個(gè) channel 數(shù)據(jù)的時(shí)候,對(duì)于已經(jīng)取完數(shù)據(jù)的 channel 可以置為空,這樣在繼續(xù)使用 select 的同時(shí)也不影響其他還有數(shù)據(jù)的 channel 的讀取,參考
func merge(a, b <-chan int) <-chan int {
c := make(chan int)
go func() {
defer close(c)
for a != nil || b != nil {
select {
case v, ok := <-a:
if !ok {
fmt.Println("a is done")
a = nil
continue
}
c <- v
case v, ok := <-b:
if !ok {
fmt.Println("b is done")
b = nil
continue
}
c <- v
}
}
}()
return c
}
高性能場(chǎng)景
使用 sync.Pool 獲取需要頻繁申請(qǐng)的對(duì)象
比較典型的場(chǎng)景是在高并發(fā)的數(shù)據(jù)流讀取和寫(xiě)入場(chǎng)景中,通過(guò) pool 緩存 buffer,避免每次都申請(qǐng)新的 buffer 造成頻繁內(nèi)存資源申請(qǐng)
在框架層代碼中會(huì)比較容易看到 pool 的使用,如 gin 用來(lái)緩存處理請(qǐng)求的 Context 對(duì)象,gorm 用來(lái)緩存序列化對(duì)象(SerializerInterface)等
性能測(cè)試結(jié)果:
func BenchmarkByteBufferWithoutPool(b *testing.B) {
for i := 0; i < b.N; i++ {
buf := bytes.Buffer{}
buf.WriteString(longStr)
io.Copy(io.Discard, &buf)
}
}
func BenchmarkByteBufferWithPool(b *testing.B) {
pool := sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
for i := 0; i < b.N; i++ {
buf := pool.Get().(*bytes.Buffer)
buf.WriteString(longStr)
io.Copy(io.Discard, buf)
buf.Reset()
pool.Put(buf)
}
}
// 測(cè)試結(jié)果
// BenchmarkByteBufferWithoutPool-8 55544210 211.1 ns/op 1072 B/op 2 allocs/op
// BenchmarkByteBufferWithPool-8 355192696 33.25 ns/op 0 B/op 0 allocs/op
從執(zhí)行次數(shù)和內(nèi)存開(kāi)銷(xiāo)來(lái)看,pool 在多協(xié)程下達(dá)到的對(duì)象復(fù)用的效果,都能帶來(lái)很大的提升
bytes 和 string 的 0 內(nèi)存申請(qǐng)方法
直接看無(wú)內(nèi)存開(kāi)銷(xiāo)的轉(zhuǎn)換方式:
func ByteSliceToString(bytes []byte) string {
var s string
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
stringHeader.Data = sliceHeader.Data
stringHeader.Len = sliceHeader.Len
return s
}
func StringToByteSlice(s string) (bytes []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&bytes))
sh := *(*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Len = sh.Len
bh.Cap = sh.Len
return
}
參考
兩者的相互轉(zhuǎn)換都用到了反射包中表示底層結(jié)構(gòu)的對(duì)象,如 slice 的 SliceHeader,string 和 StringHeader。因?yàn)?string 和 byte 數(shù)組兩者的底層數(shù)據(jù)結(jié)構(gòu)非常相似,只相差 slice 的 cap,所以轉(zhuǎn)換邏輯并不復(fù)雜
string 和 slice 的底層結(jié)構(gòu)在go源碼中如下:
// runtime/string.go
type stringStruct struct {
str unsafe.Pointer
len int
}
// runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
// reflect/value.go
type StringHeader struct {
Data uintptr
Len int
}
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
go 1.20 之后,StringHeader 和 SliceHeader 被標(biāo)注為 Deprecated,改為推薦使用 StringData 和 SliceData,寫(xiě)法上更簡(jiǎn)單了
參考-The conversion of byte slice and string has changed again in Go 1.20
func byteSliceToString(bytes []byte) string {
return unsafe.String(unsafe.SliceData(bytes), len(bytes))
}
func stringToByteSlice(s string) (bytes []byte) {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
實(shí)測(cè): 直接強(qiáng)轉(zhuǎn)和通過(guò)反射轉(zhuǎn)換的benchmark測(cè)試結(jié)果對(duì)比
bytes 轉(zhuǎn) string
BenchmarkForceConvertBytesToString-8 66501550 178.7 ns/op 1024 B/op 1 allocs/op
BenchmarkConvertBytesToString-8 1000000000 0.3236 ns/op 0 B/op 0 allocs/op
可以看到,強(qiáng)轉(zhuǎn)的方式執(zhí)行速度(平均每次 178ns)遠(yuǎn)小于通過(guò)反射方式執(zhí)行的,并且強(qiáng)轉(zhuǎn)每次需要申請(qǐng) 1kb 內(nèi)存,剛好和轉(zhuǎn)換的字符串大小對(duì)應(yīng)
string 轉(zhuǎn) bytes
BenchmarkForceConvertStringToBytes-8 67139846 200.6 ns/op 1024 B/op 1 allocs/op
BenchmarkConvertStringToBytes-8 1000000000 0.3230 ns/op 0 B/op 0 allocs/op
結(jié)果和 bytes 轉(zhuǎn) string 類(lèi)似,不再贅述
高并發(fā)的任務(wù)(如接口)創(chuàng)建協(xié)程池去消費(fèi)和執(zhí)行
協(xié)程確實(shí)很”輕“,相比操作系統(tǒng)線程默認(rèn)大小為1M 來(lái)說(shuō),它的初始大小只有 2k,確實(shí)很小(但隨著??臻g擴(kuò)大可能會(huì)擴(kuò)縮容),不過(guò)在高并發(fā)場(chǎng)景下還是需要對(duì)開(kāi)啟協(xié)程進(jìn)行控制的
協(xié)程池的選型有很多,常見(jiàn)的開(kāi)源項(xiàng)目有 tunny 和 ants,兩者實(shí)現(xiàn)方式略有區(qū)別,tunny 提交任務(wù)時(shí)是同步提交,可以拿到執(zhí)行后的返回值,ants 是異步提交,不支持獲取返回值,要拿到返回值的話得自己實(shí)現(xiàn)。示例如下:文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-727234.html
import (
"github.com/Jeffail/tunny"
"github.com/panjf2000/ants/v2"
)
func TestTunnyPool(t *testing.T) {
wg := sync.WaitGroup{}
wg.Add(100)
pool := tunny.NewFunc(10, func(payload interface{}) interface{} {
time.Sleep(3 * time.Second)
wg.Done()
return payload
})
defer pool.Close()
for i := 0; i < 100; i++ {
// tunny.pool.Process 是同步方法,所以需要開(kāi)啟協(xié)程才能并發(fā)
go func(i int) {
pool.Process(i)
}(i)
}
wg.Wait()
}
func TestAntsPool(t *testing.T) {
wg := sync.WaitGroup{}
wg.Add(100)
pool, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
fmt.Printf("%d execute\n", i)
time.Sleep(3 * time.Second)
fmt.Printf("%d finish\n", i)
wg.Done()
})
defer pool.Release()
for i := 0; i < 100; i++ {
pool.Invoke(i)
}
wg.Wait()
}
當(dāng)然,對(duì)于 web 框架來(lái)說(shuō),這種控制并發(fā)的功能官方都有。如 gin 通過(guò) limit 插件,本質(zhì)也是通過(guò) channel 控制并發(fā)協(xié)程數(shù)文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-727234.html
到了這里,關(guān)于golang 編程規(guī)范查漏補(bǔ)缺的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!