# Let's Go ![Golang-TW](https://gophercises.com/img/gophercises_logo.png) ###### tags: `Golang` :::success **Golang** 類似一套現代版C語言的程式語言 語法簡潔, 程式強大 (真的有效率! 差C語言一點, 卻又比其他的快) 即使架設網站, 已有豐富的內建函式庫, 不用框架也可簡單架設 ::: ---- ## 線上資源 1. [Go官網](https://golang.org/) 2. [php -> golang](https://yami.io/php-to-golang/) :+1: (PHP轉Go) 3. [Go Web](https://astaxie.gitbooks.io/build-web-application-with-golang/content/zh/03.2.html) :sunny: (架設網站必看) 4. [Go Book](http://www.golang-book.com/books/intro) :100: (官網書本) 5. [Go 程式設計導論](http://golang-zhtw.netdpi.net/) :boom: (詳細) 6. [Go-WebSocket](https://github.com/googollee/go-socket.io) 7. [Go語言中文網](https://studygolang.com/) (週刊好物) --- ## [安裝 Go](https://golang.org/dl/) 1. 下載Go壓縮檔 - **go1.14.9.linux-amd64.tar.gz** (檔名可能會版本不同, 請自行修改) 2. 解壓縮到指定位子 ```shell $ rm -rf /usr/local/go # 如果有裝過其他版本的go,可先移除 $ tar -C /usr/local -xzf go1.14.9.linux-amd64.tar.gz $ ls /usr/local # 可以看到 go/ 的資料夾 ``` 3. 將 Go 加入環境變數 $PATH ```shell= # ~/.bashrc ($HOME底下的檔案, 只有目前使用者可使用) # 或者加在 /etc/profile (所有使用者皆可使用, 加完須重新開機) # -------------------------------------------------- # 以下三行加在 ~/.bashrc 檔案的最底下 # Go PATH export GOPATH=~/go export GOROOT=/usr/local/go export PATH=$PATH:$GOROOT/bin:$GOPATH/bin ``` 4. 建立 hello.go ```go= package main import "fmt" func main() { fmt.Printf("hello, world\n") } ``` 5. 編譯go並執行, 有顯示 hello, world 表示安裝成功 :+1: ```shell $ go run hello.go hello, world ``` :::warning 以上步驟安裝完成後, 僅使用者可以使用go 若希望 root 身份也可以使用 go, 請執行以下指令即可 ```shell= sudo ln -s /usr/local/go/bin/go /usr/bin/go ``` ::: --- ## 練習檔案 Hello.go ```go= package main import "fmt" func main() { fmt.Println("hello, world") } ``` --- ## Go CMD 指令 ### 編譯檔案 (compile) ```shell # 目前目錄與檔案 $ pwd /home/zuolar/test $ ls hello.go # 指定 .go 檔案, 編譯並以 .go 檔名為執行檔名 $ go build hello.go $ ls hello.go hello $ ./hello hello world # 自動編譯當前目錄的 .go 檔案, 並以目前資料夾名稱為執行檔名 $ go build $ ls # hello.go test $ ./test hello, world # 自動尋找 $HOME/go/src/$PACKAGE (from $GOPATH) # 或是 /usr/local/go/src/$PACKAGE (from $GOROOT) # 底下的 .go 檔案, 並以 $PACKAGE 為執行檔名, 編譯出執行檔在當前目錄 $ go build hello # $PACKAGE = hello $ ls hello $ ./hello hello world ``` ### 編譯並執行檔案 (run) :::info 純粹執行, 不會產生exe檔 ::: ```shell # 單一 Go 檔案 $ go run hello.go hello world # 一次 Run 所有 Go 檔案,用在 Go 檔之間有互相呼叫或引用 $ go run *.go ``` --- ## Go 程式結構 ```go= /** * 類似 namespace */ package main /** * 引入套件, 如果寫成一行, 需用 ; 隔開 * 套件名稱用雙引號, 不能用單引號! */ import "fmt" import("fmt";"math/rand") import( "fmt" "math/rand" ) /** * 程式進入點, 同C語言的 int main() * {} 的位子有明確規定, { 須在 () 之後 */ func main() { // <---------- 大括號位子 // code... } /** * 以下為錯誤寫法 */ func main() // main { // <---------- 大括號位子 // code... } ``` --- ## Go 資料型態 :::success 變數宣告與JS相同, 使用 var 宣告, 並指定型態 若變數宣告時, 已有給初始值, 可選擇省略指定型態 ```go= // 以下簡單說明 var name string // ---> 使用 var 宣告, 並指定型態 var name string = "Zuolar" // ---> 宣告時, 可以直接給初始值 var name = "Zuolar" // ---> 使用 var 宣告, 直接給初始值, 可以不用給型態 name := "Zuolar" // 同 var name = "Zuolar" ``` ::: ---- ### int ```go= var a int // a = 0 var b int = 1 // b = 1 c := 2 // c = 2 fmt.Println(a, b, c) // ---> 0 1 2 ``` ---- ### float ```go= // 浮點數 精準度 // 最終測試 (浮點數第2位) -> 70兆 // 70 3687 4417 7663.99 // 最終測試 (浮點數第3位) -> 8兆 // 8 7960 9302 2207.999 // 最終測試 (浮點數第4位) -> 1兆 // 1 0899 9999 9999.9999 ``` ---- ### string :::warning 只能用雙引號 " 或 飄字號 ` , 不能使用單引號 ::: ```go= var a string // a = '' (空字串) var b string = "zuolar" // b = "zuolar" c := `hello` // c = "hello" fmt.Println(a, b, c) // ---> zuolar hello ``` ---- ### bool ```go= var boo bool // bool = false var foo bool = true // b = true c := false // c = false fmt.Println(a, b, c) // ---> false true false ``` ---- ### array ```go= // Array 宣告 var arr1 [5]int arr1[1] = 11 // 指定於 index = 1 的位子, 設定值 arr1[3] = 33 // 指定於 index = 3 的位子, 設定值 fmt.Println(arr1, arr1[0]) // ---> [0 11 0 33 0] 0 var arr2 = [5]int {11, 33} // 宣告初始值時, 不能指定 index 位置 fmt.Println(arr2, arr2[0]) // ---> [11 33 0 0 0] 11 ``` --- ### slice :::warning array 固定長度, 所以須定義長度 slice 不固定長度, 所以不須定義長度 **兩者在宣告值時候, 須注意** array 為固定長度, 所以一開始就有預設值 但 slice 為動態長度陣列, 若想加上新的值, 須使用 append 加入新值 ::: ```go= // Slice 宣告 slice := []string{"A", "B", "C", "D"} fmt.Println(slice) // ---> [A B C D] // Slice 取值 /** 1. slice[n] 取第n個值 2. slice[n : m] 取第n個到第m個之前的值 (不包含第m個) 3. slice[:m] 取到第m個之前的值 (不包含第m個) 4. slice[n:] 取第n個之後的值 **/ fmt.Println(slice, slice[1], slice[:2], slice[2:]) // ---> [A B C D] B [A B] [C D] // Slice 加入值 // append 會複製原本數值並回傳一個的新slice // 第一個參數, 想要 append 的 slice // 第二個參數以後, 放想要加入的值 // !!! 若第二個參數沒給, 會變成回傳原slice的位址, 如同下述的"錯誤複製" !!! slice = append(slice, "E", "F", "G") // ---> [A B C D E F G] // Slice 複製 /** 錯誤複製, 這樣是複製位址 (Pointer) **/ var slice2 = slice // ---> [A B C D E F G] slice[1] = "E" fmt.Println(slice) // ---> [A E C D E F G] fmt.Println(slice2) // ---> [A E C D E F G] /** 正確複製 **/ slice2 := make([]string, 7) // make 第二個參數指定長度 // var slice2 [7]string copy(slice2, slice) slice[1] = "E" fmt.Println(slice) // ---> [A E C D E F G] fmt.Println(slice2) // ---> [A B C D E F G] ``` ---- ### map ```go= mm := make(map[string]int) mm["a"] = 12 mm["b"] = 34 fmt.Println(mm, mm["a"]) // ---> map[a:12 b:34] 12 // 可用第二個參數判斷Key值是否存在 // 同PHP的isset zz, exists := mm["zz"] if !exists { fmt.Println("zz is not set") } // 也可以這樣宣告初始值 var demo = map[string]string{ "a": "AA", "b": "BB", "c": "CC", } fmt.Println(demo) // ---> map[a:AA b:BB c:CC] ``` ---- ### interface{} :::warning 跟其他語言的 interface 不同意思! interface{} 代表**任何型態**的資料 **ps. 任何型態, 也就代表包括 array, slice, map, func !!** ::: ```go= var a interface{} fmt.Println(a) // ---> <nil> a = 123 fmt.Println(a) // ---> 123 a = "hello" fmt.Println(a) // ---> hello a = true fmt.Println(a) // ---> true a = map[string]string{"a": "AA", "b": "BB", "c": "CC"} fmt.Println(a) // ---> map[a:AA b:BB c:CC] ``` :::success interface{} 如果要轉成整數 ex. var a interface{} var b int a = 123 b = a ---> Error!! b = a.(int) ---> OK ::: ---- ### struct :::warning struct 可想成 JSON 只用fmt顯示, 不會顯示Key值 ::: ```go= // 定義 struct type Person struct { name string age int } // 可以定義 struct 的方法 func (p *Person) sayHello() { fmt.Println(p.name, "say hello") } func main() { var snoopy Person = Person{name: "Snoopy", age: 100} me := &Person{name: "Zuolar", age: 23} you := new(Person) fmt.Println(snoopy) // ---> {Snoopy 100} fmt.Println(me, me.name) // ---> &{Zuolar 23} Zuolar fmt.Println(you) // ---> &{"" 0} me.sayHello() // ---> Zuolar say hello } ``` --- ## Go 邏輯判斷語法 ### For (For 就是 While) ```go= func main () { sum := 0 // 不需要小括號, 但須給大括號 for i := 0; i < 10; i++ { sum += i } // 無窮迴圈 for { // code.. } fmt.Println(sum) } ``` ### If ```go= func main () { sum := 10 // 一般用法, 一樣不需要小括號 if sum < 5 || sum == 10 { fmt.Println("小於5") } else { fmt.Println("大於5") } // if 的簡潔用法 if sum := 10; sum < 5 || sum == 10 { fmt.Println("大於5") } else { fmt.Println("小於5") } } ``` ### switch ```go= import ( "fmt" "runtime" ) func main() { fmt.Print("Go runs on ") os := runtime.GOOS switch os { case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: // freebsd, openbsd, // plan9, windows... fmt.Printf("%s.", os) } switch { case os == "darwin": fmt.Println("OS X.") case os == "linux": fmt.Println("Linux.") default: // freebsd, openbsd, // plan9, windows... fmt.Printf("%s.", os) } } ``` --- ## Go func 宣告方式 ---- ### 一般宣告 ```go= func plus(a int, b int) { fmt.Println(a, "+", b, "=", a + b) } // 參數也可以寫成 func plus(a, b int) { fmt.Println(a, "+", b, "=", a + b) } ``` ---- ### 回傳單/多值宣告 ```go= // 回傳單值 func plus(a, b int) int { return a + b } // 回傳多值 func plusAndMulti(a, b int) (int, int) { return a + b, a * b } ``` ---- ### 宣告回傳變數 ```go= // 一開始func 會自動宣告要回傳的變數(所以有預設值), ex. sum, mul // 我們只須指定值給sum & mul, 最後return func plusAndMulti(a, b int) (sum, mul int) { sum = a + b mul = a * b return } ``` ---- ### 傳入數個參數 ```go= // ...int 表示可任意傳入int參數 func sum(nums ...int) (total int) { for _, num := range nums: total += num return } // 呼叫方式 func main() { nums := []int{1, 2, 3, 4} // nums... 自動把陣列裡的值以 sum(1, 2, 3, 4) 的形式傳入呼叫 fmt.Println(nums, ">> total", sum(nums...)) // ---> [1 2 3 4] >> total 10 fmt.Println("[10, 7] >> total", sum(10, 7)) // ---> [10, 7] >> total 17 } ``` ---- ### 回傳func (Closures) ```go= func main() { nextInt := intSeq() fmt.Println(nextInt()) // ---> 1 fmt.Println(nextInt()) // ---> 2 fmt.Println(nextInt()) // ---> 3 } func intSeq() func() int { i := 0 return func() int { i += 1 return i } } ``` --- ## 一般函式庫 (Library) :::warning 有使用過的函式庫,再陸陸續續補上 :memo: ::: ---- ### fmt (顯示在螢幕) ```go= import ("fmt") fmt.Sprint("a", 10) // a10 // 轉成字串並串接 /** * 顯示訊息在終端機 */ fmt.Printf("%g >= %g\n", 10, 7) // 同C語言的printf fmt.Print("hello", "world") // 顯示 helloworld, 不會自動換行, 同JAVA語言的 System.out.print() fmt.Println("hello", "world") // 顯示 hello world, 會自動換行, 同JS語言的console.log ``` ---- ### log (顯示在螢幕加上時間) ```go= import "log" /** * 與 fmt 相似 * 但顯示訊息時, 會多顯示時間 */ log.Println("訊息") // <---- 一般顯示訊息 log.Fatal(error) // <---- 顯示錯誤的Log ``` ---- ### math (數學運算) ```go= import ("math/rand") rand.Intn(10) // 1 ``` ---- ### runtime (與Go系統程序相關) ```go= import "runtime" /** * 顯示作業系統 * "darwin" -> OS X * "linux" -> Linux * "windows" -> Windows * "freebsd", "openbsd", "plan9" */ runtime.GOOS // ; ; "windows" , "freebsd" , "openbsd" runtime.NumCPU() // 顯示CPU數量 runtime.Gosched() // 讓CPU休息 ``` ---- ### os (與command-line相關) ```go= // app.go package main import "os" import "os/exec" import "fmt" func main() { /** * os.Args 會自動取得輸入的所有參數 * ex. go build app.go * ./app aa bb cc 123 * 第一個值 = 執行程式的佔存檔 */ argsWithProg := os.Args argsWithoutProg := os.Args[1:] fmt.Println(argsWithProg) // ---> ["/tmp/go-build....", "aa", "bb", "cc", "123"] fmt.Println(argsWithoutProg) // ---> [aa", "bb", "cc", "123"] // 執行指令的方式 cmd := exec.Command("ls", "-l", "-h") cmdOut, err := cmd.Output() if err != nil { fmt.Println(err) } fmt.Println(string(cmdOut)) } ``` ---- ### flag (取command-line的option參數) ```go= // app.go package main import "flag" import "fmt" func main() { // 宣告選項參數的 Key & 預設值 & 提示訊息 wordPtr := flag.String("word", "foo", "a string") numbPtr := flag.Int("numb", 42, "an int") boolPtr := flag.Bool("fork", false, "a bool") var svar string flag.StringVar(&svar, "svar", "bar", "a string var") // 須先經過 Parse 才算術 flag.Parse() // go build app.go // ./app -word=Hello aa bb cc fmt.Println("word:", *wordPtr) // ---> Hello fmt.Println("numb:", *numbPtr) // ---> 42 fmt.Println("fork:", *boolPtr) // ---> false fmt.Println("svar:", svar) // ---> bar fmt.Println("tail:", flag.Args()) // ---> [aa, bb, cc] // ./app -h or ./app --help // Usage of ./app: // -fork // a bool // -numb int // an int (default 42) // -svar string // a string var (default "bar") // -word string // a string (default "foo") // ./app -sleep // flag provided but not defined: -sleep // Usage of ./app: // -fork // a bool // -numb int // an int (default 42) // -svar string // a string var (default "bar") // -word string // a string (default "foo") } ``` ---- ### time - 計時器 ```go= // 計時器 func main() { t1 := time.NewTimer(time.Second * 3) startTime := time.Now().Unix() fmt.Println("Start") go func() { for i := 0; i < 5; i++ { time.Sleep(time.Second) fmt.Println("second:", i+1) } }() <-t1.C endTime := time.Now().Unix() fmt.Println("Exit", endTime-startTime) } ``` - 字串轉時間 (時戳) [線上範例](https://play.golang.org/p/MhTidZnpp0Q) ```go= // 字串轉時間 func main() { // 時間字串 timeString := "2018-12-31 11:22:33" // 把字串轉成時間物件 t, parseErr := time.Parse("2006-01-02 15:04:05", timeString) if parseErr != nil { log.Fatal(parseErr) } log.Println("時間", t) log.Println("時戳", t.Unix()) } ``` - 取現在時間 [線上範例](https://play.golang.org/p/vHpG8ABFOsM) ```go= // 取現在時間 func main() { // 現在時間 now := time.Now() log.Println("現在時間", t) log.Println("現在時戳", t.Unix()) } ``` - 時戳轉時間 [線上範例](https://play.golang.org/p/i6EQyFqqoEe) ```go= // 時戳轉時間 func main() { // 現在時戳 timestamp := time.Now().Unix() log.Println("現在時戳", timestamp) // 時戳轉時間 now := time.Unix(timestamp, 0) log.Println("現在時間", now) } ``` - 時間格式化 [線上範例](https://play.golang.org/p/4Uu4vIz77RS) **[格式說明](https://golang.org/src/time/format.go)** ```go= // 時間格式化 func main() { // 現在時間 now := time.Now() // 2006-01-02 15:04:04 相當 Y-m-d H:i:s log.Println("現在時間", now.Format("2006-01-02 15:04:05")) } ``` ---- ### Signal ```go= package main import ( "fmt" "os" "os/signal" "syscall" ) func main() { sigs := make(chan os.Signal, 1) done := make(chan bool, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-sigs fmt.Println() fmt.Println(sig) done <- true }() fmt.Println("awaiting signal") <-done fmt.Println("exiting") } ```` --- ## 網路套件庫 (Net) :::info 雖然內建函式庫以包含伺服器功能, 但網路端也是一大工程, 因此開一個新章節來介紹 ::: ---- ### http (伺服器功能) :::warning 如果要建立 HTTPS 伺服器 須建立 **server.key** & **server.crt** ```shell # Generate private key (.key) # Key considerations for algorithm "RSA" ≥ 2048-bit openssl genrsa -out server.key 2048 # Key considerations for algorithm "ECDSA" ≥ secp384r1 # List ECDSA the supported curves (openssl ecparam -list_curves) openssl ecparam -genkey -name secp384r1 -out server.key # Generation of self-signed(x509) public key (PEM-encodings .pem|.crt) based on the private (.key) openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 ``` ::: ```go= package main import ( "fmt" "net/http" ) func main() { // 當瀏覽器輸入根目錄時會呼叫 indexHandler() 涵式 // 第一個參數為route, 第二個參數為callback func // callback func 會自動帶入兩個參數 (同nodejs) // http.ResponseWriter 與 *http.Request http.HandleFunc("/", indexHandler) // 開啟伺服器並監聽Port // HTTP http.ListenAndServe(":8000", nil) // HTTPS http.ListenAndServeTLS(":8000", "server.crt", "server.key", nil) } func indexHandler(res http.ResponseWriter, req *http.Request) { // 取得呼叫方法 fmt.Println("Method: ", req.Method) // 取得呼叫有帶query(參數)的Path fmt.Println("Path: ", req.URL.Path) // 取得呼叫基本Path fmt.Println("Raw Path: ", req.URL.EscapedPath()) // 開啟瀏覽器, 輸入 localost:8000 // 可以看到 Hello World fmt.Fprintln(res, "Hello World") } ``` ---- ### FileSystem Server (檔案伺服器系統) ```go= package main import ( "fmt" "net/http" ) func main() { // 第一個參數為route, 第二個參數為callback func // http.FileServer 建立一個 檔案伺服器 // http.Dir("檔案資料夾路徑") <---- 但不能把字元 ~ 當作家目錄 http.Handle("/", http.FileServer(http.Dir("/home/zuolar/go"))) // 開啟伺服器並監聽Port http.ListenAndServe(":8000", nil) } ``` ---- ### template (樣板功能) ```htmlmixed= <!-- template.gtpl --> <html> <body> <h1>Buy > {{ .Item }} : {{ .Price }}</h1> </body> </html> ``` ```go= /** http.go **/ package main import ( "net/http" "text/template" ) func main() { http.HandleFunc("/", indexHandler) http.ListenAndServe(":8000", nil) } func indexHandler(res http.ResponseWriter, req *http.Request) { // 讀取HTML檔案之後, 建立一個樣板物件 t, _ := template.ParseFiles("template.gtpl") // 定義樣板內的變數資料 {{ .Key }} data := make(map[string]interface{}) data["Item"] = "Milk" data["Price"] = 25 // 將資料自動套入樣板, 並顯示頁面 // 瀏覽器輸入 localhost:8000 // 會顯示 Buy > Milk : 25 t.Execute(res, data) } ``` ---- ### http.Form (表單功能) #### 取參數 :::warning - req.Form["key"] 會取一個陣列資料 - req.Form.Get("key") 與 req.FormValue("key) 會自動取 **req.Form["key"][0]** 的資料 (第一個資料) - req.FormValue 會自動調用 req.ParseForm() ::: ```go= /** http.go **/ package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", indexHandler) http.ListenAndServe(":8000", nil) } func indexHandler(res http.ResponseWriter, req *http.Request) { // 擷取參數資料 req.Form.Get 或 req.Form["Key"] // 但須先解析傳過來的參數 // ex. item = milk , price = 25 req.ParseForm() item1 := req.Form.Get("item") // 回傳 string price1 := req.Form["price"] // 回傳 []string fmt.Println("Parse Form", item1, price1) // Parse Form milk [25] // 也可以用 req.FormValue // 會自動調用 req.ParseForm item = req.FormValue("item") // 回傳 string price = req.FormValue("price") // 回傳 string fmt.Fprintln(res, "Buy > " + item + " : " + price) // Buy > milk : 25 } ``` ---- #### [驗證參數](https://astaxie.gitbooks.io/build-web-application-with-golang/content/zh/04.2.html) ```go= /** http.go **/ package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", indexHandler) http.ListenAndServe(":8000", nil) } func indexHandler(res http.ResponseWriter, req *http.Request) { req.ParseForm() // 判斷指定欄位的資料是否為空陣列 if len(req.Form["item"]) == 0 { fmt.Fprintln(res, "item required") return } item := req.Form.Get("item") price := req.Form.Get("price") fmt.Fprintln(res, "Buy > " + item + " : " + price) // Buy > milk : 25 } ``` ---- ### 預防XSS ```htmlmixed= <!-- template.gtpl --> <html> <body> <h1>Buy > {{ .Item }} : {{ .Price }}</h1> <form action="" method="post"> <div> <label>Item</label> <input type="text" name="item"/> </div> <div> <label>Price</label> <input type="text" name="price"/> </div> <button>OK</button> </form> </body> </html> ``` ```go= /** http.go **/ package main import ( "net/http" "text/template" ) func main() { http.HandleFunc("/", indexHandler) http.ListenAndServe(":8000", nil) } func indexHandler(res http.ResponseWriter, req *http.Request) { // 讀取HTML檔案之後, 建立一個樣板物件 t, _ := template.ParseFiles("template.gtpl") // 定義樣板內的變數資料 {{ .Key }} data := make(map[string]interface{}) data["Item"] = template.HTMLEscapeString(req.FormValue("item")) data["Price"] = template.HTMLEscapeString(req.FormValue("price")) // 瀏覽器輸入 localhost:8000 // Item 輸入 <script>window.alert("OK")</script> // Price 輸入 25 // 會顯示 Buy > <script>window.alert("OK")</script> : 25 // 而不會執行script, 只會顯示純文字 t.Execute(res, data) } ``` ---- ### 預防CSRF ```htmlmixed= <!-- template.gtpl --> <html> <body> <form action="" method="post"> <div> <label>Item</label> <input type="text" name="item"/> </div> <div> <label>Price</label> <input type="text" name="price"/> </div> <input type="hidden" name="token" value="{{.}}" /> <button>OK</button> </form> </body> </html> ``` ```go= package main import ( "crypto/md5" "fmt" "io" "strconv" "text/template" "time" ) func main() { http.HandleFunc("/", indexHandler) http.ListenAndServe(":8000", nil) } func indexHandler() { // 取現在的時間戳 crutime := time.Now().Unix() // 建立一個 md5 的物件 h := md5.New() // 寫入資料, 進行md5編碼 io.WriteString(h, strconv.FormatInt(crutime, 10)) // 取出編碼, 並轉換成字串 token := fmt.Sprintf("%x", h.Sum(nil)) // 讀取HTML檔案之後, 建立一個樣板物件 t, _ := template.ParseFiles("template.gtpl") // 瀏覽器輸入 localhost:8000 t.Execute(res, token) } ``` ---- ### [上傳檔案](https://astaxie.gitbooks.io/build-web-application-with-golang/content/zh/04.5.html) :::warning :memo: 待補 ::: ---- ### Cookie :::success 內建Cookie資料結構 ```go= type Cookie struct { Name string Value string Path string Domain string Expires time.Time RawExpires string // MaxAge=0 means no 'Max-Age' attribute specified. // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' // MaxAge>0 means Max-Age attribute present and given in seconds MaxAge int Secure bool HttpOnly bool Raw string Unparsed []string // Raw text of unparsed attribute-value pairs } ``` ::: ```go= package main import ( "net/http" "time" ) func main() { http.HandleFunc("/", indexHandler) http.ListenAndServe(":8000", nil) } func indexHandler(res http.ResponseWriter, req *http.Request) { // 讀取 Cookie 值 cookieValue, _ := req.Cookie("uid") fmt.Println("cookie", cookieValue) fmt.Println("all cookie", req.Cookies()) // 設置 Cookie 值 expiration := time.Now() expiration = expiration.AddDate(1, 0, 0) cookie := http.Cookie{Name: "uid", Value: "Zuolar", Expires: expiration} http.SetCookie(res, &cookie) } ``` ---- ### Json處理 #### Json Encode ```go= // JSON 編碼 (Encode) package main import ( "encoding/json" "fmt" ) type UserData struct { Name string // 要可以輸出Json的資料Key值需用大寫 email string // 小寫的Key經過Json編碼時, 會自動忽略 } type UserDataArray struct { User []UserData } func main() { // 資料範例(一) : map data := map[string]interface{}{ "Name": "Zuolar", "Age": 23, "Lang": []interface{}{ "Go", "PHP", 219, }, } // json.Marshal 回傳 []byte 跟 error jsonData, err := json.Marshal(data) // 如果 err 不為 nil , 表示編碼有問題 if err != nil { fmt.Println("Json Encode Error") } else { // Json資料為 []byte 型態, 須轉字串才看得懂 output := string(jsonData) fmt.Println(output) // ---> {"Age":23,"Lang":["Go","PHP",219],"Name":"Zuolar"} // ---> {"User":[{"Name":"Zuolar"},{"Name":"Golang"}]} } // 資料範例(二) : struct users := UserDataArray{} users.User = append(users.User, UserData{Name: "Zuolar", email: "yam8511@gmail.com"}) users.User = append(users.User, UserData{Name: "Golang", email: "golang@gmail.com"}) // json.Marshal 回傳 []byte 跟 error jsonData, err = json.Marshal(users) // 如果 err 不為 nil , 表示編碼有問題 if err != nil { fmt.Println("Json Encode Error") } else { // Json資料為 []byte 型態, 須轉字串才看得懂 output := string(jsonData) fmt.Println(output) // ---> {"User":[{"Name":"Zuolar"},{"Name":"Golang"}]} // 會發現 email 不在輸出Json中 } } ``` ---- #### Json Decode ```go= // JSON 解碼 (Decode) package main import ( "encoding/json" "fmt" ) func main() { /** Decode 方法(一) : 解碼 Json 字串 **/ // 模擬一個 Json 字串 jsonString := `{"Age":23,"Lang":["Go","PHP",219],"Name":"Zuolar"}` // 轉成 byte[] 型態 jsonData := []byte(jsonString) // 先宣告一個變數, 用來儲存 json decode 的資料 var data map[string]interface{} // 進行 Json 解碼, 存到變數, json.Unmarshal 會回傳 error 物件 err := json.Unmarshal(jsonData, &data) if err != nil { fmt.Println("Json Decode Error", err) } else { fmt.Println(data) // ---> map[Lang:[Go PHP 219] Name:Zuolar Age:23] } /** Decode 方法(二) : 解碼網路請求的 Json 資料 **/ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { var v map[string]interface{} // 對於 Content-Type: application/json 的請求, 可以直接進行解碼 err := json.NewDecoder(r.Body).Decode(&v) if err != nil { fmt.Println("Json Decode Error", err) } else { fmt.Println(data) } } } ``` ---- ### [Curl](http://cepave.com/http-restful-api-with-golang/) :::warning :memo: 待補 **ps. 請看 Postman 的 Code** ::: --- ## 推薦資料庫套件 (Gorm) **有中文!** 官方文檔 - http://gorm.io/zh_CN/docs/index.html --- ## 原生資料庫套件 (MySQL) :::info 以下用MySQL示範 :boom: 須先安裝MySQL套件 ```shell $ go get github.com/go-sql-driver/mysql ``` ::: ---- ### 連線設定 ```go= package main import ( "database/sql" _ "github.com/go-sql-driver/mysql" ) func connect(driver string, database string, username string, password string) (*sql.DB, error) { // "root:qwe123@/go?charset=utf8" info := username + ":" + password + "@/" + database + "?charset=utf8" // 建立資料庫連線 db, err := sql.Open(driver, info) return db, err } func main() { // 建立連線 db, err := connect("mysql", "go", "root", "qwe123") // 於結束前, 關閉連線 defer db.Close() // 進行資料庫操作... } ``` ---- ### 異動資料 (新增/修改/刪除) ```go= package main import ( "database/sql" _ "github.com/go-sql-driver/mysql" ) func connect(driver string, database string, username string, password string) (*sql.DB, error) { // "root:qwe123@/go?charset=utf8" info := username + ":" + password + "@/" + database + "?charset=utf8" // 建立資料庫連線 db, err := sql.Open(driver, info) return db, err } func main() { // 建立連線 db, err := connect("mysql", "go", "root", "qwe123") // 於結束前, 關閉連線 defer db.Close() // 進行資料庫操作... // db.Prepare 建立一個SQL Query, ? 代表變數 query, err := db.Prepare("INSERT userinfo SET username=?,departname=?,created=?") // query.Exec 執行SQL, 自動將資料依序帶入 ? res, err := query.Exec("Zuolar", "RD", "1994-02-19") // res.LastInsertId 可以取得最新插入的ID id, err := res.LastInsertId() // res.RowsAffected 可以取得影響行數 affect, err := res.RowsAffected() } ``` ---- ### 查詢資料 ```go= package main import ( "database/sql" _ "github.com/go-sql-driver/mysql" ) func connect(driver string, database string, username string, password string) (*sql.DB, error) { // "root:qwe123@/go?charset=utf8" info := username + ":" + password + "@/" + database + "?charset=utf8" // 建立資料庫連線 db, err := sql.Open(driver, info) return db, err } func main() { // 建立連線 db, err := connect("mysql", "go", "root", "qwe123") // 於結束前, 關閉連線 defer db.Close() // 進行資料庫操作... // db.Query 進行查詢的動作, 但不異動資料 rows, err := db.Query("SELECT * FROM userinfo") // rows.Next 依序迭代 SELECT 的資料 for rows.Next() { var uid int var username string var department string var created string // rows.Scan 丟入變數, 取得相對應資料 (依照欄位順序) err = rows.Scan(&uid, &username, &department, &created) fmt.Println("uid: ", uid) fmt.Println("username: ", username) fmt.Println("department: ", department) fmt.Println("created: ", created) } } ``` --- ### 客製化機制 - 安全資料庫連線操作 ```go= package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) func main() { dbHandle(func(db *sql.DB) { rows, err := db.Query("SELECT * FROM userinfo") if err != nil { panic(err) } // rows.Next 依序迭代 SELECT 的資料 for rows.Next() { var uid int var username string var department string var created string // rows.Scan 丟入變數, 取得相對應資料 (依照欄位順序) err = rows.Scan(&uid, &username, &department, &created) if err != nil { panic(err) } fmt.Println("uid:", uid) fmt.Println("username:", username) fmt.Println("department:", department) fmt.Println("created:", created) } }) } /** 連線資料庫 **/ func connect(driver string, database string, username string, password string) (*sql.DB, error) { // "root:a7319779@/go?charset=utf8" info := username + ":" + password + "@/" + database + "?charset=utf8" db, err := sql.Open(driver, info) return db, err } /** 資料庫操作, 結束後自動關閉連線 **/ func dbHandle(handle func(*sql.DB)) { // 如果最後有錯誤, 顯示錯誤 defer func() { if err := recover(); err != nil { fmt.Println("Error: ", err) } }() // 建立連線 db, err := connect("mysql", "go", "root", "a7319779") // 於結束前, 關閉連線 defer db.Close() // 連線有錯誤時, 丟出錯誤 if err != nil { panic(err) } // 主要處理邏輯 handle(db) } ``` --- ## WebSocket (即時通訊) :::info 先下載 go - socket.io 套件 ```shell= go get github.com/googollee/go-socket.io ``` ::: ---- ### Server端 ```go= package main import ( "" "net/http" socket "github.com/googollee/go-socket.io" ) func main() { server, err := socket.NewServer(nil) if err != nil { log.Fatal(err) } server.On("connection", socketConnHandler) // 設定 Socket 發生錯誤時, 需進行的CallBack server.On("error", socketErrorHandler) } func socketErrorHandler(so socket.Socket, err error) { log.Fatal(err) } func socketConnHandler(so socket.Socket) { log.Println("on connection") // 加入Socket通道 (類似群組) so.Join("chat") // 監聽事件, 接受訊息 so.On("chat message", func(msg string) { log.Println("emit:", so.Emit("chat message", msg)) so.BroadcastTo("chat", "chat message", msg) }) // Socket.io acknowledgement example // The return type may vary depending on whether you will return // For this example it is "string" type so.On("chat message with ack", func(msg string) string { return msg + " by Zuolar" }) // 中斷連線 so.On("disconnection", func() { log.Println("on disconnect") }) } ``` ---- ### Client端 ```htmlmixed= <!doctype html> <html> <head> <title>Socket.IO chat</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font: 13px Helvetica, Arial; } form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; } form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; } form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; } #messages { list-style-type: none; margin: 0; padding: 0; } #messages li { padding: 5px 10px; } #messages li:nth-child(odd) { background: #eee; } </style> </head> <body> <ul id="messages"></ul> <form action=""> <input id="m" autocomplete="off" /><button>Send</button> </form> <script src="/socket.io-1.3.7.js"></script> <script src="/jquery-1.11.1.js"></script> <script> var socket = io(); $('form').submit(function(){ socket.emit('chat message with ack', $('#m').val(), function(data){ $('#messages').append($('<li>').text('ACK CALLBACK: ' + data)); }); socket.emit('chat message', $('#m').val()); $('#m').val(''); return false; }); socket.on('chat message', function(msg){ $('#messages').append($('<li>').text(msg)); }); </script> </body> </html> ``` --- ## 單元測試 https://openhome.cc/Gossip/Go/Testing.html --- ## 效能分析 :::warning 需先安裝 Graphviz **$ apt-get install graphviz** ::: :::success - go1.11以後提供內建的PProf介面,以下說明就可忽略 ```shell= # go tool pprof -http=[host:port] [binary] [pprof url] go tool pprof -http=:8080 ./pepper http://127.0.0.1:8000/debug/pprof/heap ``` ::: - 產生pprof檔案 ```shell # 產生pprof檔案 # go tool pprof -raw <url> go tool pprof -raw -inuse_space http://127.0.0.1/debug/pprof/heap Fetching profile over HTTP from http://127.0.0.1/debug/pprof/heap Saved profile in /Users/zoular/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz PeriodType: space bytes .... ``` - 產生線圖 ```shell # 產生線圖 # go tool pprof -svg <*.pb.gz> > heap.svg $ go tool pprof -svg pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz > heap.svg # or # go tool pprof -svg <url> > heap.svg $ go tool pprof -svg http://127.0.0.1/debug/pprof/heap > heap.svg ``` - 產生火焰圖 ```shell= # 產生火焰圖 # go-torch <*.pb.gz> $ go-torch pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz INFO[15:04:35] Run pprof command: go tool pprof -raw -seconds 30 pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz INFO[15:04:36] Writing svg to torch.svg ``` --- ## [GoMobile](https://github.com/golang/go/wiki/Mobile#building-and-deploying-to-android-1) ### Quick Start (Android) 1. [安裝Android Studio](https://developer.android.com/studio/index.html) 執行檔: **/usr/local/android-studio/bin/studio.sh** 因為需要SDK管理套件 2. [安裝NDK (Native Development Kit)](https://developer.android.com/ndk/guides/index.html) gomobile 才能順利產出APK檔 3. 初始化 gombile ```shell $ gomobile init -ndk ~/Android/Sdk/ndk-bundle/ ``` 4. 下載範例 ```shell $ go get -d golang.org/x/mobile/example/basic ``` 5. 編譯檔案 ```shell $ gomobile build -target=android golang.org/x/mobile/example/basic ``` 6. 安裝到手機上 :::warning 用USB線連接電腦與手機, 開啟開發人員模式 ::: ```shell $ gomobile install golang.org/x/mobile/example/basic ``` ---- ### [Matcha](https://github.com/gomatcha/matcha) (目前只能在MacOS開發) --- ## [輕量化檔案](http://s.itho.me/day/2017/gopher/06_Execution_Mode_David_Chou.html#/) --- ## 常用套件 --- ### [Excelize - Go 语言 (golang) Excel 文档基础库](https://studygolang.com/articles/27831) ### [Toml Config](https://github.com/BurntSushi/toml) :::success 安裝套件 ```shell go get github.com/BurntSushi/toml # 可以用來驗證 toml 語法有無正確 go get github.com/BurntSushi/toml/cmd/tomlv tomlv some-toml-file.toml ``` ::: ---- ### [Telegram Bot](https://github.com/tucnak/telebot) :::success 安裝套件 ```shell go get -u gopkg.in/tucnak/telebot.v2 ``` ::: ---- #### 範例 ```go= package main import ( "time" "log" tb "gopkg.in/tucnak/telebot.v2" ) func main() { b, err := tb.NewBot(tb.Settings{ Token: "TOKEN_HERE", Poller: &tb.LongPoller{Timeout: 10 * time.Second}, }) if err != nil { log.Fatal(err) return } b.Handle("/hello", func(m *tb.Message) { b.Send(m.Sender, "hello world") }) b.Start() } ``` --- ## GOGC=off 關閉GC | GODEBUG 偵錯訊息 [GODEBUG之gctrace干货解析](https://zhuanlan.zhihu.com/p/73183820) [用 GODEBUG 看调度跟踪](https://segmentfault.com/a/1190000020108079) [Golang Official GEDEBUG](https://golang.org/pkg/runtime/) ---- 1. `inittrace` 可以檢視 `init` 的順序。 **Go >= 1.16** 2. `gctrace` 可以查看`GC`事件紀錄 3. `GOGC=off`關閉GC後,可以用`runtime.GC()`手動觸發。 指令 ```shell GOGC=off GODEBUG=inittrace=1,gctrace=1,schedtrace=1000 go run . ``` --- ## Go 1.16 Embed 內嵌靜態資源筆記 [golang1.16内嵌静态资源指南](https://www.cnblogs.com/apocelipes/p/13907858.html) [Go 1.16 推出 Embedding Files](https://blog.wu-boy.com/2020/12/embedding-files-in-go-1-16/) [1.13 Go 应用内存占用太多,让排查?(VSZ篇)](https://eddycjy.gitbook.io/golang/di-1-ke-za-tan/why-vsz-large) ---- 1. `//go:embed` [OK] , `// go:embed` [FAIL] 2. 📝 go:embed only global variables 3. 📝 go:embed only one variable per line, except embed.FS 5. 📝 if use `*` will include `.file` or `_file` 6. 📝 if use no `*` will exclude `.file` or `_file` 7. 📝 `VSZ` will big! but no worry. 8. ✅ 會自動過濾重複「內容」的檔案,不會加倍載入 4. ✅ embed file no need memory, but add into binary disk size 9. 👿 如果使用`embed.FS`,記憶體好像會佔用原檔案大小的兩倍! >*** 適合用於單一檔案,非必要不使用`embed.FS`** >以下Benchmark顯示,如果用於網頁靜態頁面資料夾,使用`embed.FS`比原本`ioutil.ReadFile`好 >[color=red] - VSZ 代表虛擬記憶體的使用量 - RSS 代表實體記憶體的使用量 Code ```go= import "embed" //go:embed hello.txt var b []byte //go:embed hello* .gitignore //go:embed go* var f embed.FS ``` >embed * 4 MEM VSZ RSS 0.0 6420684 (6G) 2188 (2K) 1.4G binary >embed * 1 MEM VSZ RSS 0.0 5335024 (5G) 2124 (2K) 355M binary >embed * 0 --> mean `import _ "embed"` MEM VSZ RSS 0.0 4973316 2152 2.1M binary >no embed MEM VSZ RSS 0.0 4973316 2148 2.1M binary ### Benchmark - 大檔案 MB等級 ```go= package main import ( "embed" "io/ioutil" "testing" ) //go:embed data/big.png var big []byte //go:embed data/big.png var fs embed.FS func Benchmark_embed(t *testing.B) { for i := 0; i < t.N; i++ { _ = big } } func Benchmark_fs_dir(t *testing.B) { for i := 0; i < t.N; i++ { fs.ReadFile("data/big.png") } } func Benchmark_fs_file(t *testing.B) { for i := 0; i < t.N; i++ { fs.ReadFile("data/big.png") } } func Benchmark_read_file(t *testing.B) { for i := 0; i < t.N; i++ { ioutil.ReadFile("data/big.png") } } ``` ```shell goos: darwin goarch: amd64 pkg: gopro cpu: Intel(R) Core(TM) i5-7267U CPU @ 3.10GHz Benchmark_embed-4 1000000000 0.3221 ns/op 0 B/op 0 allocs/op Benchmark_fs_dir-4 1 1673365686 ns/op 740786192 B/op 2 allocs/op Benchmark_fs_file-4 10 289954438 ns/op 740786230 B/op 2 allocs/op Benchmark_read_file-4 7 149943087 ns/op 740786504 B/op 5 allocs/op PASS coverage: 0.0% of statements ok gopro 8.487s ``` ### Benchmark - web頁面打包實例 (只適合用 embed.FS) ```go= package main import ( "embed" "io/ioutil" "testing" ) //go:embed dist var fs embed.FS func Benchmark_fs_dir(t *testing.B) { for i := 0; i < t.N; i++ { fs.ReadFile("dist/index.html") } } func Benchmark_read_file(t *testing.B) { for i := 0; i < t.N; i++ { ioutil.ReadFile("data/index.html") } } ``` ```shell goos: darwin goarch: amd64 pkg: gopro cpu: Intel(R) Core(TM) i5-7267U CPU @ 3.10GHz Benchmark_fs_dir-4 3148616 429.5 ns/op 912 B/op 2 allocs/op Benchmark_read_file-4 314264 4190 ns/op 64 B/op 2 allocs/op PASS coverage: 0.0% of statements ok gopro 4.879s ``` ---