# 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
```
---