---
# System prepended metadata

title: Golang 教學筆記

---


---
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 兩種, 會很瑣碎, 可攜性較差
![](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)



# 第一個程式 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)

基本單一通道原理: 接收者跟傳送者都要準備好, 才能開始收發資料, 不然會阻塞卡在此處
![](https://i.imgur.com/tgYzeWx.jpg)

建立通道
```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
 
 ![](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](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
![](https://i.imgur.com/cI2OpWK.png)


# 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/)