owned this note
owned this note
Published
Linked with GitHub
---
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)
在命令提示字元輸入指令即可
編譯 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
```
# 使用 GoMod 來建立專案
傳統舊方式是設定 gopath, 來讓編輯器知道你程式碼檔案路徑
以方便編譯出執行檔
但這種方式每一台電腦都要設定一次 windows環境變數檔案
又分windows 和 linux 兩種, 會很瑣碎, 可攜性較差


使用GoMod 來管理專案編譯路徑
建立好專案資料夾後
輸入 go mod init 專案名稱

會出現兩個檔案 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)
實際目錄配置




# 第一個程式 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
* 沒使用到的變數, 要註解掉, 不然編譯會錯誤 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}/game_server.go",
"env": {},
"args": []
}
]
}
```
"program": 是告訴vscode編輯器, 預設執行的程式碼路徑
上面範例是強制使用 game_server.go 當成第一個執行的檔案
預設值為 "program": "${fileDirname}",
代表是成為視窗焦點的程式碼會被執行
# 基本資料型態
基本型態
| 資料型態 | 說明 |
| -------- | -------- |
| uint | 無號整數 |
| uint8 | 無號整數 |
| uint16 | 無號整數 |
| uint32 | 無號整數 |
| uint64 | 無號整數 |
| int | 有號整數 |
| int8 | 有號整數 |
| int16 | 有號整數 |
| int32 | 有號整數 |
| int64 | 有號整數 |
| flaot32 | 浮點數 |
| flaot64 | 浮點數 |
| string | 字串 |
| bool | 布林值 true 或 false |
儲存結構
| 資料型態 | 說明 |
| -------- | -------- |
| Array和Slice | 陣列和切片 |
| Map | 映射或字典, key 和 value |
| struct | 結構 |
非同步使用
| 資料型態 | 說明 |
| -------- | -------- |
| channel | 通道, 工作佇列或訊息佇列 |
| sync.Mutex | 執行序鎖 lock unlock |
# 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"
)
// 列舉
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 (映射)
```go
package main
import (
"fmt"
)
// 列印map
func PrintfMap(m map[string]int) {
// 使用range 尋訪map
fmt.Println("使用range 尋訪map 長度=", len(m) )
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
delete(m, "dog")
// 列印map
PrintfMap(m)
}
```
# array 陣列
```go
package main
import (
"fmt"
)
func main() {
var a [5]int //宣告一個大小5的 int array, 陣列初始值為0
var b []int //宣告空的 int array
var c = []int{1, 2, 3, 4, 5} //宣告一個大小5的 int array, 並給初始值
// 尋訪陣列1
for i := 0; i < len(c); i++ {
value := c[i]
fmt.Println("尋訪陣列1 值=", value)
}
// 尋訪陣列2
for i := range c {
value := c[i]
fmt.Println("尋訪陣列2 值=", value)
}
fmt.Println("a=", a)
fmt.Println("b=", b)
fmt.Println("c=", c)
}
```
# slice 切片 (動態陣列)
slice切出來的陣列其實都指向同一個陣列
所以操作時候要特別小心避免改到原始陣列
如果說想要複製一個新的陣列, 就使用 copy 來拷貝
完整範例
```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, 5)
var list4 []int // 未初始化
//var list3 []int = make([]int, 20)
for i := 0; i < 10; i++ {
list = append(list, i+1)
list2 = append(list2, i+10)
}
// 陣列[6] 到 陣列[9-1]
slice := list[6:9]
// 陣列[4] 到底
slice2 := list[4:]
// 陣列[0] 到 陣列[6-1]
slice5 := list[0:6]
// 冒號左邊沒寫數字代表預設0 => 陣列[0] 到 陣列[6-1]
slice6 := list[:6]
// COPY slice ( 若目的陣列 空間過小, 則copy時, 以目的陣列大小為主, 所以通常copy前, 目的陣列會配置跟來源陣列一樣大 )
var list3 []int = make([]int, len(list2)-5)
copy(list3, list2)
// 一次append 整個 list2, 但從 list[10] 到最後
list3 = append(list3, list2[10:]...)
fmt.Printf("TraditionArray(傳統陣列, 基本預設值就都是0了)=%v \r\n", TraditionArray)
fmt.Printf("list(有make初始化, 所以內容都塞0)=%v \r\n", list)
fmt.Printf("slice=%v \r\n", slice)
fmt.Printf("slice2=%v \r\n", slice2)
fmt.Printf("list3=%v \r\n", list3)
fmt.Printf("list4(沒有make初始化, 所以是nil)=%v \r\n", list4)
fmt.Printf("slice5=%v \r\n", slice5)
fmt.Printf("slice6=%v \r\n", slice6)
// 使用 range 搜尋陣列 (注意 他雖然可以每一個陣列都搜尋, 但不保證順序 )
// 要固定順序請使用 傳統搜尋陣列法
// 使用 range 搜尋陣列, data是拷貝出來的變數 並不等於 list3[n] 內的值
//
for i, data := range list3 {
fmt.Printf("使用range搜尋法 i=%d, data(指標)=%p, data(內容)=%v \r\n", i, &data, data)
}
// 傳統陣列搜尋法
for i := 0; i < len(list3); i++ {
pData := &list3[i]
fmt.Printf("傳統陣列搜尋法 i=%d, data(指標)=%p, data(內容)=%v \r\n", i, pData, pData)
}
}
```
# 物件導向
```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) {
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
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())
}
```
# 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 {
pDodizu := &Dodizu{
Name: name,
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 (執行緒)
```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()
time.Sleep(time.Millisecond)
}
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)
}
```
# channel 通道
用來當作工作佇列(JobQueue)或是訊息佇列用(MessageQueue)
基本單一通道原理: 接收者跟傳送者都要準備好, 才能開始收發資料, 不然會阻塞卡在此處

建立通道
```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)
}
```
# 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

* 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](https://medium.com/@hugo.bjarred/rest-api-with-golang-and-mux-e934f581b8b5)
[教學2](https://juejin.im/post/5dd11baff265da0c0c1fe813)
完整範例
```go
package main
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"strconv"
"github.com/gorilla/mux"
)
type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Body string `json:"body"`
}
var posts []Post
func getPosts(w http.ResponseWriter, r *http.Request) {
fmt.Println("收到封包 getPosts")
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(posts)
}
func createPost(w http.ResponseWriter, r *http.Request) {
fmt.Println("收到封包 createPost")
w.Header().Set("Content-Type", "application/json")
var post Post
_ = json.NewDecoder(r.Body).Decode(&post)
post.ID = strconv.Itoa(rand.Intn(1000000))
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() {
// 設定 監聽的 router
router := mux.NewRouter()
posts = append(posts, Post{ID: "1", Title: "My first post", Body: "This is the content of my first post"})
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")
// 啟動webapi, 阻塞監聽 port 8000
http.ListenAndServe(":8000", router)
}
```
驗證工具使用 post man

# Garbage Collection (記憶體回收)
Golang 內建 記憶體回收
# import 注意
錯誤訊息 import cycle not allowed
發生原因
```
模組A import 模組B
模組B import 模組A
```
解決方法1
想辦法調整架構 模組B import 模組A
解決方法2
使用interface
[解決教學](https://blog.csdn.net/ggq89/article/details/81148558)
# 套件推薦
[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/)
# 教學連結
[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/)