<style> .markdown-body { max-width: 1280px; } </style> --- title: 'Golang 教學筆記' ---[TOC] # Golang 教學筆記 # 安裝 ### [Golang官網](https://golang.org/) ### [線上免安裝小編輯器](https://play.golang.org/p/MAohLsrz7JQ) ### [線上文件](https://golang.org/doc/) ### [命令提示字元指令集](https://wiki.jikexueyuan.com/project/go-command-tutorial/0.1.html) 在命令提示字元輸入指令即可 # 安裝Vscode後的設定 Windows 熱鍵 Ctrl + , [Go 开发相关配置](https://songjiayang.gitbooks.io/go-basic-courses/content/ch2/readme.html) ![](https://i.imgur.com/wW1QMKN.png) ```go= "files.autoSave": "onFocusChange", "editor.formatOnSave": true, "go.gopath":"${workspaceRoot}:/Users/jinxue/golib", // 当前工作空间${wordspaceRoot}加上系统 GOPATH 目录 "go.goroot": "/usr/local/Cellar/go/1.9/libexec", // go 的安装目录 "go.formatOnSave": true, //在保存代码时自动格式化代码 "go.formatTool": "goimports", //使用 goimports 工具进行代码格式化,或者使用 goreturns 和 gofmt "go.buildOnSave": true, //在保存代码时自动编译代码 "go.lintOnSave": true, //在保存代码时自动检查代码可以优化的地方,并给出建议 "go.vetOnSave": false, //在保存代码时自动检查潜在的错误 "go.coverOnSave": false, //在保存代码时执行测试,并显示测试覆盖率 "go.useCodeSnippetsOnFunctionSuggest": true, //使用代码片段作为提示 "go.gocodeAutoBuild": false //代码自动编译构建 ``` **編譯 test.go 程式碼, 產生執行檔** ```go go build test.go ``` 清除編譯結果 ```go go clean ``` 安裝套件1 [go get 套件url] ```go go get github.com/gorilla/websocket ``` 編譯 + 執行 test.go ```go go run test.go ``` 單元測試 (函式要宣告成 func TestNew(t *testing.T)) ```go go test -v -run=New ``` 檢查資料競爭 ```go go run -race test.go ``` * go cover : 測試代碼覆蓋率 * godoc: 用於生成go文檔 * pprof:用於性能調優,針對cpu,內存和並發 * race:用於競爭檢測 # Golang - GOROOT、GOPATH、Go Modules 三者的關係介紹 [Golang - GOROOT、GOPATH、Go Modules 三者的關係介紹](https://blog.kennycoder.io/2019/12/23/Golang-GOROOT%E3%80%81GOPATH%E3%80%81Go-Modules-%E4%B8%89%E8%80%85%E7%9A%84%E9%97%9C%E4%BF%82%E4%BB%8B%E7%B4%B9/) # 使用 GoMod 來建立專案 傳統舊方式是設定 gopath, 來讓編輯器知道你程式碼檔案路徑 以方便編譯出執行檔 但這種方式每一台電腦都要設定一次 windows環境變數檔案 又分windows 和 linux 兩種, 會很瑣碎, 專案可攜性較差 ![](https://i.imgur.com/n6wcrBA.jpg) ![](https://i.imgur.com/TsA6EIw.jpg) 使用GoMod 來管理專案編譯路徑 建立好專案資料夾後 輸入 go mod init 專案名稱 ![](https://i.imgur.com/bQZZbcJ.jpg) 會出現兩個檔案 go.mod 和 go.sum 其中 go.mod 是我們要維護的【使用套件檔案】 (編譯器發現你有使用第三方套件, 會幫你自動添加套件github路徑) 實際運行後的套件檔案內容 ```go= module example require ( CommonLib v0.0.0 // 自製套件 github.com/cweill/gotests v1.5.3 // indirect github.com/go-sql-driver/mysql v1.4.1 github.com/gorilla/mux v1.7.3 github.com/mitchellh/mapstructure v1.1.2 golang.org/x/tools v0.0.0-20191210221141-98df12377212 // indirect ) replace CommonLib => ../CommonLib go 1.13 ``` [gomod教學](https://medium.com/@zamhuang/golang-%E9%82%84%E5%9C%A8%E6%8A%8A-library-%E6%94%BE%E5%9C%A8%E5%B0%88%E6%A1%88%E8%A3%A1-%E8%A9%B2%E8%B7%9F%E4%B8%8A%E4%BD%BF%E7%94%A8-go-module-%E4%BA%86-4185df23442a) 實際目錄配置 ![](https://i.imgur.com/l5GqnDO.jpg) ![](https://i.imgur.com/IVL7PXW.jpg) ![](https://i.imgur.com/LMBPXiR.jpg) ![](https://i.imgur.com/btHeTvx.jpg) ## 使用 go mod 後, import 路徑改為 輸入 go mod init 專案名稱 ![](https://i.imgur.com/6JwRZ1h.jpg) 由於套件名稱輸入 Example 所以 import 時的根目錄就從 Example開始 ![](https://i.imgur.com/cgnAniU.jpg) ![](https://i.imgur.com/iKunHUV.jpg) ![](https://i.imgur.com/d63M6Xn.jpg) ```go package main import ( "fmt" "time" "Example/src/ModelDefine" "Example/src/ModelWebapi" // webapi 範例 "Example/src/ModelWebsocket" // websocket 範例 ) func main() { // websocket 範例, 阻塞在這邊 ModelWebsocket.DemoWebsocket() } ``` 套件內容, 程式碼最上方 package ModelWebsocket 為規劃的套件名稱 ```go package ModelWebsocket import ( "log" "net/http" "time" //"time" "github.com/gorilla/websocket" ) // 收到封包的callbackfunc // 當有Client Connect時, 就會觸發此函式 ( 所以是多執行序並行處理 ) func RecvFunc(w http.ResponseWriter, r *http.Request) { // websocket 環境設定 設定緩衝區, 封包是否壓縮 upgrader := &websocket.Upgrader{ //如果有 cross domain 的需求,可加入這個,不檢查 cross domain CheckOrigin: func(r *http.Request) bool { return true }, ReadBufferSize: 10000, EnableCompression: true, } // 取得 websocketObj => ws ws, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("upgrade:", err) return } defer func() { log.Println("disconnect !!") // websocket 連線資源釋放 ws.Close() }() // TODO 將 websocketObj 儲存在動態陣列內(slice) // 無窮迴圈, 這個client 的 connection, 不間斷的收發資料 for { // 線上有發現即使close websocket, ReadMessage低機率不會return error // 每次將收封包前都設定閒置 2分鐘 timeout , 太久沒反應直接把client斷線 deadline := time.Now().Add(time.Duration(2) * time.Minute) ws.SetReadDeadline(deadline) // 阻塞在此, 直到 ReadMessage 有讀取到資料 mtype, msg, err := ws.ReadMessage() if err != nil { log.Println("接收資料錯誤, 有client斷線了 mtype:", mtype, " err=", err) // DOTO 如果有將 websocketObj儲存在slice內的話, 這邊要釋放client資源 break } // 列印收到的封包資料 通常是 json 字串 log.Printf("receive: %s\n", msg) // DOTO: 通常封包資料是Json字串, 所以需要 json to obj // DOTO: 根據obj 的封包資料, 來決定要呼叫哪個函式來處理 // DOTO: 打包封包回應資料 obj to json // 簡單的將收到的封包送出 (WriteMessage 本身不支援多執行序, 需要用lock, unlock包起來, 或使用channel jobqueue包起來) err = ws.WriteMessage(mtype, msg) if err != nil { log.Println("準備傳送資料 write:", err) break } } } func DemoWebsocket() { // 設定收到封包的 callbackfunc 完整的gameServer Url= ws://127.0.0.1:8899/echo http.HandleFunc("/echo", RecvFunc) log.Println("websocket server start at :8899") // 啟動 websocket 模組, 會阻塞在此, 監聽Port:8899 log.Fatal(http.ListenAndServe(":8899", nil)) } ``` # Go專案的目錄結構詳解 https://codertw.com/%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC/391937/ # 第一個程式 hello world ```go= package main import "fmt" func main() { fmt.Println("Hello world!") var b int = 123 // 正規 宣告+定義 a := 10 // 快速 整數快速初始化 str := "測試字串" // 快速 字串快速初始化 pi := 3.14 fmt.Printf("測試範例 %d, str=%s \r\n", a, str) fmt.Printf("b=%d, 指標=%p \r\n", b, &b) fmt.Printf("pi=%f pi=%v\r\n", pi, pi ) var c int64 = 456 // 有指定初始值 var d int64 // d = 789 fmt.Println("類似console.log c=", c, " d=", d) } ``` package main 代表是主程式進入點 會搭配 func main() 來告知編譯器起始執行的函式 import 類似 node.js 的 require, 用來引入第三方套件或是自製套件 寫程式要點 * 程式碼副檔名為 go * 沒使用到的import 要註解掉, 不然編譯會錯誤 (或編輯器偶爾會貼心幫你刪除此行) * 沒使用到的變數, 要註解掉, 不然編譯會錯誤 declared and not used * 函式的 { } 格式和位置有固定, 亂排版會編譯錯誤 missing function body * if else, 或 switch 的 { } 格式也有固定, 亂排版會編譯錯誤 syntax error: unexpected newline, expecting { after if clause * 程式碼結尾不用加分號 ; [撰寫第一個 Go 程式詳細說明](https://michaelchen.tech/golang-programming/write-first-program/#go-%E7%A8%8B%E5%BC%8F%E7%9A%84%E7%B5%84%E6%88%90) # 執行專案方式 * vscode 直接對著主程式碼, 按下F5 * 或進入命令提示字元, 切到專案目錄下執行 go build exapmle.go 會輸出執行檔案 * 或進入命令提示字元, 切到專案目錄下執行 go build 會根據套件名稱建立執行檔案 * go clean 會清除編譯相關檔案 ```javascript= { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Launch", "type": "go", "request": "launch", "mode": "auto", "program": "${workspaceRoot}/example.go", "env": {}, "args": [] } ] } ``` "program": 是告訴vscode編輯器, 預設執行的程式碼路徑 上面範例是強制使用 example.go 當成第一個執行的檔案 預設值為 "program": "${fileDirname}", 代表是成為視窗焦點的程式碼會被執行 詳細解釋各參數說明 [使用VSCode 調試Golang](https://gocn.vip/topics/9922) # 基本資料型態 基本型態 | 資料型態 | 大小 | 範圍 | 說明 | | -------- |:---------- |:----------- |:-------------------- | | uint | 取決於平台 | 取決於平台 | 無號整數 | | uint8 | 8 bits | 0 到 255 | 無號整數 | | uint16 | 16 bits | 0 到 216 -1 | 無號整數 | | uint32 | 32 bits | 0 到 232 -1 | 無號整數 | | uint64 | 64 bits | 0 到 264 -1 | 無號整數 | | int | 取決於平台 | 取決於平台 | 有號整數 | | int8 | | | 有號整數 | | int16 | | | 有號整數 | | int32 | | | 有號整數 | | int64 | | | 有號整數 | | flaot32 | 32 bits | | 浮點數 | | flaot64 | 64 bits | | 浮點數 | | string | | | 字串 | | bool | | | 布林值 true 或 false | 儲存結構 | 資料型態 | 說明 | | -------- | -------- | | Array和Slice | 陣列和切片 | | Map | 映射或字典, key 和 value | | struct | 結構 | 非同步使用 | 資料型態 | 說明 | | -------- | -------- | | channel | 通道, 工作佇列或訊息佇列 | | sync.Mutex | 執行序鎖 lock unlock | # 區域變數和全域變數 ## 全域變數(global variable) 在函式體外宣告的變數稱之為全域性變數,全域性變數可以在整個包甚至外部包(被匯出後)使用。 全域性變數可以在任何函式中使用 ## 區域變數(local variable) * 函式內的變數, 都算區域變數, 離開此函式時, 記憶體會被釋放掉, 也不能再使用此變數 * 括號內的變數, 只存在該括號內 * 全域性變數與區域性變數名稱可以相同,但是函式內的區域性變數會被優先考慮。(意思是撞名時候, 會優先使用區域變數) ```go package main import "fmt" /* 宣告全域性變數 */ var a int = 20; func main() { /* main 函式中宣告區域性變數 */ var a int = 10 var b int = 20 var c int = 0 fmt.Printf("main()函式中 a = %d\n", a) c = sum( a, b) fmt.Printf("main()函式中 c = %d\n", c) e = delete( a, b) fmt.Printf("main()函式中 e = %d\n", e) } /* 函式定義-兩數相減 */ func delete(a int, b int) (int) { fmt.Printf("delete() 函式中 a = %d\n", a) fmt.Printf("delete() 函式中 b = %d\n", b) return a - b } /* 函式定義-兩數相加 */ func sum(a, b int) (ret int) { fmt.Printf("sum() 函式中 a = %d\n", a) fmt.Printf("sum() 函式中 b = %d\n", b) ret = a + b return } ``` [Go 語言變數作用域](https://www.itread01.com/content/1543079463.html) # if 判斷 ```go= package main import ( "fmt" ) func main() { var b int = 123 a := 10 // 兩個數值比較 if a >= b { fmt.Printf("%d >= %d \r\n", a, b) } else { fmt.Printf("%d < %d \r\n", a, b) } } ``` # switch 和 列舉 ``` go package main import ( "fmt" ) type NET_STATUS int // 列舉 const ( NET_STATUS_UNKNOW int = iota // 預設值 0 NET_STATUS_IDLE // 閒置狀態 1 NET_STATUS_CONNECT // 連線中 2 NET_STATUS_LOBBY // 大廳中 3 NET_STATUS_GAME // 遊戲中 4 ) // 多參數回傳值 /* * @param status 傳入網路狀態 * @return result 狀態判斷是否成功旗標 * @return message 回傳處理的狀態字串 */ func NetStatusCheck(status int) (result bool, message string) { // 判斷網路狀態 switch status { case NET_STATUS_IDLE: result = true message = fmt.Sprintf("閒置狀態 status=%d", status) case NET_STATUS_CONNECT: result = true message = fmt.Sprintf("連線中 status=%d", status) case NET_STATUS_LOBBY: result = true message = fmt.Sprintf("大廳中 status=%d", status) case NET_STATUS_GAME: result = true message = fmt.Sprintf("遊戲中 status=%d", status) default: result = false message = fmt.Sprintf("未知狀態 status=%d", status) } // 因為函式 【明確】 宣告兩個回傳值, 所以return 時, 可以簡寫 return } // 單一參數回傳值 /* * @param a 兩數相加的參數1 * @param b 兩數相加的參數2 * @return 回傳處理的相加結果 */ func Add( a int , b int) ( int ){ value := a + b return value } func main() { // 呼叫 NetStatusCheck 函式, 並且 result, msg := NetStatusCheck(NET_STATUS_IDLE) if result == true { println("判斷1 msg=", msg) } // 上面已經使用 := 創造變數了, 所以可以繼續使用 result, msg = NetStatusCheck(NET_STATUS_LOBBY) if result == true { println("判斷2 msg=", msg) } // 使用 _ 來忽略不想處理的回傳參數 _, msg = NetStatusCheck(NET_STATUS_UNKNOW) println("判斷3 msg=", msg) } ``` # 指標 待補 # map (映射) 使用 key 和 value 來儲存資料的結構 * Map 是一种无序的键值对的集合 ( 所以使用 for 迴圈掃描,數值順序每次會不一樣 ) * 底層是使用 hash 雜湊表来实现 (一個超大陣列) * 實作將 key 利用雜湊函數算出索引值1, 就儲存在在陣列[1]內 * 雜湊碰撞時,使用串鍊 一樣儲存在同樣的索引內,只是串列串起來 [雜湊表 Hash Map](https://medium.com/@ralph-tech/%E8%B3%87%E6%96%99%E7%B5%90%E6%A7%8B%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-%E9%9B%9C%E6%B9%8A%E8%A1%A8-hash-table-15f490f8ede6) * 建立一個固定陣列 * 將要計算的數值,透過雜湊函式算出索引值, 如果得到索引1, 就儲存在陣列1的空間 (假設key=Frodo value=88-7-666) * 如此推導下去 * 如果要獲取資料, key=Frodo, 再透過同樣的雜湊函式也會得到索引1, 就取得原先設定的數值了 ![](https://i.imgur.com/B3W65Wd.png) ![](https://i.imgur.com/W9iL586.png) * 長時間使用 map時候 會造成記憶體一直增長壓不下來 (原因mpa只會擴容,不會縮容) * 解決方案1 定時冷門時段更換配置一個新map m = make(map[string]int) * 解決方案2 刪除map後使用 gc 強制釋放 * 解決方案3 value使用物件指標 map[int]*obj ( 刪除時要先 map[key] = nil, 在 delete(map,key), 這樣gc才會釋放到物件 ) ```go package main import ( "fmt" ) // 列印map func PrintfMap(m map[string]int) { // 使用range 尋訪map fmt.Println("使用range 尋訪map 長度=", len(m) ) // 使用 range 搜尋陣列 (注意 他雖然可以每一個陣列都搜尋, 但不保證順序 ) // 在意順序, 儲存結構就使用 slice for key, value := range m { fmt.Println(fmt.Sprintf("key=%v, value=%v", key, value)) } } func main() { // map 初始化 (要使用make) m := make(map[string]int) // var m map[string]int 錯誤的使用方式, 沒有使用make配置記憶體, 會導致程式panic噴掉 // 設定值 m["test"] = 1 m["dog"] = 10 m["cat"] = 111 m["monkey"] = 222 fmt.Printf("m=%v \r\n", m) // 取得值1 value := m["cat"] fmt.Printf("取得值1 value=%v\r\n", value) // 取得值2 value, ret := m["dog"] if ret == true { fmt.Printf("取得值2 value=%v, ret=%v \r\n", value, ret) } // 列印map PrintfMap(m) // 刪除map某一個key => delete // 刪除時其實不會釋放記憶體空間, 他還是存在, 如果真的要釋放map, 就要把 m = nil delete(m, "dog") // 列印map PrintfMap(m) //釋放全部記憶體 m = nil } ``` # array 陣列(靜態陣列) 類似傳統的靜態固定大小的陣列 使用 len 來判斷陣列大小 初始化時有給長度數值, 就是陣列, 沒給數值會歸類在 slice ```go package main import ( "log" ) func main() { var a [5]int //宣告一個大小5的 int array, 陣列初始值為0 var b [4]int = [4]int{10, 20, 30, 40} //宣告一個大小4的 int array, 陣列初始值為 10, 20, 30, 40 c := [3]int{111, 222, 333} //宣告一個大小3的 int array, 並給初始值 111, 222, 333 // 尋訪陣列1 for i := 0; i < len(a); i++ { value := a[i] log.Println("尋訪陣列 a 值=", value) } // 尋訪slice for i := range b { value := b[i] log.Println("尋訪陣列 b 值=", value) } log.Println("a=", a) log.Println("b=", b) log.Println("c=", c) } ``` # slice 切片 (動態陣列) slice切出來的陣列其實都指向同一個陣列 所以操作時候要特別小心避免改到原始陣列 如果想要新增一筆資料在陣列尾端, 就使用append 如果說想要複製一個新的陣列, 就使用 copy 來拷貝 * 使用 slice 方式很像 array ,但實際上 slice 是一個 descriptor struct ,其中會有一個 array pointer 指向真正存放數值的 array 中 * 初始化時沒給長度數值會歸類在 slice * slice 由三個東西組成。 陣列指標 + length(實際資料長度) + cap(陣列最大長度) * 切出來的slice, 一開始指向原始陣列 list, 此時修改哪一個, 都會直接修改到 list * 當一直不斷對切出來的slice變數做 append時, **如果 slice 的 len 超過 cap, 會觸發自動擴充cap*2的長度**, 其記憶體會重新分配且已經視為不同的slice陣列了 * 當擴容時候,如果capacity小於256時候,一次會擴充2倍,當超過capacity時候,一次會擴充1.25倍 * 針對一個為空的slice做切片, 會引發panic * 當函式參數傳遞slice時候, 有傳記憶體位址進去, 才能改同一份slice資料, 不然會視為兩個slice。 len() 和 cap() 都是 slice在使用的函式 slice 底層結構 ![](https://i.imgur.com/bRyT6aH.png) 完整範例 ```go package main import "fmt" func main() { var TraditionArray [10]int // 傳統陣列只有 length 沒有 cap var list []int = make([]int, 5) // slice 有 length 有 cap, 當length超過cap, 則會自動擴充cap*2的長度 => 資料型態為int, 配置空間為5的陣列 var list2 []int = make([]int, 4) // 有make給值 len=4 cap=4 [0,0,0,0] var list3 []int = make([]int, 20) // 有make給值 var list4 []int // 未初始化 len=0 cap=0 nil // 依序寫入資料到slice內 for i := 0; i < 10; i++ { list = append(list, i+1) list2 = append(list2, i+10) } // 陣列[6] 到 陣列[9-1] sliceList := list[6:9] // 陣列[4] 到底 sliceList2 := list[4:] // 陣列[0] 到 陣列[6-1] sliceList3 := list[0:6] // 冒號左邊沒寫數字代表預設0 => 陣列[0] 到 陣列[6-1] sliceList4 := list[:6] // copy slice ( 若目的陣列 空間過小, 則copy時, 以目的陣列大小為主, 所以通常copy前, 目的陣列會配置跟來源陣列一樣大 ) var list5 []int = make([]int, len(list2)-5) copy(list5, list2) // 一次append 整個 list2, 但從 list[10] 到最後 list5 = append(list5, list2[10:]...) //因為append時, 當容量滿會自動擴充, 且記憶體會重新分配, 所以要指向自己 list5 = append() fmt.Printf("TraditionArray(傳統陣列, 基本預設值就都是0了)=%v \r\n", TraditionArray) fmt.Printf("list=%v \r\n", list) fmt.Printf("list2=%v \r\n", list2) fmt.Printf("list3=%v \r\n", list3) fmt.Printf("list4(沒有make初始化, 所以是nil)=%v \r\n", list4) fmt.Printf("深度 copy slice list5=%v \r\n", list5) // 視為兩個不同的記憶體空間的slice //列印切片後的slice資料 fmt.Printf("sliceList=%v \r\n", sliceList) fmt.Printf("sliceList2=%v \r\n", sliceList2) fmt.Printf("sliceList3=%v \r\n", sliceList3) fmt.Printf("sliceList4=%v \r\n", sliceList4) // 使用 range 迭代器 搜尋陣列, data是複製list[n]內的變數, 所以記憶體不是同一份, 修改data, 不會影響到list5的資料 for i, data := range list5 { fmt.Printf("使用range搜尋法 i=%d, data(指標)=%p, data(內容)=%v \r\n", i, &data, data) } // 傳統陣列搜尋法 for i := 0; i < len(list5); i++ { //取得陣列的記憶體位置 pData := &list5[i] fmt.Printf("傳統陣列搜尋法 i=%d, data(指標)=%p, data(內容)=%v \r\n", i, pData, *pData) *pData = i * 100 //修改內容方式1 list5[i] = i * 200 //修改內容方式2 } } ``` //執行結果 ```go= 已清除主控台 TraditionArray(傳統陣列, 基本預設值就都是0了)=[0 0 0 0 0 0 0 0 0 0] list=[0 0 0 0 0 1 2 3 4 5 6 7 8 9 10] list2=[0 0 0 0 10 11 12 13 14 15 16 17 18 19] list3=[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] list4(沒有make初始化, 所以是nil)=[] 深度 copy slice list5=[0 0 0 0 10 11 12 13 14 16 17 18 19] sliceList=[2 3 4] sliceList2=[0 1 2 3 4 5 6 7 8 9 10] sliceList3=[0 0 0 0 0 1] sliceList4=[0 0 0 0 0 1] 使用range搜尋法 i=0, data(指標)=0xc000013c08, data(內容)=0 使用range搜尋法 i=1, data(指標)=0xc000013c08, data(內容)=0 使用range搜尋法 i=2, data(指標)=0xc000013c08, data(內容)=0 使用range搜尋法 i=3, data(指標)=0xc000013c08, data(內容)=0 使用range搜尋法 i=4, data(指標)=0xc000013c08, data(內容)=10 使用range搜尋法 i=5, data(指標)=0xc000013c08, data(內容)=11 使用range搜尋法 i=6, data(指標)=0xc000013c08, data(內容)=12 使用range搜尋法 i=7, data(指標)=0xc000013c08, data(內容)=13 使用range搜尋法 i=8, data(指標)=0xc000013c08, data(內容)=14 使用range搜尋法 i=9, data(指標)=0xc000013c08, data(內容)=16 使用range搜尋法 i=10, data(指標)=0xc000013c08, data(內容)=17 使用range搜尋法 i=11, data(指標)=0xc000013c08, data(內容)=18 使用range搜尋法 i=12, data(指標)=0xc000013c08, data(內容)=19 傳統陣列搜尋法 i=0, data(指標)=0xc0000c61b0, data(內容)=0 傳統陣列搜尋法 i=1, data(指標)=0xc0000c61b8, data(內容)=0 傳統陣列搜尋法 i=2, data(指標)=0xc0000c61c0, data(內容)=0 傳統陣列搜尋法 i=3, data(指標)=0xc0000c61c8, data(內容)=0 傳統陣列搜尋法 i=4, data(指標)=0xc0000c61d0, data(內容)=10 傳統陣列搜尋法 i=5, data(指標)=0xc0000c61d8, data(內容)=11 傳統陣列搜尋法 i=6, data(指標)=0xc0000c61e0, data(內容)=12 傳統陣列搜尋法 i=7, data(指標)=0xc0000c61e8, data(內容)=13 傳統陣列搜尋法 i=8, data(指標)=0xc0000c61f0, data(內容)=14 傳統陣列搜尋法 i=9, data(指標)=0xc0000c61f8, data(內容)=16 傳統陣列搜尋法 i=10, data(指標)=0xc0000c6200, data(內容)=17 傳統陣列搜尋法 i=11, data(指標)=0xc0000c6208, data(內容)=18 傳統陣列搜尋法 i=12, data(指標)=0xc0000c6210, data(內容)=19 ``` ### 備註 slice 在 copy時, 沒辦法處理 物件內slice的copy 要自己手動個別copy 在使用 = 和 copy 時, 要注意其影響範圍 然後slice 在append 後, 只要資料容量超過cap時, 會自動擴充兩倍 [Golang Slice 介紹](https://calvertyang.github.io/2019/11/13/introduction-to-slices-in-golang/) # 函式 將重複性的功能, 製作成一個函式, 方便重複利用 ```go package main import "fmt" // 相加函式 func Add(x int, y int) (value int) { value = x + y return } // 檢查函式 func Check(value int) bool { var ret bool if value >= 100 { ret = true // value >= 100 } else { ret = false // value小於100 } return ret } // 產生放大一萬倍的點數值 func MakeMoney(value int) (result bool, money int) { if value > 0 { money = value * 10000 result = true } else { money = -1 result = false } return } func main() { fmt.Println("Hello world!") value := Add(1, 2) fmt.Println("相加結果=", value) ret := Check(value) fmt.Println("相減結果=", ret) result, money := MakeMoney(value) fmt.Printf("檢查結果 result=%v, money=%d \r\n", result, money) } ``` # defer 1. 註冊離開此function(函式)時, 要執行的東西 2. 使用時機 例如有開 fopen(開檔案)與fclose(關閉檔案) 3. 變數的傳遞,會在 **呼叫defer的時候傳入**, 所以他並不是很簡單的直接移到最後呼叫 4. defer時, 會將要執行的函式指標push塞入Queue內, 離開時會依序pop出來執行函式指標(先進後出) ``` go // You can edit this code! // Click here and start typing. package main import "fmt" func main() { // 注意執行順序 (因為放在堆疊內, 所以是先進後出) defer fmt.Println("Hello, 世界1") defer fmt.Println("Hello, 世界2") defer fmt.Println("Hello, 世界3") } ``` ```go // You can edit this code! // Click here and start typing. package main import "fmt" func MyPrint(step string, value int) { // 注意列印的數值 fmt.Printf("MyPrint step=%s, value=%d \r\n", step, value) } func main() { //變數的傳遞,會在 **呼叫defer的時候傳入**, 所以他並不是很簡單的直接移到最後呼叫 fmt.Println("觀察傳入參數 aaaaaa") var a int = 10 defer MyPrint("第1次", a) fmt.Println("觀察傳入參數 bbbbbb") a++ defer MyPrint("第2次", a) fmt.Println("觀察傳入參數 ccccc") a++ defer MyPrint("第3次", a) fmt.Println("觀察傳入參數 ddddd") } ``` **輸出結果** ```go= 觀察傳入參數 aaaaaa 觀察傳入參數 bbbbbb 觀察傳入參數 ccccc 觀察傳入參數 ddddd MyPrint step=第3次, value=12 MyPrint step=第2次, value=11 MyPrint step=第1次, value=10 ``` [教學-[Golang] 相當好用但又要注意的defer](https://www.evanlin.com/golang-know-using-defer/) # 攔截例外 避免程式崩潰 ```go= package main import ( "fmt" "runtime" ) //列印發生例外時執行的函式 func PrintStack() (str string) { var buf [81920]byte n := runtime.Stack(buf[:], false) str = string(buf[:n]) return str } func tryCatchTest() { fmt.Printf("進入函式=== \r\n") defer func() { err := recover() if err != nil { detailStr := PrintStack() fmt.Printf("err=%v, detailStr =%s \r\n", err, detailStr ) } }() // 故意引發例外 trycatch var i *int //沒有配置記憶體的 int 指標 *i = 1234 //準備引發例外 //panic(1) fmt.Printf("離開函式=== \r\n") } func main() { tryCatchTest() } ``` # 物件導向 * golang的物件是閹割版 * 只有組合 ( c++ 的 has-a ) 和 覆蓋(override) * 沒有 過載(overload ) * 小寫是私有成員, 只能同一個package內存取 * 大寫是公有成員, 可以對外開放使用 **覆蓋(override)** 是指父類別和子類別都有同樣函式 如果 子類別繼承父類別, 執行有被覆蓋的函式時 會以子類別的為主 ```go= type Point struct { x float64 y float64 } // 產生一個物件 func NewPoint(xParm float64, yParm float64) *Point { //物件初始化1 使用new來配置一個 Point 物件, 會回傳記 Point 物件 的憶體位址 p := new(Point) p.SetX(xParm) p.SetY(yParm) return p } func (p *Point) SetX(x float64) { p.x = x log.Println("父類別 SetX=", x) } func (p *Point) SetY(y float64) { p.y = y log.Println("父類別 SetY=", y) } type Point2 struct { Point } // 產生一個物件 func NewPoint2(xParm float64, yParm float64) *Point2 { //物件初始化1 使用new來配置一個 Point 物件, 會回傳記 Point 物件 的憶體位址 p := new(Point2) p.SetX(xParm) p.SetY(yParm) return p } // override 父類別的函式 func (p *Point2) SetY(y float64) { p.y = y log.Println("子類別 SetY=", y) } func TestObj() { p := NewPoint(1, 2) log.Println("p=", p) p2 := NewPoint2(1, 2) log.Println("p2=", p2) } ``` **過載(overload )( golang不支援 )** 是指同樣函式名稱, 但不同參數 ```go= func (p *Point) SetX(x float64) { } func (p *Point) SetX(x float64, x2 float64) { } ``` ```go package main import "fmt" // 列舉 const ( GameType_Poker int = iota // 撲克牌 GameType_Slot // 老虎機 GameType_Fish // 魚機 ) // 父類別 type GameBase struct { GameId int GameName string } // 衍生類別 type GameInfo struct { GameBase // (大寫) 組合 ( c++ 的 has-a ) 公有物件 Money float32 // (大寫) 衍生類別的 public 公有變數 gameType int // (小寫) 衍生類別的 private 私有變數 } // 建立一個遊戲物件 func NewGameInfo(gameId int, gameName string, gameType int) (pGame *GameInfo) { //GameInfo指標指派給pGame, 也可使用 new pGame = &GameInfo{ // 父類別初始化 GameBase: GameBase{ GameId: gameId, GameName: gameName, }, // 衍生類別初始化(直接使用不用多一層) Money: 0.0, gameType: gameType, } return } // GameInfo的函式 設定 gameType 注意 *符號, 代表是修改記憶體的內容 func (pGame *GameInfo) SetGameType(gameType int) { pGame.gameType = gameType } // GameInfo的函式 取得 gameType func (game GameInfo) GetGameType() (gameType int) { gameType = game.gameType return } // GameInfo的函式 設定 money (注意 * 符號, 可以當作this指標, 可以修改物件成員變數) func (pGame *GameInfo) SetMoney(money float32) { pGame.Money = money } // GameInfo的函式 取得 Money func (pGame *GameInfo) GetMoney() float32 { return pGame.Money } func main() { // 建立一個物件 gameFish := NewGameInfo(1001, "蟲蟲危機", GameType_Fish) fmt.Printf("GameId=%v, GameName=%v, GameType=%v \r\n", gameFish.GameId, gameFish.GameName, gameFish.GetGameType()) gameFish.SetMoney(123.456) fmt.Printf("money=%f \r\n", gameFish.GetMoney()) } ``` is-a 父類別:man 子類別:superMan, 超人繼承人類物件 has-a 組合 公司裡面有員工 有電腦 # interface 介面 * 用來搭配工廠模式使用 * 用來當萬用變數使用, 任何變數都可以指向 interface{}, 最後需要時再轉型回原本資料型態即可 * 新增 interface 的方法函式時, 所有的子類別都要額外實做寫一份,不然會編譯錯誤 [interface教學](https://michaelchen.tech/golang-programming/interface/) 完整範例 ```go package main import "fmt" type GameId int // 遊戲列舉 const ( GameId_Unknow GameId = iota GameId_Dodizu // 鬥地主 GameId_Big2 // 大老二 GameId_Slot // 老虎機 GameId_Fish // 魚機 ) // 定義介面 interface, 所有的遊戲都要實作相關函式 type IGame interface { Init() // 遊戲初始化 Play() // 遊玩 // Process() // 遊戲邏輯 End() // 離開 } // 鬥地主遊戲 type Dodizu struct { Name string GameId GameId } // 產生鬥地主控制器 func NewDodizu(name string, gameId GameId) *Dodizu { // 方法1 pDodizu := &Dodizu{ Name: name, GameId: gameId, } // 方法2 pDodizu2 := new(Dodizu) pDodizu2.Name = name pDodizu2.GameId = gameId return pDodizu } // 每一個物件實作 interface 內的函式 func (pDodizu *Dodizu) Init() { fmt.Println("Dodizu Init") } func (pDodizu *Dodizu) Play() { fmt.Println("Dodizu Play") } func (pDodizu *Dodizu) End() { fmt.Println("Dodizu End") } type Big2 struct { Name string GameId GameId } // 產生一個大老二物件 func NewBig2(name string, gameId GameId) *Big2 { pBig2 := &Big2{ Name: name, GameId: gameId, } return pBig2 } // 每一個物件實作 interface 內的函示 func (pBig2 *Big2) Init() { fmt.Println("Big2 Init") } func (pBig2 *Big2) Play() { fmt.Println("Big2 Play") } func (pBig2 *Big2) End() { fmt.Println("Big2 End") } // 工廠模式, 根據不同的GameId, 產生不同的 遊戲邏輯層物件 // 回傳時, 轉型成 IGame 介面, 如果遊戲物件沒有實作介面, 編譯會錯誤 func FactoryNewGame(gameName string, gameId GameId) IGame { switch gameId { case GameId_Dodizu: return NewDodizu(gameName, gameId) case GameId_Big2: return NewBig2(gameName, gameId) default: panic(fmt.Sprintf("Unknown gameName=%s, GameId=%d", gameName, gameId)) } } // 將 interface 當參數傳進去, 會根據不同遊戲物件, 呼叫到相對應的函式 func Common_Init(iGame IGame) { iGame.Init() // 介面直接呼叫 Init } func Common_Play(iGame IGame) { iGame.Play() // 介面直接呼叫 Play } func Common_End(iGame IGame) { iGame.End() // 介面直接呼叫 End } func main() { fmt.Println("start Demo2 (簡易工廠模式 + 介面實作)") // 介面 var GameList []IGame var GameListTmp IGame fmt.Printf("%v \r\n", GameListTmp) // 產生鬥地主和大老二遊戲 gameDodizu := FactoryNewGame("鬥地主", GameId_Dodizu) gameBig2 := FactoryNewGame("大老二", GameId_Big2) // 使用方式1 將物件塞入 Interface列表內, 統一執行 ========================== // 加入 GameList列表 [] 或是在 Table 物件內, 宣告一個 GameLogic IGame 變數, 在 GameLogic = gameDodizu 之後, 就可以直接使用 GameLogic.Init() ] GameList = append(GameList, gameDodizu) GameList = append(GameList, gameBig2) // 因為放在動態陣列內, 所以依序掃秒陣列值 for _, game := range GameList { // 呼叫遊戲邏輯層的init game.Init() // 呼叫遊戲邏輯層的Play game.Play() // 呼叫遊戲邏輯層的End game.End() } // 其實直接 new 出物件後, 他會會傳介面, 就可以直接對介面做操作了 newGame := FactoryNewGame("新大老二", GameId_Big2) newGame.Init() newGame.Play() newGame.End() newGame2 := FactoryNewGame("新鬥地主", GameId_Dodizu) newGame2.Init() newGame2.Play() newGame2.End() // 使用方式2 將物件塞入 Common_XXX函式內, 根據不同物件執行相對應函式 ========================== // 各遊戲初始化 Common_Init(newGame) Common_Init(newGame2) // 各遊戲執行 Common_Play(newGame) Common_Play(newGame2) // 各遊戲釋放 Common_End(newGame) Common_End(newGame2) fmt.Println("end Demo2") } ``` # goroutine (執行緒) * 非同步處理使用 * 輕量化的goroutine * 執行所佔用的資源成本比C++的 thread 少 * 同一個goroutine內, 重複 寫鎖lock, 會造成死鎖 * 同一個goroutine內, 重複 鎖unlock, 會造成panic thread 和 goroutine 比較 ![image](https://hackmd.io/_uploads/H1pWCLnF1x.png) **資料競爭原理** cpu1獲取變數a,準備加1時(還沒更新完整個暫存器的變數),cpu2也獲取了變數a, 也同樣準備加1, 導致兩邊都是拿到 a=30 開始加1。 ![](https://i.imgur.com/4b8VPab.png) [淺談Golang 中的 Data Race Condition(資料競爭)](https://larrylu.blog/race-condition-in-golang-c49a6e242259) [理解資料庫『悲觀鎖』和『樂觀鎖』的觀念](https://medium.com/dean-lin/%E7%9C%9F%E6%AD%A3%E7%90%86%E8%A7%A3%E8%B3%87%E6%96%99%E5%BA%AB%E7%9A%84%E6%82%B2%E8%A7%80%E9%8E%96-vs-%E6%A8%82%E8%A7%80%E9%8E%96-2cabb858726d) * 悲觀鎖: 一次只能一個 sql command 操作資料,其他 command 無法在資料正在被操作時異動,只能排隊等 * 優點: 因為使用前會先鎖住佔有,可以確保資料正確性 * 缺點: 因為需要等前面的解鎖後才能繼續使用,要排隊異動,因此處理時間拉長 * 樂觀鎖: 允許多個 sql command 同時操作資料 (類似大家一開始都用 rlock獲取資料 ) * 優點: 可以快速的完成異動 * 缺點: 容易造成資料出現不合預期的結果 ```go= Golang中的樂觀鎖與悲觀鎖 [sync/atomic](https://golang.google.cn/pkg/sync/atomic/) Golang中有一個atomic包,可以在不形成臨界區和創建互斥量的情況下完成並發安全的值替換操作,這個包應用的便是樂觀鎖的原理。 不過這個包只支持int32/int64/uint32/uint64/uintptr這幾種數據類型的一些基礎操作(增減、交換、載入、存儲等) [sync](https://golang.google.cn/pkg/sync/) Golang中的sync包,提供了各種鎖,如果使用了這個包,基本上就以悲觀鎖的工作模式了。 [Go超时锁的设计和实现](https://www.jianshu.com/p/4d85661fba0a) ``` ```go= // You can edit this code! // Click here and start typing. package main import ( "fmt" "sync" "time" ) var DataMutex sync.Mutex // 互斥鎖 var Value int // 全域同步資料 // 加法 func Add() { DataMutex.Lock() // 上鎖 defer DataMutex.Unlock() // 解鎖, 用defer包起來, 是為了要確保一定會執行到 unlock // 同步資料運算 Value++ } // 減法 func Sub() { DataMutex.Lock() // 上鎖 defer DataMutex.Unlock() // 解鎖, 用defer包起來, 是為了要確保一定會執行到 unlock // 同步資料運算 Value-- } // 處理執行序1 func Process1(jobName string) { fmt.Println("Process1 開始 jobName=", jobName, " Value=", Value) ProcessCount := 0 for { // 離開 goroutine 的方法 ProcessCount++ if ProcessCount > 30 { fmt.Println("Process1 離開迴圈 jobName=", jobName) break } // 工作1 執行累加 Add() //睡 1ms time.Sleep(time.Millisecond) // 死命工作, 類似 c語言的sleep(0) //runtime.Gosched() } fmt.Println("Process1 離開 jobName=", jobName) } // 處理執行序2 func Process2(jobName string) { fmt.Println("Process2 開始 jobName=", jobName, " Value=", Value) ProcessCount2 := 0 for { // 離開 goroutine 的方法 ProcessCount2++ if ProcessCount2 > 40 { fmt.Println("Process2 離開迴圈 jobName=", jobName) break } // 工作2, 執行遞減 Sub() time.Sleep(time.Millisecond) } fmt.Println("Process2 離開 jobName=", jobName) } func main() { fmt.Println("DemoGoroutine 開始") go Process1("啟動goroutine1") // 執行30次 add go Process2("啟動goroutine2") // 執行40次 sub // 等待10秒 time.Sleep(time.Second * 10) fmt.Println("DemoGoroutine 結果 Value=", Value) } ``` # RWMutex(讀寫鎖) 1. 讀寫鎖的讀鎖可以重入,在已經有讀鎖的情況下,可以任意加讀鎖。 2. 在讀鎖沒有全部解鎖的情況下,寫操作會阻塞直到所有讀鎖解鎖。 3. 寫鎖定的情況下,其他協程的讀寫都會被阻塞,直到寫鎖解鎖。 Go語言的讀寫鎖方法主要有下面這種 1. Lock/Unlock:針對寫操作。 不管鎖是被reader還是writer持有,這個Lock方法會一直阻塞,Unlock用來釋放鎖的方法 2. RLock/RUnlock:針對讀操作 當鎖被reader所有的時候,RLock會直接返回,當鎖已經被writer所有,RLock會一直阻塞,直到能獲取鎖,否則就直接返回,RUnlock用來釋放鎖的方法 [Go語言中的互斥鎖和讀寫鎖(Mutex和RWMutex)](https://iter01.com/549872.html) [讀寫鎖和互斥鎖的性能比較](https://geektutu.com/post/hpg-mutex.html) # channel 通道 * 用來當作工作佇列(JobQueue)或是訊息佇列用(MessageQueue) * 通道是先進先出的一個佇列Queue * 有分單向或雙向佇列 * 已經關閉的通道,只能讀取不能寫入,一旦寫入會引發panic * 不能重複關閉通道 **基本單一通道原理: 接收者跟傳送者都要準備好, 才能開始收發資料, 不然會阻塞卡在此處** ![](https://i.imgur.com/tgYzeWx.jpg) * 有緩衝區的通道, 則可以一直塞資料, 直到通道滿時才會阻塞(寫入101筆時) var iQueue chan int = make(chan int, 100) * 如果建立一個0的通道, 會無法寫入資料, 也無法讀取資料(會阻塞, 編譯時會有警告) var iQueue chan int = make(chan int, 0) * 關閉channel => 使用 close(channelName) **重複關閉channel, 會導致panic** 建立通道 ```go // 建立一個字元通道 var c chan string = make(chan string) // 建立一個整數值通道 var i chan int = make(chan int) fmt.Println("i=", i) // 建立一個整數值有緩存100的通道 ( 代表資料塞到101筆時 會阻塞住 ) var iQueue chan int = make(chan int, 100) fmt.Printf("iQueue=%v, len=%d, cap(長度)=%d \r\n", iQueue, len(iQueue), cap(iQueue)) ``` 接收通道內的資料, 將資料往通道內送 ```go // 建立一個通道 make(chan string) // 通道名稱為 c // 通道內的資料為 string var c chan string = make(chan string) // 開啟一個goroutine, 來接收資料 go func() { // 看箭頭方向, 箭頭從通道方向出來, 代表從通道內讀取資料 message := <-c fmt.Printf("接收到通道內的資料 message=%s \r\n", message) }() fmt.Println("通道範例 執行開始") // 看箭頭方向, 箭頭往通道, 代表塞資料進通道內 c <- "卡住" fmt.Println("通道範例 睡眠中...") time.Sleep(time.Minute * 1) fmt.Println("通道範例 執行完畢") ``` 完整範例 ```go package main import ( "fmt" "time" ) // 傳送方 func pinger(c chan string) { fmt.Println("Run pinger....") // 無窮迴圈 for i := 0; ; i++ { msgData := fmt.Sprintf("ping i=%d", i) // 將字串資料往通道內塞 // 由於是單一通道, 若另一端沒準備好, 則會阻塞在此 c <- msgData } } // 接收方 func printer(c chan string) { fmt.Println("Run printer....") for { // 從通道內接收資料 (資料型態是string) // 由於是單一通道, 若另一端沒準備好, 則會阻塞在此 msgData := <-c fmt.Println(msgData) time.Sleep(time.Second * 1) } } func main() { // 建立一個通道 make(chan string) // 通道名稱為 c // 通道內的資料為 string var c chan string = make(chan string) // 呼叫 goroutine 將通道c, 當參數傳遞進去 go pinger(c) // 傳送方: 不斷的ping, 並且將資料送到 通道內 go printer(c) // 接收方: 不斷的收資料 // 睡10秒, 等待 goroutine 處理完畢 time.Sleep(time.Second * 10) //var input string //fmt.Scanln(&input) } ``` [Golang 教學系列 - 何謂 Channel? 先從宣告 Channel 開始學起](https://blog.kennycoder.io/2020/12/23/Golang%E6%95%99%E5%AD%B8%E7%B3%BB%E5%88%97-%E4%BD%95%E8%AC%82Channel-%E5%85%88%E5%BE%9E%E5%AE%A3%E5%91%8AChannel%E9%96%8B%E5%A7%8B%E5%AD%B8%E8%B5%B7/) [Go 語言使用 Select 四大用法](https://blog.wu-boy.com/2019/11/four-tips-with-select-in-golang/) [[Golang]關於 Channels 的控制一些要注意的事項(一)](https://www.evanlin.com/go-channels-handle/) # context **同時取消某個api請求產生的一堆goroutine** **goroutine共享一些變數** [contex教學](https://golang.design/go-questions/stdlib/context/why/) # websocket 使用 github.com/gorilla/websocket 這是一個小型websocket範例 所以有很多GameServer機制, 請自行慢慢補上 安裝套件 ```go go get github.com/gorilla/websocket ``` ## 測試websocket 的網路小工具 http://www.websocket.org/echo.html ## 下面範例啟動後的 websocket server address ws://127.0.0.1:8899/echo ![](https://i.imgur.com/Xcn5vji.jpg) * Location 欄位填入 Server 監聽的Url= ws://127.0.0.1:8899/echo * 按下Connect按鈕, 建立連線 * Message欄位 輸入想傳送的資料 ```go package main import ( "log" "net/http" "time" //"time" "github.com/gorilla/websocket" ) // 收到封包的callbackfunc // 當有Client Connect時, 就會觸發此函式 ( 所以是多執行序並行處理 ) func RecvFunc(w http.ResponseWriter, r *http.Request) { // websocket 環境設定 設定緩衝區, 封包是否壓縮 upgrader := &websocket.Upgrader{ //如果有 cross domain 的需求,可加入這個,不檢查 cross domain CheckOrigin: func(r *http.Request) bool { return true }, ReadBufferSize: 10000, EnableCompression: true, } // 取得 websocketObj => ws ws, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("upgrade:", err) return } defer func() { log.Println("disconnect !!") // websocket 連線資源釋放 ws.Close() }() // TODO 將 websocketObj 儲存在動態陣列內(slice) // 無窮迴圈, 這個client 的 connection, 不間斷的收發資料 for { // 線上有發現即使close websocket, ReadMessage低機率不會return error // 每次將收封包前都設定閒置 2分鐘 timeout , 太久沒反應直接把client斷線 deadline := time.Now().Add(time.Duration(2) * time.Minute) ws.SetReadDeadline(deadline) // 阻塞在此, 直到 ReadMessage 有讀取到資料 mtype, msg, err := ws.ReadMessage() if err != nil { log.Println("接收資料錯誤, 有client斷線了 mtype:", mtype, " err=", err) // DOTO 如果有將 websocketObj儲存在slice內的話, 這邊要釋放client資源 break } // 列印收到的封包資料 通常是 json 字串 log.Printf("receive: %s\n", msg) // DOTO: 通常封包資料是Json字串, 所以需要 json to obj // DOTO: 根據obj 的封包資料, 來決定要呼叫哪個函式來處理 // DOTO: 打包封包回應資料 obj to json // 簡單的將收到的封包送出 (WriteMessage 本身不支援多執行序, 需要用lock, unlock包起來, 或使用channel jobqueue包起來) err = ws.WriteMessage(mtype, msg) if err != nil { log.Println("準備傳送資料 write:", err) break } } } func main() { // 設定收到封包的 callbackfunc 完整的gameServer Url= ws://127.0.0.1:8899/echo http.HandleFunc("/echo", RecvFunc) log.Println("server start at :8899") // 啟動 websocket 模組, 會阻塞在此, 監聽Port:8899 log.Fatal(http.ListenAndServe(":8899", nil)) } ``` # Webapi 使用 github.com/gorilla/mux 安裝套件 ```go go get -u github.com/gorilla/mux ``` [教學1-REST-API with Golang and Mux](https://medium.com/@hugo.bjarred/rest-api-with-golang-and-mux-e934f581b8b5) [教學2-深入理解Golang之http server](https://juejin.im/post/5dd11baff265da0c0c1fe813) [教學3-取得GetUrl參數](https://ithelp.ithome.com.tw/articles/10202800) 完整範例 ```go package main import ( "encoding/json" "fmt" "math/rand" "net/http" "strconv" "runtime" "runtime/debug" "github.com/gorilla/mux" ) type Post struct { ID string `json:"id"` Title string `json:"title"` Body string `json:"body"` } var posts []Post // 回應封包 type ModelCommonHttpResponseInfo struct { Code int // 錯誤代碼 Message string // 錯誤代碼 Data interface{} // 傳輸的資料 } // http get 例子 => http://127.0.0.1:8000/testGCMem?version=123&gameCode=fish func testGCMem(w http.ResponseWriter, r *http.Request) { fmt.Println("收到封包 testGCMem 開始===") w.Header().Set("Content-Type", "application/json") // 取得 http Url參數 //vars := mux.Vars(r) //version := vars["version"] //gameCode := vars["gameCode"] //fmt.Printf("version=%v, gameCode=%s \r\n", version, gameCode) values := r.URL.Query() version := r.URL.Query().Get("version") gameCode := r.URL.Query().Get("gameCode") fmt.Printf("values=%v, version=%s, gameCode=%s \r\n", values, version, gameCode) //=============做事情================== // 釋放OS的記憶體 debug.FreeOSMemory() // 強制觸發GC runtime.GC() //=================================== // 組合回傳封包 var commResponse ModelCommonHttpResponseInfo commResponse.Code = 0 commResponse.Message = "無資料回傳" json, err := json.Marshal(commResponse) if err != nil { fmt.Printf("json解析錯誤 err=%v", err) return } // 輸出 w.Write(json) fmt.Println("收到封包 testGCMem 離開===") } func getPosts(w http.ResponseWriter, r *http.Request) { fmt.Println("收到封包 getPosts") w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(posts) } // 接收 http post func createPost(w http.ResponseWriter, r *http.Request) { fmt.Println("收到封包 createPost") w.Header().Set("Content-Type", "application/json") //header := r.Header.Get("test") //fmt.Println("header=", header) for key, header := range r.Header { fmt.Printf("列印 header key=[%s], header=[%s] \r\n", key, header) } // 接收 form-data username := r.PostFormValue("UserName") password := r.PostFormValue("Password") fmt.Println("username=", username, "password=", password) // 讀取body內的rawData (Text) bodybyte, err := ioutil.ReadAll(r.Body) if err != nil { json.NewEncoder(w).Encode("錯誤") return } strBody := string(bodybyte[:]) fmt.Println("收到封包 strBody=", strBody) // 組合回傳資訊 var post Post _ = json.NewDecoder(r.Body).Decode(&post) //post.ID = strconv.Itoa(rand.Intn(1000000)) post.Title = "收到getPosts的回應" post.ID = username post.Body = password posts = append(posts, post) // 回傳 json.NewEncoder(w).Encode(&post) } func getPost(w http.ResponseWriter, r *http.Request) { fmt.Println("收到封包 getPost") w.Header().Set("Content-Type", "application/json") params := mux.Vars(r) for _, item := range posts { if item.ID == params["id"] { json.NewEncoder(w).Encode(item) return } } json.NewEncoder(w).Encode(&Post{}) } func updatePost(w http.ResponseWriter, r *http.Request) { fmt.Println("收到封包 updatePost") w.Header().Set("Content-Type", "application/json") params := mux.Vars(r) for index, item := range posts { if item.ID == params["id"] { posts = append(posts[:index], posts[index+1:]...) var post Post _ = json.NewDecoder(r.Body).Decode(&post) post.ID = params["id"] posts = append(posts, post) json.NewEncoder(w).Encode(&post) return } } json.NewEncoder(w).Encode(posts) } func deletePost(w http.ResponseWriter, r *http.Request) { fmt.Println("收到封包 deletePost") w.Header().Set("Content-Type", "application/json") params := mux.Vars(r) for index, item := range posts { if item.ID == params["id"] { posts = append(posts[:index], posts[index+1:]...) break } } json.NewEncoder(w).Encode(posts) } func main() { fmt.Println("啟動webapi") // 組合一些等等要回傳的訊息 posts = append(posts, Post{ID: "1", Title: "My first post", Body: "This is the content of my first post"}) // 設定 監聽的 router router := mux.NewRouter() router.HandleFunc("/posts", getPosts).Methods("GET") router.HandleFunc("/posts", createPost).Methods("POST") router.HandleFunc("/posts/{id}", getPost).Methods("GET") router.HandleFunc("/posts/{id}", updatePost).Methods("PUT") router.HandleFunc("/posts/{id}", deletePost).Methods("DELETE") // 目前拿來測GC router.HandleFunc("/testGCMem", testGCMem).Methods("GET") // 啟動webapi, 阻塞監聽 port 8000 port := 8000 fmt.Println("啟動webapi port=", port) configStr := fmt.Sprintf(":%d", port) http.ListenAndServe(configStr, router) } ``` 驗證工具使用 post man ![](https://i.imgur.com/cI2OpWK.png) # Garbage Collection (記憶體回收) Golang 內建 記憶體回收 有 new 就要 delete 有 new 不須 delete, 代表GC系統, 幫你判斷沒使用到此變數時, 就幫你釋放此記憶體 GC時, Server會停頓一下 [GO GC 垃圾回收机制](https://segmentfault.com/a/1190000018161588) [图解Golang的GC算法](https://juejin.im/post/6844903793855987719) # import 注意 錯誤訊息 import cycle not allowed 發生原因 ``` 模組A import 模組B 模組B import 模組A ``` 1. 解決方法1 想辦法調整架構 模組B import 模組A 有上下關係, 但別import繞成一個圓圈 2. 解決方法2 使用interface [解決教學](https://blog.csdn.net/ggq89/article/details/81148558) 3. 解決方法3 使用函式指標, 降低彼此依賴耦合 模組B 開放一些函式指標, 模組B.Add = 模組A.Add 模組B.Add( 1, 2 ) [Golang學習筆記--函式作為值的使用](https://www.itread01.com/cll.html) # GDB 遠端debug [Setting up Debugging in Go with VS Code](https://dev.to/nyxtom/debugging-in-go-in-vs-code-1c7f) [Golang 單步除錯利器 — Delve](https://medium.com/@dubiety/golang-%E5%96%AE%E6%AD%A5%E9%99%A4%E9%8C%AF%E5%88%A9%E5%99%A8-delve-7cf4c05e2f08) # 效能除錯 [go benchmark 性能测试] 1. 基準測試的代碼文件必須以_test.go結尾 2. 基準測試的函數必須以基准開頭,必須是可衍生的 3. 基準測試函數必須接受一個指向基準類型的指針作為唯一參數 4. 基準測試函數不能有返回值 5. b.ResetTimer是重置計時器,這樣可以避免循環之前的初始化代碼的干擾 6. 最後的對於循環很重要,被測試的代碼要放到循環裡 7. b.N是基準測試框架提供的,表示循環的次數,因為需要反複調用測試的代碼,才可以評估性能 8. 如果想讓測試運行的時間更長,可以通過-benchtime指定,大約3秒 ```go= // 方式1 go test -bench=. -run=none // 方式2 等待3秒 go test -bench=. -benchtime=3s -run=none // 方式3 產生web圖需要的 profile.out 檔案 go test -bench=. -benchmem -cpuprofile profile.out ``` ```go= package main import ( "fmt" "strconv" "testing" ) // 效能測試的殼, 函式名稱強制規定 Benchmark 開頭 func Benchmark_Sprintf(b *testing.B) { num := 10 // 重置計時器 b.ResetTimer() for i := 0; i < b.N; i++ { // 實際要測試的區塊 各種組合 數字轉字串 方式1 fmt.Sprintf("%d", num) } } func Benchmark_Format(b *testing.B) { num := int64(10) // 重置計時器 b.ResetTimer() for i := 0; i < b.N; i++ { // 實際要測試的區塊 各種組合 數字轉字串 方式2 strconv.FormatInt(num, 10) } } func Benchmark_Itoa(b *testing.B) { num := 10 // 重置計時器 b.ResetTimer() for i := 0; i < b.N; i++ { // 實際要測試的區塊 各種組合 數字轉字串 方式3 strconv.Itoa(num) } } ``` [線上程式碼, 請copy回本地端執行](https://play.golang.org/p/YyOb-T3UuXD) ![](https://i.imgur.com/v02aeAa.png) [go benchmark 性能测试](https://my.oschina.net/solate/blog/3034188) # 多執行序相關套件 ```go= // 代碼中, import此套件可以偵測哪一個鎖延遲最久, 方便偵測死鎖 github.com/sasha-s/go-deadlock //偵測資料競爭 //編譯時增加參數 race go build -race ``` # pprof ### 安裝套件 graphviz [Windows安裝包](https://www2.graphviz.org/Packages/stable/windows/10/msbuild/Release/Win32/) ### 設定window path 解壓縮檔案後, copy 到此目錄 C:\Program Files\Graphviz\bin 再去環境別數內設定此工作path ![](https://i.imgur.com/r3EuZFZ.png) ```go= package main import ( "fmt" "net/http" ) func main() { // 監聽web圖要使用 最基本的webapi框架 err := http.ListenAndServe(":9999", nil) if err != nil { fmt.Println("PprofInit 錯誤 err=", err) panic(err) } } ``` 瀏覽器網址輸入 http://127.0.0.1:9999/debug/pprof/ 即可觀看基本的運行效能參數 ![](https://i.imgur.com/g6CfEdi.png) 這幾個路徑表示的是 /debug/pprof/profile:訪問這個連結會自動進行 CPU profiling,持續 30s,並生成一個檔案供下載 /debug/pprof/block:Goroutine阻塞事件的記錄。預設每發生一次阻塞事件時取樣一次。 /debug/pprof/goroutines:活躍Goroutine的資訊的記錄。僅在獲取時取樣一次。 /debug/pprof/heap: 堆記憶體分配情況的記錄。預設每分配512K位元組時取樣一次。 /debug/pprof/mutex: 檢視爭用互斥鎖的持有者。 /debug/pprof/threadcreate: 系統執行緒建立情況的記錄。 僅在獲取時取樣一次。 ### 使用web圖 觀察的函式需要用 Benchmark 包裝起來 ```go func Benchmark_Fib10(b *testing.B) { // run the Fib function b.N times for n := 0; n < b.N; n++ { fmt.Sprintf("組合字串 n=%d", n) } } ``` 製造 profile.out 檔案 ```go= // 方式3 產生web圖需要的 profile.out 檔案 go test -bench=. -benchmem -cpuprofile profile.out ``` windows cmd 命令列輸入 go tool pprof profile.out pprof 命令列輸入 web ![](https://i.imgur.com/V44zysr.png) 或 直接輸入 go tool pprof -http=:9999 profile.out ![](https://i.imgur.com/5R3W0kS.png) ps: 紅線和框框越大, 占用時間越長 # 火焰图 ```go= go tool pprof -http=":10000" http://localhost:9999/debug/pprof/profile ``` 过一会儿会产生个web窗口, 选择 VIEW->Flame Graph 得到火焰图形 ![](https://i.imgur.com/Sv8a2JP.png) [教學1-go benchmark 性能测试](https://my.oschina.net/solate/blog/3034188) [教學2-golang性能优化之pprof及其火焰图](https://www.jianshu.com/p/6175798c03b4) # 遠端DEBUG [透過VSCode在Docker裡頭debugging](https://ithelp.ithome.com.tw/articles/10232971) # 套件推薦 [websocket套件-gorilla](https://github.com/gorilla/websocket) [gameServer框架-mqant](https://github.com/liangdas/mqant) [後台GUI套件-goadmin](https://github.com/chenhg5/go-admin/blob/master/README_CN.md) [訊息佇列-rabbitmq](http://www.prochainsci.com/2018/11/message-queue-1-rabbitmq-go.html) [grpc](https://myapollo.com.tw/zh-tw/golang-grpc-tutorial-part-1/) # VSCODE 常用套件 | 名稱 | 內容 | 教學 | | --------------- | ------------------- |:------------------------------------------------------------------------------ | | gotest-snippets | 單元測試使用 | [Go 語言單元測試學習筆記](https://hackmd.io/@XdPAx-MLS1O3IFd5rxQKVA/rkfoVVLhs) | | Code Navigation | vscode 看 interface | | Code Navigation 具體步驟如下: 1. 安裝Code Navigation 外掛程式。 2. 在程式碼中選取介面名稱,並右鍵點選「Go to Implementations」。 3. 如果介面有多個實現,會彈出一個列表,選擇要查看的實作。 4. 在實作檔中,選取具體方法名,右鍵點選「Go to Definition」即可跳到該方法的定義處。 注意:在使用該插件時,需要確保程式碼中的介面和實作檔案都在同一個工作區中。 # 教學連結 [30天導入Golang](https://ithelp.ithome.com.tw/users/20092379/ironman/2062) [Go 程式設計導論](http://golang-zhtw.netdpi.net/10-concurrency/10-02-channel) [Go 语言教程](https://www.runoob.com/go/go-tutorial.html) [語言技術:Go 語言](https://openhome.cc/Gossip/Go/index.html) [[Golang] 程式設計教學](https://michaelchen.tech/golang-programming/) [物件導向與封裝](https://medium.com/%E7%A8%8B%E5%BC%8F%E6%84%9B%E5%A5%BD%E8%80%85/%E4%BB%80%E9%BA%BC%E6%98%AFoo-%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E8%88%87%E5%B0%81%E8%A3%9D-80379c24e62) [低耦合,高內聚](https://www.itread01.com/content/1544331626.html) [設計模式 ( Design Pattern )](https://ithelp.ithome.com.tw/articles/10201706) [三種好用的 gRPC 測試工具](https://blog.wu-boy.com/2022/08/three-grpc-testing-tool/) [為什麼程式需要單元測試? - 概念篇](https://www.gss.com.tw/blog/why-program-need-unit-test-intro) [Go: 关于锁(mutex)的一些使用注意事项](https://mozillazg.com/2019/04/notes-about-go-lock-mutex.html) [Web 基礎](https://willh.gitbook.io/build-web-application-with-golang-zhtw/03.0) [VS Code 常用快速鍵](https://eudora.cc/posts/31485/) [VS Code 快捷鍵大全](https://kknews.cc/n/gpq2xxm.html) ```go= windows alt+← mac control + - 在Windows中可以使用快捷鍵 "Alt + ←" 實現。 在Linux中可以使用快捷鍵 "Ctrl + Alt + -" 實現。 在Mac中可以使用快捷鍵 "Ctrl + - "實現。 ``` # 考題 [Go常見面試題【由淺入深】2022版](https://zhuanlan.zhihu.com/p/471490292)