title: 'Golang 教學筆記'
–-
在命令提示字元輸入指令即可
Windows 熱鍵 Ctrl + ,
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 build test.go
清除編譯結果
go clean
安裝套件1 [go get 套件url]
go get github.com/gorilla/websocket
編譯 + 執行 test.go
go run test.go
單元測試 (函式要宣告成 func TestNew(t *testing.T))
go test -v -run=New
檢查資料競爭
go run -race test.go
Golang - GOROOT、GOPATH、Go Modules 三者的關係介紹
傳統舊方式是設定 gopath, 來讓編輯器知道你程式碼檔案路徑
以方便編譯出執行檔
但這種方式每一台電腦都要設定一次 windows環境變數檔案
又分windows 和 linux 兩種, 會很瑣碎, 專案可攜性較差
使用GoMod 來管理專案編譯路徑
建立好專案資料夾後
輸入 go mod init 專案名稱
會出現兩個檔案 go.mod 和 go.sum
其中 go.mod 是我們要維護的【使用套件檔案】 (編譯器發現你有使用第三方套件, 會幫你自動添加套件github路徑)
實際運行後的套件檔案內容
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
實際目錄配置
輸入 go mod init 專案名稱
由於套件名稱輸入 Example
所以 import 時的根目錄就從 Example開始
package main
import (
"fmt"
"time"
"Example/src/ModelDefine"
"Example/src/ModelWebapi" // webapi 範例
"Example/src/ModelWebsocket" // websocket 範例
)
func main() {
// websocket 範例, 阻塞在這邊
ModelWebsocket.DemoWebsocket()
}
套件內容, 程式碼最上方 package ModelWebsocket
為規劃的套件名稱
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))
}
https://codertw.com/前端開發/391937/
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, 用來引入第三方套件或是自製套件
寫程式要點
{
// 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
基本型態
資料型態 | 大小 | 範圍 | 說明 |
---|---|---|---|
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 |
在函式體外宣告的變數稱之為全域性變數,全域性變數可以在整個包甚至外部包(被匯出後)使用。
全域性變數可以在任何函式中使用
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
}
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)
}
}
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)
}
待補
使用 key 和 value 來儲存資料的結構
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
}
類似傳統的靜態固定大小的陣列
使用 len 來判斷陣列大小
初始化時有給長度數值, 就是陣列, 沒給數值會歸類在 slice
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切出來的陣列其實都指向同一個陣列
所以操作時候要特別小心避免改到原始陣列
如果想要新增一筆資料在陣列尾端, 就使用append
如果說想要複製一個新的陣列, 就使用 copy 來拷貝
len() 和 cap() 都是 slice在使用的函式
slice 底層結構
完整範例
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
}
}
//執行結果
已清除主控台
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
要自己手動個別copy
在使用 = 和 copy 時, 要注意其影響範圍
然後slice 在append 後, 只要資料容量超過cap時, 會自動擴充兩倍
將重複性的功能, 製作成一個函式, 方便重複利用
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)
}
// 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")
}
// 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")
}
輸出結果
觀察傳入參數 aaaaaa
觀察傳入參數 bbbbbb
觀察傳入參數 ccccc
觀察傳入參數 ddddd
MyPrint step=第3次, value=12
MyPrint step=第2次, value=11
MyPrint step=第1次, value=10
避免程式崩潰
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()
}
覆蓋(override)
是指父類別和子類別都有同樣函式
如果 子類別繼承父類別, 執行有被覆蓋的函式時 會以子類別的為主
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不支援 )
是指同樣函式名稱, 但不同參數
func (p *Point) SetX(x float64) {
}
func (p *Point) SetX(x float64, x2 float64) {
}
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 組合 公司裡面有員工 有電腦
完整範例
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")
}
thread 和 goroutine 比較
資料競爭原理
cpu1獲取變數a,準備加1時(還沒更新完整個暫存器的變數),cpu2也獲取了變數a, 也同樣準備加1, 導致兩邊都是拿到 a=30 開始加1。
淺談Golang 中的 Data Race Condition(資料競爭)
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)
// 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)
}
Go語言的讀寫鎖方法主要有下面這種
基本單一通道原理: 接收者跟傳送者都要準備好, 才能開始收發資料, 不然會阻塞卡在此處
有緩衝區的通道, 則可以一直塞資料, 直到通道滿時才會阻塞(寫入101筆時)
var iQueue chan int = make(chan int, 100)
如果建立一個0的通道, 會無法寫入資料, 也無法讀取資料(會阻塞, 編譯時會有警告)
var iQueue chan int = make(chan int, 0)
關閉channel => 使用 close(channelName)
重複關閉channel, 會導致panic
建立通道
// 建立一個字元通道
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))
接收通道內的資料, 將資料往通道內送
// 建立一個通道 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("通道範例 執行完畢")
完整範例
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 開始學起
Go 語言使用 Select 四大用法
[Golang]關於 Channels 的控制一些要注意的事項(一)
同時取消某個api請求產生的一堆goroutine
goroutine共享一些變數
contex教學
這是一個小型websocket範例
所以有很多GameServer機制, 請自行慢慢補上
安裝套件
go get github.com/gorilla/websocket
http://www.websocket.org/echo.html
ws://127.0.0.1:8899/echo
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))
}
安裝套件
go get -u github.com/gorilla/mux
教學1-REST-API with Golang and Mux
教學2-深入理解Golang之http server
教學3-取得GetUrl參數
完整範例
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
Golang 內建 記憶體回收
有 new 就要 delete
有 new 不須 delete, 代表GC系統, 幫你判斷沒使用到此變數時, 就幫你釋放此記憶體
GC時, Server會停頓一下
錯誤訊息 import cycle not allowed
發生原因
模組A import 模組B
模組B import 模組A
解決方法1
想辦法調整架構 模組B import 模組A
有上下關係, 但別import繞成一個圓圈
解決方法2
使用interface
解決教學
解決方法3
使用函式指標, 降低彼此依賴耦合
模組B 開放一些函式指標,
模組B.Add = 模組A.Add
模組B.Add( 1, 2 )
Golang學習筆記–函式作為值的使用
Setting up Debugging in Go with VS Code
[go benchmark 性能测试]
// 方式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
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)
}
}
// 代碼中, import此套件可以偵測哪一個鎖延遲最久, 方便偵測死鎖
github.com/sasha-s/go-deadlock
//偵測資料競爭
//編譯時增加參數 race
go build -race
解壓縮檔案後, copy 到此目錄
C:\Program Files\Graphviz\bin
再去環境別數內設定此工作path
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/
即可觀看基本的運行效能參數
這幾個路徑表示的是
/debug/pprof/profile:訪問這個連結會自動進行 CPU profiling,持續 30s,並生成一個檔案供下載
/debug/pprof/block:Goroutine阻塞事件的記錄。預設每發生一次阻塞事件時取樣一次。
/debug/pprof/goroutines:活躍Goroutine的資訊的記錄。僅在獲取時取樣一次。
/debug/pprof/heap: 堆記憶體分配情況的記錄。預設每分配512K位元組時取樣一次。
/debug/pprof/mutex: 檢視爭用互斥鎖的持有者。
/debug/pprof/threadcreate: 系統執行緒建立情況的記錄。 僅在獲取時取樣一次。
觀察的函式需要用 Benchmark 包裝起來
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 檔案
// 方式3 產生web圖需要的 profile.out 檔案
go test -bench=. -benchmem -cpuprofile profile.out
windows cmd 命令列輸入
go tool pprof profile.out
pprof 命令列輸入 web
或 直接輸入
go tool pprof -http=:9999 profile.out
ps: 紅線和框框越大, 占用時間越長
go tool pprof -http=":10000" http://localhost:9999/debug/pprof/profile
过一会儿会产生个web窗口, 选择 VIEW->Flame Graph 得到火焰图形
websocket套件-gorilla
gameServer框架-mqant
後台GUI套件-goadmin
訊息佇列-rabbitmq
grpc
名稱 | 內容 | 教學 |
---|---|---|
gotest-snippets | 單元測試使用 | Go 語言單元測試學習筆記 |
Code Navigation | vscode 看 interface |
Code Navigation 具體步驟如下:
注意:在使用該插件時,需要確保程式碼中的介面和實作檔案都在同一個工作區中。
30天導入Golang
Go 程式設計導論
Go 语言教程
語言技術:Go 語言
[Golang] 程式設計教學
物件導向與封裝
低耦合,高內聚
設計模式 ( Design Pattern )
為什麼程式需要單元測試? - 概念篇
Go: 关于锁(mutex)的一些使用注意事项
windows alt+←
mac control + -
在Windows中可以使用快捷鍵 "Alt + ←" 實現。
在Linux中可以使用快捷鍵 "Ctrl + Alt + -" 實現。
在Mac中可以使用快捷鍵 "Ctrl + - "實現。