title: 'Golang 教學筆記'
-

Golang 教學筆記

安裝

Golang官網

線上免安裝小編輯器

線上文件

命令提示字元指令集

在命令提示字元輸入指令即可

編譯 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

使用 GoMod 來建立專案

傳統舊方式是設定 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

gomod教學

實際目錄配置



第一個程式 hello world

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 程式

執行專案方式

  • vscode 直接對著主程式碼, 按下F5
  • 或進入命令提示字元, 切到專案目錄下執行 go build exapmle.go 會輸出執行檔案
  • 或進入命令提示字元, 切到專案目錄下執行 go build 會根據套件名稱建立執行檔案
  • go clean 會清除編譯相關檔案
{ // 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 判斷

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 和 列舉

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 (映射)

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 陣列

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 來拷貝

完整範例

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)
    }	
}

物件導向

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教學

完整範例

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 (執行緒)

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

基本單一通道原理: 接收者跟傳送者都要準備好, 才能開始收發資料, 不然會阻塞卡在此處

建立通道

// 建立一個字元通道
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)
}

websocket 使用 github.com/gorilla/websocket

這是一個小型websocket範例
所以有很多GameServer機制請再慢慢補上

安裝套件

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欄位 輸入想傳送的資料
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 get -u github.com/gorilla/mux

教學1
教學2

完整範例

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

解決教學

套件推薦

websocket套件-gorilla
gameServer框架-mqant
後台GUI套件-goadmin
訊息佇列-rabbitmq
grpc

教學連結

30天導入Golang
Go 程式設計導論
Go 语言教程
語言技術:Go 語言
[Golang] 程式設計教學

Select a repo