go 筆記#
基礎#
變量#
var 變量名 類型 = 表達式
其中 “類型” 或 “= 表達式” 兩個部分可以省略其中的一個。如果省略的是類型信息,那麼將根據
初始化表達式來推導變量的類型信息。如果初始化表達式被省略,那麼將用零值初始化該變
量。數值類型變量對應的零值是 0,布爾類型變量對應的零值是 false,字符串類型對應的零
值是空字符串,接口或引用類型(包括 slice、指針、map、chan 和函數)變量對應的零值是
nil。數組或結構體等聚合類型對應的零值是每個元素或字段都是對應類型的零值。
make、new#
i := 0
u := user{}
二者都是內存的分配(堆上),但是 make 只用於 slice、map 以及 channel 的初始化(非零值);而 new 用於類型的內存分配,並且內存置為零
make 返回的還是這三個引用類型本身;而 new 返回的是指向類型的指針
new 不常用,通常都是採用短語句聲明以及結構體的字面量達到我們的目的
range#
Go 語言中 range 關鍵字用於 for 循環中迭代數組 (array)、切片 (slice)、通道 (channel) 或集合 (map) 的元素。在數組和切片中它返回元素的索引和索引對應的值,在集合中返回 key-value 對
切片#
//創建一個初始元素長度為5的數組切片,元素初始值為0:
mySlice1 := make([]int, 5)
//創建一個初始元素長度為5的數組切片,元素初始值為0,並預留10個元素的存儲空間:
mySlice2 := make([]int, 5, 10)
//切片字面量創建長度為5容量為5的切片,需要注意的是 [ ] 裡面不要寫數組的容量,因為如果寫了個數以後就是數組了,而不是切片了。
mySlice3 := []int{10,20,30,40,50}
函數#
-
函數語法格式:
func funcName(parametername type1, parametername type2) (output1 type1, output2 type2) { //這裡是處理邏輯代碼 //返回多個值 return value1, value2 }
- func:函數由 func 開始聲明
- funcName:函數名稱,函數名和參數列表一起構成了函數簽名
- 小寫:同一個包內可調用,不同包不可調用
- 大寫:不同包內可調用(通過 import 包)
- parametername type:參數列表
- output type:返回值,Go 語言支持一個函數返回多個值
-
值傳遞
-
引用傳遞
引用傳遞本質引用傳遞本質上也是值傳遞,只不過這份值是一個指針(地址)
Go 語言中 slice,map 和 channel 這三種類型的實現機制類似指針, 所以可以直接傳遞,而不用取地址後傳遞指針。
-
defer
函數中添加 defer 語句,defer 是採用後進先出模式
func ReadWrite() bool { file.Open("file") defer file.Close() }
踩坑點:
func testDefer() (r int) { x := 1 defer func(xPtr *int) { *xPtr++ }(&x) return x }
return x 不是一條原子指令,實際上 x=r def return
defer 是後進先出,協程遇到 panic 時,遍歷本協程的 defer 鏈表,並執行 defer,如果沒有遇到 recover,遍歷完本協程的 defer 鏈表後,向 stderr 跑出 panic 信息
-
方法
Go 語言中同時有函數和方法。一個方法就是一個包含了 接受者 的函數,接受者可以是命名類型或者結構體類型的一個值或者是一個指針
-
繼承
type Human struct { Name string Age int Phone string } type Employee struct { Human Salary int Currency string }
-
接口
// 定義一個接口 Men type Men interface { SayHi() Sing(lyrics string) }
- interface 可以被任意的對象實現
- 一個對象可以實現任意多個 interface
- 任意的類型都實現了空的 interface
- 一個 struct 如果實現了一個 interface,那麼一個 interface 類型的變量 i,是可以被賦值為 struct 類型的,我們也可以從這個變量 i 中取出這個 struct 類型的值
錯誤#
一個函數或方法返回一個錯誤,必須是函數返回的最後一個值
處理錯誤的方法:返回的錯誤與 nil 進行比較,nil 值表示沒有發生錯誤
goroutine#
封裝 main 函數的 goroutine 稱為主 goroutine
- 設定每一個 goroutine 所能申請的棧空間的最大值
- 創建一個特殊的 defer 語句,用於 goroutine 退出時做必要的善後處理
- 啟動專用於在後台清掃內存垃圾的 goroutine,並設置 gc 可用的標識
- 執行 main 包中的 init 函數
- 執行 main 函數
- 檢查 goroutine 是否引發運行 panic
- 最後主 goroutine 會結束自己以及當前進程的運行
channel#
-
使用 channel 多重返回值
-
使用 for range 簡化語法
-
對一個關閉的 channel 再發送值就會導致 panic
-
對一個關閉的 channel 進行接收會一直獲取值直到 channel 为空
-
對一個關閉的並且沒有值的 channel 執行接收操作會得到對應類型的零值
-
關閉一個已經關閉的 channel 會導致 panic
-
無緩衝 channel (make (chan int)),必須有接收者
-
關閉 channel
- m 個接收者,1 個發送者,發送者通過關閉數據通道
func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const MaxRandomNumber = 100000 const NumReceivers = 100 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // 發送者 go func() { for { if value := rand.Intn(MaxRandomNumber); value == 0 { // 唯一的發送者可以安全地關閉通道。 close(dataCh) return } else { dataCh <- value } } }() // 接收者 for i := 0; i < NumReceivers; i++ { go func() { defer wgReceivers.Done() // 接收數據直到 dataCh 被關閉或者 // dataCh 的數據緩存隊列是空的。 for value := range dataCh { log.Println(value) } }() }
1 個接收者,n 個發送者,接收者通過關閉一個信號通道
package main import ( "time" "math/rand" "sync" "log" ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const MaxRandomNumber = 100000 const NumSenders = 1000 wgReceivers := sync.WaitGroup{} wgReceivers.Add(1) // ... dataCh := make(chan int, 100) stopCh := make(chan struct{}) // stopCh 是一個信號通道。 // 它的發送者是 dataCh 的接收者。 // 它的接收者是 dataCh 的發送者。 // 發送者 for i := 0; i < NumSenders; i++ { go func() { for { // 第一个 select 是为了尽可能早的尝试退出 goroutine。 // 事实上,在这个特殊的例子中,这不是必要的,所以它能省略。 select { case <- stopCh: return default: } // 即使 stopCh 已经关闭,如果发送给 dataCh 没有阻塞,那么在第二个 select 中第一个分支可能会在一些循环中不会执行。 // 但是在这里例子中是可接受的, 所以上面的第一个 select 代码块可以被省略。 select { case <- stopCh: return case dataCh <- rand.Intn(MaxRandomNumber): } } }() } // 接收者 go func() { defer wgReceivers.Done() for value := range dataCh { if value == MaxRandomNumber-1 { // dataCh 通道的接收者也是 stopCh 通道的发送者。 // 在这里关闭停止通道是安全的。. close(stopCh) return } log.Println(value) } }() // ... wgReceivers.Wait() }
m 個接收者,n 個發送者,一個通過通知一個主持人關閉一個信號通道
package main import ( "time" "math/rand" "sync" "log" "strconv" ) func main() { rand.Seed(time.Now().UnixNano()) log.SetFlags(0) // ... const MaxRandomNumber = 100000 const NumReceivers = 10 const NumSenders = 1000 wgReceivers := sync.WaitGroup{} wgReceivers.Add(NumReceivers) // ... dataCh := make(chan int, 100) stopCh := make(chan struct{}) // stopCh 是一個信號通道。 // 它的發送者是下面的主持人 goroutine。 // 它的接收者是 dataCh的所有發送者和接收者。 toStop := make(chan string, 1) // toStop 通道通常用來通知主持人去關閉信號通道( stopCh )。 // 它的發送者是 dataCh的任意發送者和接收者。 // 它的接收者是下面的主持人 goroutine var stoppedBy string // 主持人 go func() { stoppedBy = <-toStop close(stopCh) }() // 發送者 for i := 0; i < NumSenders; i++ { go func(id string) { for { value := rand.Intn(MaxRandomNumber) if value == 0 { // 這裡,一個用於通知主持人關閉信號通道的技巧。 select { case toStop <- "sender#" + id: default: } return } // 這裡的第一个 select 是为了尽可能早的尝试退出 goroutine。 //這個 select 代碼塊有 1 個接受行為 的 case 和 一個將作為 Go 編譯器的 try-receive 操作進行特別優化的默認分支。 select { case <- stopCh: return default: } // 即使 stopCh 被關閉, 如果 发送到 dataCh 的操作没有阻塞,那么第二个 select 的第一个分支可能会在一些循环(和在理论上永远) // 不会执行 // 這就是為什麼上面的第一个 select 代碼塊是必須的。 select { case <- stopCh: return case dataCh <- value: } } }(strconv.Itoa(i)) } // 接收者 for i := 0; i < NumReceivers; i++ { go func(id string) { defer wgReceivers.Done() for { // 和發送者 goroutine 一樣, 這裡的第一个 select 是为了尽可能早的尝试退出 這個 goroutine。 // 這個 select 代碼塊有 1 個發送行為 的 case 和 一個將作為 Go 編譯器的 try-send 操作進行特別優化的默認分支。 select { case <- stopCh: return default: } // 即使 stopCh 被關閉, 如果從 dataCh 接受數據不會阻塞,那麼第二個 select 的第一分支可能會在一些循環(和理論上永遠) // 不會被執行到 // 這就是為什麼第一个 select 代碼塊是必要的。 select { case <- stopCh: return case value := <-dataCh: if value == MaxRandomNumber-1 { // 同樣的技巧用於通知主持人去關閉信號通道。 select { case toStop <- "receiver#" + id: default: } return } log.Println(value) } } }(strconv.Itoa(i)) } // ... wgReceivers.Wait() log.Println("stopped by", stoppedBy) }
json#
- 首先查找 tag 名字(關於 json tag 的解釋參看下一節)為
Field
的字段 - 然後查找名字為
Field
的字段 - 最後再找名字為
Field
等大小寫不敏感的匹配字段。 - 如果都沒有找到,就直接忽略這個 key,也不会報錯。這對於要從眾多數據中只選擇部分來使用非常方便。
reflect#
var i int = 3
type test struct{}
var t test
v := reflect.ValueOf(i)
v2 := reflect.ValueOf(&t).Elem()
reflect.ValueOf ():返回值
reflect.ValueOf ().Elem ():傳入必須是指針類型
struct#
struct {}{}:空結構體的作用
因為空結構體不佔據內存空間,因此被廣泛作為各種場景下的佔位符使用。一是節省資源,二是空結構體本身就具備很強的語義,即這裡不需要任何值,僅作為佔位符。
Go 語言標準庫沒有提供 Set 的實現,通常使用 map 來代替。事實上,對於集合來說,只需要 map 的鍵,而不需要值。即使是將值設置為 bool 類型,也會多佔據 1 個字節,那假設 map 中有一百萬條數據,就會浪費 1MB 的空間。
因此呢,將 map 作為集合 (Set) 使用時,可以將值類型定義為空結構體,僅作為佔位符使用即可。
go 內存申請#
程序向操作系統申請一塊內存 (堆和棧)
申請到棧內存好處:函數返回直接釋放,不會引起垃圾回收,對性能沒有影響
申請到堆內存:引起垃圾回收
// 實例1:棧
func F() {
temp := make([]int, 0, 20)
...
}
// 實例2:堆
func F() []int{
a := make([]int, 0, 20)
return a
}
// 實例3
func F() {
a := make([]int, 0, 20) // 棧
b := make([]int, 0, 20000) // 容量比較大,堆
l := 20
c := make([]int, 0, l) // 不定長,堆
}
go init#
import --> const --> var --> init()
每個 package 中每個 init () 函數都會被調用,且順序固定
對同一個 go 文件的 init () 調用順序是從上到下的
對同一個 package 中不同文件是按文件名字符串比較 “從小到大” 順序調用各文件中的 init () 函數,對於
對不同的 package,如果不相互依賴的話,按照 main 包中 "先 import 的後調用" 的順序調用其包中的 init ()
如果 package 存在依賴,則先調用最早被依賴的 package 中的 init ()
unsafe.Pointer#
大多數指針類型會寫成
*T
,表示是 “一個指向 T 類型變量的指針”。unsafe.Pointer 是特別定義的一種指針類型(譯注:類似 C 語言中的void*
類型的指針),它可以包含任意類型變量的地址
- Sizeof 接受任意類型的值 (表達式),返回其佔用的字節數,這和 c 語言裡面不同,c 語言裡面 sizeof 函數的參數是類型,而這裡是一個值,比如一個變量
- Offsetof:返回結構體成員在內存中的位置距離結構體起始處的字節數,所傳參數必須是結構體的成員(結構體指針指向的地址就是結構體起始處的地址,即第一個成員的內存地址)
flag 庫#
使用
flag
庫的一般步驟:
- 定義一些全局變量存儲選項的值,如這裡的
intflag/boolflag/stringflag
; - 在
init
方法中使用flag.TypeVar
方法定義選項,這裡的Type
可以為基本類型Int/Uint/Float64/Bool
,還可以是時間間隔time.Duration
。定義時傳入變量的地址、選項名、默認值和幫助信息; - 在
main
方法中調用flag.Parse
從os.Args[1:]
中解析選項。因為os.Args[0]
為可執行程序路徑,會被剔除。
go -ini 庫#
context 庫#
創建 context:
- context.Backgroud()
- context.TODO
With 系列函數
// 取消控制
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 超時控制
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 攜帶數據
func WithValue(parent Context, key, val interface{}) Context
sync 庫#
-
sync.map:#
sync.map.Range () 遍歷求長度
-
sync.pool:緩存對象#
在 Put 之前重置,在 Get 之後重置
-
sync.RWMutex:讀寫鎖#
如果設置了寫鎖,其他讀線程和寫線程就無法獲得鎖。這時候和互斥鎖的作用是一樣的
如果設置了讀鎖,其他寫線程不能拿到鎖,但是其他讀線程可以拿到鎖
-
sync.Cod:
sync.Cond
典型的使用場景是
生產 - 消費者模式,多個
goroutine等待某個事件發生, 單個
goroutine 通知某個事件已發生。
etcd#
etcd 是一個高可用強一致性的鍵值倉庫在很多分佈式系統架構中得到了廣泛的應用,其最經典的使用場景就是服務發現
- 一個強一致性、高可用的服務存儲目錄
- 一種註冊服務和監控服務健康狀態的機制
- 一種查找和連接服務的機制
流程:
從 Leader (主節點)流向 Follower;
用戶可以對 etcd 集群中的所有節點進行讀寫
micro#
go 語言構建微服務的基礎框架
例子:
服務端
service guild {
//查詢
rpc Find(FindRequest) returns (FindResponse){}
//查詢玩家數據
rpc FindUser(FindUserRequest) returns (FindUserResponse){}
}
guild 服務:
main.go
func main() {
reg := etcd.NewRegistry(func(op *registry.Options) {
op.Addrs = settings.GetEtcdSysAddr()
})
uuid := utils.GenUUID()
s := grpc.NewService(
micro.Server(grpc2.NewServer(func(options *server.Options) {
options.Id = uuid
})),
micro.Name(fmt.Sprintf("com.jsw.server.guild.%d", settings.GetSetId())),
micro.Registry(reg),
micro.RegisterInterval(time.Second*2),
micro.RegisterTTL(time.Second*5),
micro.WrapHandler(LogHandler),
)
// GuildHandler對象必須實現.proto文件定義的這些接口函數
registerErr := guildProto.RegisterGuildHandler(s.Server(), &handler.GuildHandler{})
s.Init()
s.Run()
}
guild.go
type GuildHandler struct {
}
func (m *GuildHandler) Find(ctx context.Context, request *guildProto.FindRequest, response *guildProto.FindResponse) error {}
func (m *GuildHandler) FindUser(ctx context.Context, request *guildProto.FindUserRequest, response *guildProto.FindUserResponse) error {}
game 服務:
guildServer = guildProto.NewGuildService(fmt.Sprintf("com.jsw.server.guild.%d", setId), ser.Client())
gorm#
// 獲取第一條記錄(主鍵升序)
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;
// 獲取一條記錄,沒有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;
// 獲取最後一條記錄(主鍵降序)
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
go protobuf#
-
.proto 文件定義
go_package = "xxx/proto/game;com_jsw_server_game"
,;前面是生成.pb.go 文件路徑,;後面是別名 -
//go:generate protoc --proto_path=. --proto_path=../ --proto_path=../public --proto_path=../game --gofast_out=. --micro_out=. common.proto
-
grpc
message SimpleRequest { string name = 1; } message SimpleResponse { string message = 1; } service SimpleService { rpc Get(SimpleRequest) returns (SimpleResponse) {} // 簡單模式 rpc GetUser(SimpleRequest) return (stream SimpleResponse) {} // 服務端數據流模式 rpc GetUser(stream SimpleRequest) return (SimpleResponse) {} // 客戶端數據流模式 rpc GetUser(stream GetUserRequest) returns (stream GetUserResponse) {} // 雙端模式 }
四種模式:
-
簡單 RPC
// c resopnse, err := client.Get(contect.Background(), &pb.SimpleRequest{Name: "hello"}) // s func (s *Simple) Get(ctx context.Context, req *pb.SimpleRequest) (resp *pb.SimpleResponse, error) { return resp }
-
服務端數據流
// c user, err := client.GetUser(context.Background(), &pb.SimpleRequest{Name: "hello"}) for { resp, err := user.Recv() // 調用Recv去接收 if err == io.EOF { break } ... } // s func (s *ServerSide) GetUser(req *pb.GetUserRequest, stream pb.ServerSide_GetUserServer) error { ... for { stream.Send(&pb.SimpleResponse{}) // 調用Send去回覆 } return nil }
-
客戶端數據流
// c user, err := client.GetUser(context.Background()) for { err := user.Send(&pb.SimpleRequest{Name: "hello"}) } // s func (c *ClientSide) GetUser(stream pb.ClientSide_GetUserServer) error { recv, err := stream.Recv() if err == io.EOF { return stream.SendAndClose(&pb.GetUserResponse{}) } }
-
雙向數據流
// c user, err := client.GetUser(context.Background()) err := user.Send(&pb.SimpleRequest{Name: "hello"}) err = user.CloseSend() // s func (b *BidirectionalService) GetUser(stream pb.BidirectionalService_GetUserServer) (err error) { req, err := stream.Recv() err = stream.Send(&pb.SimpleRequest{Name: "hello"}) }
-
go cli#
cli
是一個用於構建命令行程序的庫,創建一個 cli.App 結構對象
func main() {
app := &cli.App{
Name: "hello",
Usage: "hello world example",
Action: func(c *cli.Context) error {
fmt.Println("hello world")
return nil
},
}
err := app.Run(os.Args)
}
Name 和 Usage 都顯示在幫助中
Action 是調用該命令行程序實際執行的函數