---
# System prepended metadata

title: Golang 自學筆記
tags: [自學筆記系列]

---

---
tags: 自學筆記系列
---
# Golang 自學筆記

## 參考教學
使用golang打造web應用程式
https://willh.gitbook.io/build-web-application-with-golang-zhtw/

https://ithelp.ithome.com.tw/articles/10155678

echo document
https://echo.labstack.com/guide/customization/

redis document
https://redis.uptrace.dev/guide/go-redis.html#installation

redis 教學
https://www.tizi365.com/archives/304.html

gRPC document
https://grpc.io/docs/languages/go/quickstart/

gRPC 影片
https://www.youtube.com/watch?v=8FnZbiZCdxA&list=PLo0iJFLQIBEYOnAgZx-fjNB5eEai6A3lw

## 創建檔案
使用vscode中的"開啟資料夾"的方式，並創建檔案

解決"gopls was not able to find modules in your workspace."問題

具體原因由是因為 Go 1.13 之後預設開啟 go modules, 因此 go 會試圖從 go.mod 尋找模組的路徑，由於我們少了 go.mod 造成編譯錯誤，解決方法為創建自己的go module即可

1. 在vscode中ctrl+shift+P後，輸入
> Go: Initialize go.mod

![](https://hackmd.io/_uploads/HkUdnyow2.png)

2. 輸入 "main" 後enter即可
![](https://hackmd.io/_uploads/ryxla21sw3.png)

## package概念
* 參考資料
    https://learnku.com/go/t/32464
    https://home.gamer.com.tw/artwork.php?sn=5463024

* package所包含的函式可以拆分成不同檔案，但通常會將這些檔案放在與package名稱相同的資料夾下
* 包含在package main下的程式通常會帶有一個main()函式，是整個程式的起始點；同一個資料夾下一定只能有一個main()函式
* go mod應該要被放在最外層，要引入package時所需輸入的路徑，都是相對於go mod所在位置(最前面加上go mode名)

* 一個含有多個package的go專案架構如下，資料夾名稱盡量與pageage name相同
![](https://hackmd.io/_uploads/rk1NA86u2.png)
![](https://hackmd.io/_uploads/HJhiRUad2.png)

* 深入探討各程式的實作方式
    ![](https://hackmd.io/_uploads/Byj0eDpdh.png)
    main.go中要引入其他package時，import那邊寫的位置是
    > go mod名字/package所在資料夾相對於go mod的路徑

    ex: myPackage/calculator
    myPackage是go mod名字，calculator是package calculator相對於go mod所在位置的路徑

## 輸入與輸出
範例
```go
package main //此檔案在main這個package中

import "fmt" //引用別人寫好的標準package

func main() { //只支援右括，且 package main中只能恰有一個main函式
	a := ""      //使用簡短宣告
	fmt.Scan(&a) //使用標準package中的Scan函式輸入變數並存到變數a中(輸入Jason)

	//使用使用標準package中的Print系列函式輸出
    
        //接受一或多個參數，並依序輸出至stdout中(以空白隔開)
	fmt.Print("Hallo, World1\n")        
    
        //支援格式化字符串(%s等)，並依序輸出至stdout中(以空白隔開)
	fmt.Printf("%s, Hallo World2\n", a)
	fmt.Println(a, "Hallo World3!")     //類似Print，會在結尾自動加上換行

	s := fmt.Sprintf("%s, Hallo World4!", a) //Sprintf系列會輸出成字串
	fmt.Println(s)
}

//輸入:
// Jason

//輸出:
// Hallo, World1
// Jason, Hallo World2
// Jason Hallo World3!
// Jason, Hallo World4!

```

## 變數
* 宣告的區域變數一定要使用，不然編譯不會過
* 變數的型態可以為
    1. int,int8,int16,int32(rune),int64
    2. uint,uint8(byte),uint16,uint32,uint64
    3. float32,float64
    4. bool
    5. string
    6. rune 可以當作字元來用(範圍為unicode)

* 不同類型的變數不可以直接賦值或運算
* 若沒有初始化，則編譯器會自動初始化成0

變數宣告範例
```go
package main

import "fmt"

//var 通常用於宣告全域變數，但其實也可以宣告成區域變數
var a int                   //基本格式是 var 變數名 變數型態
var b = 2                   //宣告的同時初始化(可以加或不加變數名)
var c, d float32            //同時宣告多個變數
var e, f float32 = 3.2, 6.8 //同時宣告多個變數，並在宣告的同時初始化
const PI = 3.14159          //常數，把var替換成const即可
const NAME = "Jason"

func main() {
	g := 9 //宣告區域變數的方式(簡短宣告，一定要在宣告時同時賦值)，一定要使用不然會跳錯
	fmt.Println(a, b, c, d, e, f, g)
	fmt.Println(PI, NAME)
}
```

## 字串處理
* go 中的字串可以使用 "" 或 \`\` 來包住，型別都是string
* 不可以像C++一樣直接改字串內的某個位置

範例
```go
package main

import "fmt"

func main() {
	str1 := "apple" //宣告方法如同一般變數
	fmt.Println(str1)

	//str1[0] = 'c' //字串宣告後不可以直接改裡面的值
	str1 = "c" + str1[1:] //可以透過切片並重新賦值的方式修改

	fmt.Println(str1)
}
```

## 陣列
* 陣列宣告後長度固定
* 把一個陣列作為參數傳入函式的時候，是call by value

範例
```go
package main

import "fmt"

//var arr1 [10]int //基本格式是 var 陣列名 [陣列大小]陣列型態 (全域變數的宣告方式())
var arr1 = [10]int{1, 2, 3} //可以在宣告時部分初始化

func main() {
	arr1[0] = 5 //跟C++一樣是0-index，賦值操作同C++
	fmt.Println("arr1= ", arr1)
    fmt.Println(arr1[0:2]) // 支援切片操作

	arr2 := [10]int{} //簡短宣告，同樣只能用在區域變數中
	//arr2 := [10]int{1, 2, 3} //可以在宣告時部分初始化
	fmt.Println("arr2= ", arr2)

	arr3 := [2][3]int{{1, 3, 5}, {2, 4, 6}} //也可以宣告成多維陣列
	fmt.Println("arr2= ", arr3)
}

```

## slice(動態陣列)
* slice在宣告時不需要給定長度
* 確切地來說，slice的底層是reference一個陣列
![](https://hackmd.io/_uploads/HkrgpCpwn.png)
* slice賦值的來源可以是array或是另一個slice
* append函式會改變 slice 所參考的陣列的內容，從而影響到參考同一陣列的其它slice。 但當 slice 中沒有剩餘空間（即(cap-len) == 0）時，此時將動態分配新的陣列空間。回傳的 slice 陣列指標將指向這個空間，而原陣列的內容將保持不變；其它參考此陣列的 slice 則不受影響。

範例
```go
package main

import "fmt"

var s1 []int //用var宣告一個slice，不用指定大小，也可以不用初始化

func main() {
	arr := [5]int{1, 2, 3, 4, 5}
	s1 = arr[0:3] //從arr中切出0-2三個位置賦值給s1(注意: s1跟arr共用記憶體位址)
	fmt.Println(s1)

        //也可以用簡短賦值宣告，從s1中切出1-s1最後一個元素給s2(注意: s1跟s2也共用同記憶體位址)
	s2 := s1[1:]
	fmt.Println(s2)

        //這樣就可以當成C++ vector來用了
        //(make可以用來創建slice,map，第二個參數用來指定slice的大小)
	s3 := make([]int, 0)
	s3 = append(s3, 9)   //具有append功能
	s3 = append(s3, 2)
	s3 = append(s3, 5)
	fmt.Println(s3)
	fmt.Println("len=", len(s3)) //可以用len()來取得目前大小

	s4 := make([]int, len(s3))
	copy(s4, s3) //從s3中複製一份給s4，此時s3跟s4不共用相同的記憶體位址
	s3[0] = -1
	fmt.Println(s3, s4)
}

```

## map
* 底層是hash_table(代表無序)，同樣有key跟value，其中key可以不為整數

範例
```go
package main

import "fmt"

var mp1 map[string]int //使用var宣告的map初始值為nil，必須使用make()對其初始化才可使用

func main() {
	mp1 = make(map[string]int) //使用make初始化
	mp1["apple"] = 1           //賦值
	mp1["apple"] = 3           //針對已存在的key，可以直接修改
	temp1 := mp1["apple"]      //透過[]存取map中的內容(若不存在，則會回傳該型態的零值)
	fmt.Println(temp1)
    
        //透過[]存取map中的內容時，其實會有兩個回傳值，其中第二個回傳值可以用來判斷是否具有該key
	temp2, ok := mp1["cat"]
	fmt.Println(temp2, ok)

	mp2 := make(map[string]int) //也可以直接使用make來初始化
	mp2["hi"] = 1
	mp2["hallo"] = 2
	fmt.Println("len:", len(mp2)) //可以使用len，會回傳map當前有多少不童的key
	delete(mp2, "hallo")          //可以使用delete刪除map裡面的某個key
	fmt.Println("len:", len(mp2))

	//mp1=mp2 //注意，像這樣的賦值方式兩者會共用記憶體空間，可以用迴圈一一複製來避免這種情況

}

```

## if-else,switch
* if-else使用方法類似C++，不同之處有:
    1. 只能用右括格式安排大括號
    2. if 中的條件可以不加小括號

* 與C++不同的是，switch中預設都會加一個default，不用自己加

範例
```go
package main

import "fmt"

func main() {
	temp := 0
	fmt.Scan(&temp)

	if temp < 0 {
		fmt.Println("less than 0")
	} else if temp >= 0 && temp < 5 {
		fmt.Println("between 0-5")
	} else {
		fmt.Println("equal or larger than 5")
	}

	switch temp {
	case 1, 2, 3, 4, 5: //支援聚合逾法，意義是"如果temp等於1-5其中一個"
		fmt.Println("1-5")
		fallthrough //這個關鍵字會強制執行後面所有case中的程式碼(不含default)
	case 10:
		fmt.Println("equal 10")
	default:
		fmt.Println("NONE")
	}
	//輸出:
	//1-5
	//equal 10
}

```

## Loop
* golang中沒有while迴圈，將其功能並到for迴圈中了
* 跟if-else類似，for 中的條件可以不加小括號

範例
```go
package main

import "fmt"

func main() {
	//同C++，for迴圈的規則為:
	// for 初始式;條件式;本輪結束後執行的內容 {}

	for i := 0; i < 10; i++ {
		fmt.Println("i=", i)
	}

	cnt := 0
	for cnt < 10 { //模擬while() 迴圈
		fmt.Println("cnt=", cnt)
		cnt++
	}

	counter := 0
	for { // 模擬while(1) 無窮迴圈
		if counter >= 10 {
			break //continue也能用
		}
		fmt.Println("counter=", counter)
		counter++
	}

	mp := make(map[string]int)
	mp["apple"] = 1
	mp["banana"] = 2

	for key, value := range mp { //使用range關鍵字，可以遍歷slice 跟map
		fmt.Println(key, value)
	}
}
```

## function
* 格式:
    > func 函式名 (輸入的參數) (回傳的參數) {}
* 與C++不同的是，回傳值型態是寫在後面的，且允許多個回傳值；若沒有回傳值，直接省略即可
* channel，slice，map本來就是以call by reference的方式傳入的，而其他的一般變數(含struct)則是call by value(可以傳入其記憶體位址，並用對應的指標接以達到call by reference的效果)
* 新增了defer語法，會在離開這個函式前呼叫；在一個函式中，可以有多個defer 的statement，離開前會倒序執行
* function可以當成變數，並當成參數傳入其他函式
* 不可有相同名字的函式(作為struct的method除外)

範例:
```go
package main

import "fmt"

func Max(a, b int) int { //單個回傳值的函式，注意回傳值型態是寫在後面
	if a >= b {
		return a
	} else {
		return b
	}
}

//可以命名回傳值，這樣一來return就不用寫東西了
//示範以假的call by reference方式傳入
func Min(a, b *int) (ans int) {
	if *a <= *b {
		ans = *a
	} else {
		ans = *b
	}
	return
}

func MinMax(a, b int) (min, max int) { //多個回傳值的函式
	if a <= b {
		min = a
		max = b
	} else {
		min = b
		max = a
	}
	return
	//return a,b //如果不命名回傳值，又想一次回傳多個變數，可以寫成這樣
}

//可以使函式傳入不定數量的參數(以這個例子來說，會將所有的參數都存入型態為int的slice)
func f1(input ...int) {
	//input是一個slice，可以使用for range 遍歷，"_"代表讀了即丟的變數，用來暫存讀到的index
	for _, value := range input {
		fmt.Println(value)
	}
}

func f2() { //defer語法
	defer fmt.Println("A")
	defer fmt.Println("B")
	defer fmt.Println("C")
	fmt.Println("f2")
}

//輸出:
//f2
//C
//B
//A

func main() {
	a, b := 0, 0
	fmt.Scan(&a, &b)
	fmt.Println("max=", Max(a, b))
	fmt.Println("min=", Min(&a, &b))
	temp1, temp2 := MinMax(a, b)
	fmt.Println("Max=", temp2, "Min=", temp1)
	f1(1, 3, 5, 7, 9)
	f2()
}

```

## struct
* 跟C++的使用方法非常像
* 欄位分成一般欄位跟匿名欄位(繼承的概念)

範例:
```go
package main

import "fmt"

type human struct {
	name string
	age  int
}

type student struct {
	sid   string
	human //匿名欄位，這樣寫即預設student擁有human的所有欄位
	int   //匿名欄位也可以是內建的
}

var Jason human //以var宣告的方式使用struct

func main() {
	Jason.name = "Jason"
	Jason.age = 20
	fmt.Println(Jason)

	JasonY := human{} //以簡短宣告的方式使用struct
	//JasonY := human{name: "JasonY", age: 21} //也可以在宣告的時候直接初始化
	JasonY.name = "JasonY"
	JasonY.age = 21
	fmt.Println(JasonY)

	Denial := student{}
    
        //可以用類似constructor的方式賦值給匿名欄位
	Denial.human = human{name: "Denial", age: 19}
	Denial.age = 19                               //也可以直接存取匿名欄位中的一分子
	Denial.int = 1314                             //賦值給內建型態的匿名欄位
	Denial.sid = "5403"                           //賦值給一般欄位
	fmt.Println(Denial)
}

```

## method
* 實做了C++ 中物件導向的概念。其概念是將一個函式定義在某個struct底下；換句話說，讓某個struct "接收" 這個函式，並作為這個struct的一種method
* 格式:
    > func (別名 接收者) 函式名 (輸入參數) (回傳參數) {}
* 注意: 這裡的接收者是call by value，所以這樣寫並沒辦法有效的改到本身
    ```go
    func (r retangle) edit(input int) {
	    r.edge = input //r是原struct的副本
    }
    ```
* 使用指標作為別名可以解決上述問題
    ```go
    func (r *retangle) edit(input int) {
	r.edge = input
	//(*r).edge = input //相同意思，go允許用上述方法進一步簡化
    }
    ```
* 除了作用在struct上面以外，method還可以作用在任何自訂的型別(用類似typedef的方法)、內建型別上面
* method是可以用匿名欄位的方式繼承給其他struct的；類似的，某個struct繼承下來後，也可以redifition這個method

範例:
```go
package main

import "fmt"

type retangle struct {
	edge int
}

func (r retangle) area() int {
	return r.edge * r.edge
}
func (r *retangle) edit(input int) { //接收者必須加上*，才能真正改到本身
	r.edge = input
	//(*r).edge = input //相同意思，go允許用上述方法進一步簡化
}

func main() {
	a := retangle{edge: 10}
	fmt.Println(a.area())
	a.edit(1)
	fmt.Println(a)
}

```

## interface
* 目的是實現C++的多型(polymorphism)，讓一個slice中能存放"實際為不同實作方法，但都擁有相同模板函式"的不同struct型態
* interface為多個抽象method所組成的集合(之所以說是抽象，是因為interface中的method是仰賴實際struct中的method定義方式)
* 當一個struct至少擁有所有"interface中擁有的method"時，稱這個struct"實現"了該interface(注意: 函式名跟參數數量都要一模一樣才能被算進去)；此時，該struct將被視為一種interface型態

範例:
```go
package main

import "fmt"

type dog struct {
	age int
}
type cat struct {
	age int
}

func (d dog) speak() {
	fmt.Println("Wolf!")
}
func (d dog) eat() {
	fmt.Println("Wolf! yummy")
}

func (c cat) speak() {
	fmt.Println("Meow!")
}
func (d cat) eat() {
	fmt.Println("Meow! yummy")
}

type aninal interface { //宣告了一個名為aninal的interface，其中包含兩個特定格式的函式
	speak() //此時的函式是抽象的，實際使用時會根據當前放入的struct來決定這個函式的實際定義
	eat()
}

func f(input aninal) {
	input.eat()
}

func main() {
	arr := make([]aninal, 0)       //宣告了一個aninal型態的slice
	arr = append(arr, dog{age: 7}) //只有實現了該interface的那些struct才能被加進去
	arr = append(arr, cat{age: 3})
	arr[0].speak() //輸出Wolf!
	arr[1].speak() //輸出Meow!
	//如此一來在arr中，同時可以使用dog及cat的函式

	//f的輸入參數為animal型態，因為dog跟cat都實現了animal，故可以正常轉換
	f(arr[0])
	f(arr[1])

	//此時的arr[0]可以理解為，他其實是dog型態
	//但被animal這個interface限制住了，只能用interface有的函式互動

	//interface中無法直接存取實際的變數，必須透過accesser()存取(例如寫一個getage())
	//arr[0].age = 10

	//interface{}代表"空interface"，因為所有的型態都實現了空interface
	//故空interface可以代表"任何型態"
	//若將其作為函式的參數型態，則代表這個函式接受任意型別的值作為參數
	temp := make([]interface{}, 0)
	temp = append(temp, 1)
	temp = append(temp, "abc")
	temp = append(temp, dog{age: 1})
	fmt.Println(temp)
}
```

## goroutine
* golang的核心之一，可以用來平行處理(類似fork())。
* goroutine就像執行緒，但規模比執行緒更小，十幾個 goroutine可能體現在底層就是五六個執行緒而已
* 可以用以下語法將這個函式交由goroutine執行
    >go 函式
* 注意: 當主程式結束後，所有的goroutine也隨之一併消失(不管有沒有執行完)，這是與fork()不同之處
* 可以使用channel在主程式與分出來的goroutine間傳遞資訊，channel被建出來後每個人都可以使用它發送與接收資訊
範例:
```go
package main

import "fmt"

func getSum(arr []int, c chan int) {
	sum := 0
	for _, val := range arr { //將arr中的元素相加
		sum += val
	}

	//將sum的值寫進channel中
	//因為buffer只有1，故goroutine這邊會等到寫進去的值確實讀到後才會繼續
	c <- sum
}
func main() {
	array := []int{1, 2, 3, 4, 5, 6}
	arr := array[:]

	//只能使用make()來創建channel，這邊創建的是buffer為0的channel
	//這邊的buffer指的是"造成阻塞的channel元素個數下限"
	//若channel內的元素已經是這個數字，再寫入一個值後將造成阻塞
	c := make(chan int)
	//c := make(chan int,指定buffer) //可以指定要多少buffer

	//以下兩個goroutine共用同一個channel
        //將getSum交給新的goroutine執行，並使用channel連結主程式跟此goroutine
	go getSum(arr[0:3], c)
    
        //將getSum交給新的goroutine執行，並使用channel連結主程式跟此goroutine
	go getSum(arr[3:6], c)

	x, y := <-c, <-c //主程式這邊會等待從channel中順利讀出資料後才繼續
	fmt.Println(x, y)

	//其他操作: 使用range遍歷channel，該迴圈會不斷讀取直到該channel被關閉
	// for i := range c {
	// 	fmt.Println(i)
	// }

	//其他操作: 關閉channel，關閉後便無法發送資料至channel了
	//但仍可能有殘留的資料待讀取(強烈建議應該由發送方關閉，而非接收方)
	//不應隨便關閉channel，通常是當想要明確的結束 range 迴圈時才會用到
	//close(c)

	//其他操作: 接收方確認channel是否已被關閉
	//temp,OK := <- c //若OK為false，則代表channel沒有任何資料且已被關閉
}

```

## select
* select的用法與switch有點像，但是只能接channel；會隨機從已滿足的case中挑出一個執行，執行完後才會離開select區域

範例:
```go
package main

import (
	"fmt"
	"time"
)

func f(c, quit chan int) {
	for i := 0; i < 5; i++ {
		time.Sleep(time.Second * 1)
		c <- i //將i當前的值丟到channel c中
	}
	//quit <- 1 //將1丟到channel quit中
}

func main() {
	c := make(chan int)
	quit := make(chan int)

	go f(c, quit) //使用另一個goroutine執行f，並以兩個channel作為通訊

	for { //無窮迴圈，因為有多個 channel 需要讀取，而讀取需不間斷
		select { //從諸多case中挑一個當前可行的來執行
		case x := <-c: //若當前可以從channel c中讀出一個元素，本次select執行這個
			fmt.Println("receive", x)
		case <-quit: //若當前可以從channel quit中讀出一個元素，本次select執行這個
			fmt.Println("end")
			return
		case <-time.After(time.Second * 5):
                //當卡在這個select超過五秒時，便會進入到這個case中
			fmt.Println("timeout!")
			return
			// default: //若以上case皆不滿足，則執行這個
			// 	fmt.Println("default")
		}
	}
}

```

## log
```go
package main

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"

	"github.com/sirupsen/logrus"
)

type MyFormatter struct{}

func (m *MyFormatter) Format(entry *logrus.Entry) ([]byte, error) {
	var b *bytes.Buffer
	if entry.Buffer != nil {
		b = entry.Buffer
	} else {
		b = &bytes.Buffer{}
	}

	timestamp := entry.Time.Format("2006-01-02 15:04:05")

	var logLevel string
	switch entry.Level {
	case logrus.DebugLevel:
		logLevel = "\033[1;35mDEBUG\033[0m" // 使用紫色上色
	case logrus.InfoLevel:
		logLevel = "\033[1;32mINFO\033[0m" // 使用綠色上色
	case logrus.WarnLevel:
		logLevel = "\033[1;33mWARN\033[0m" // 使用黃色上色
	case logrus.ErrorLevel:
		logLevel = "\033[1;31mERROR\033[0m" // 使用紅色上色
	case logrus.FatalLevel:
		logLevel = "\033[1;31mFATAL\033[0m" // 使用紅色上色
	case logrus.PanicLevel:
		logLevel = "\033[1;31mPANIC\033[0m" // 使用紅色上色
	default:
		logLevel = fmt.Sprintf("[%s]", entry.Level)
	}

	var newLog string

	//HasCaller()為true才會有調用信息
	if entry.HasCaller() {
		fName := filepath.Base(entry.Caller.File)
		newLog = fmt.Sprintf("[%s][%s][%s:%d] %s\n",
			logLevel, timestamp, fName, entry.Caller.Line, entry.Message)
	} else {
		newLog = fmt.Sprintf("[%s][%s] %s\n", logLevel, timestamp, entry.Message)
	}

	b.WriteString(newLog)
	return b.Bytes(), nil
}

func initLogger() *logrus.Logger {
	// 創建一個新的 logrus 實例
	logger := logrus.New()

	// 設定 logrus 日誌紀錄格式
	logger.SetFormatter(&MyFormatter{})

	// 設定 logrus 輸出位置為 os.Stderr (終端輸出)
	logger.SetOutput(os.Stderr)

	// 設定報告呼叫函式的行數
	logger.SetReportCaller(true)

	return logger
}

func main() {
	// 初始化 logrus 日誌紀錄
	log := initLogger()

	// 使用不同的函式來記錄不同日誌等級的訊息
	log.Info("CCUCSIE Plus is running")
	log.Warn("Some warning occurred")
	log.Error("Some error occurred")
	log.Debug("Debug message")
	log.Fatal("A fatal error occurred")

	// 手動觸發致命錯誤，以模擬非正常結束
	os.Exit(1)
}
```

## json 處理
### 將資料打包成 json
```go
// 準備回傳給客戶端的資料
returnValue := map[string]interface{}{
	"SQL_cmd": SQL_cmd,
	"amount":  ans,
}

// 將回傳資料轉換成JSON格式，jsonData 是 []byte 型態，已打包好的 JSON 資料
jsonData, err := json.Marshal(returnValue)
if err != nil {
	log.Error("轉換 JSON 時發生錯誤:", err)
}
```
### 解析伺服器回傳的 json response
```go
// 發送 HTTP request，範例網址
url := fmt.Sprintf("http://localhost:8000?size=%d", multiple)
response, err := http.Get(url)
if err != nil {
	log.Fatal(err)
}
defer response.Body.Close()

if response.StatusCode == http.StatusOK {
	var Datas interface{} // 定義一個空的 interface{}，用來接回傳的 JSON 字串
	err := json.NewDecoder(response.Body).Decode(&Datas)
	if err != nil {
		log.Fatal(err)
	}

	// 此時 Datas 是 interface{} 型態，實際存的是字串
	myString, ok := Datas.(string) // 將 interface{} 轉換為字串
	if !ok {
		log.Fatal("convert to string failed")
	}

	// 解析 JSON 字串，存至 []map[string]interface{} 中
	var rawPoints []map[string]interface{}
	err = json.Unmarshal([]byte(myString), &rawPoints)
	if err != nil {
		fmt.Println("解析 JSON 時發生錯誤:", err)
		return
	}
}
```

註: 如果出現錯誤，試著把 Datas 也改成 []map[string]interface{} 型態


## 讀取 env 參數
* 有時候，一些程式中的重要參數會拉出來另外寫成 .env 這個檔案
* golang 提供了一種超方便的方法快速讀取這些參數

* ex:
	* 檔案架構，執行的程式是 main.go
	![image](https://hackmd.io/_uploads/rkLN_Jw8a.png)
	* .env 檔案內容
	![image](https://hackmd.io/_uploads/SkeddJDUT.png)


依賴模組
```go
import (
	"os"
	
	"github.com/joho/godotenv"
)
```

使用方式:
```go
err := godotenv.Load(".env") //參數是.env 檔案相對於程式的路徑
if err != nil {
	log.Error("無法載入 .env 檔案")
}

os.Getenv("user") // 透過這種方式讀取
```

## web 處理表單的輸入(基礎)
以下的程式碼在運行時的執行順序如下:
1. 在開始監聽前，使用http.HandleFunc註冊Handler，Handler function的傳入參數須為固定格式
2. 使用http.ListenAndServe在指定的阜監聽請求
3. 客戶端發起請求至網站的跟目錄，server呼叫Handler function "login"來處理
4. 在login function中，使用r.ParseForm()解析請求，使用r.Method得知請求的方法；接著使用template.ParseFiles解析server端的login.gtpl檔案成模板，解析完畢後使用t.Execute回傳給使用者看；如此一來，使用者便看到可以輸入帳密的介面
5. 使用者在表單中輸入完帳密，按下送出(使用POST方法)，此時因為login.gptl中的設定，會將請求轉送至"/home"這個url中，並因為先前已經註冊好Handler function，故會呼叫home這個函式處理
6. 在home function中，同樣先解析請求，並確定請求的方法；接著利用r.Form取的表單中的資料，並回傳給使用者，於是使用者便能看到剛剛輸入的帳密
```go
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

// w是將要回傳給客戶端的內容，r是客戶端送給伺服器的請求
// 此函式是一個Handler，用來處理請求，並產生回傳資訊
func login(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()                    //解析客戶端傳來的請求
	fmt.Println("method:", r.Method) //r.Method可以得知請求的方法(GET,POST等等)
	if r.Method == "GET" {
		//解析login.gtpl，並將其結果回傳給t(第二個參數是錯誤處理用)
		//t是一個指向已解析的模板的指標
		t, _ := template.ParseFiles("login.gtpl")
        
                //將解析後的模板寫入至w中，回傳給使用者看，第二個參數是放要動態顯示的內容
		t.Execute(w, nil)
	}
}

// 表單中的這行: <form action="/home" method="post">
// 指定了要送將資料送給後端的/home
// 經由註冊的url handler，變能順利交由此函式處理
func home(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()                    //解析客戶端傳來的請求
	fmt.Println("method:", r.Method) //r.Method可以得知請求的方法(GET,POST等等)
    
        //<form action="/home" method="post"> 中可以用method指定請求的方法
	if r.Method == "POST" {          
		//r.Form是一個map，存有客戶端在表單內輸入的內容
		//"username"對應至.gtpl中 <input type="text" name="username"> 的name
		fmt.Fprintln(w, "username:", r.Form["username"])
		fmt.Fprintln(w, "password:", r.Form["password"])
	}
}

func main() {
	http.HandleFunc("/", login)              //註冊對應url的handler function
	http.HandleFunc("/home", home)           //註冊對應url的handler function
	err := http.ListenAndServe(":8000", nil) //設定在 http://localhost:8000/ 監聽請求
	if err != nil {
		fmt.Println(err)
	}
}
```

## http.request
* golang 中的http.request可以使用多種方法解析，詳情見程式碼註解

範例:
```go
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

func login(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	if r.Method == "GET" {
		t, _ := template.ParseFiles("login.gtpl")
		t.Execute(w, nil)
	} else if r.Method == "POST" {
		//r.URL系列操作可以取得跟URL有關的資訊
		fmt.Fprintf(w, "----------URL data----------\n")
		fmt.Fprintf(w, "URL: %s\n", r.URL.String())  //回傳完整的 URL 字串
		fmt.Fprintf(w, "Scheme: %s\n", r.URL.Scheme) //回傳URL 的協議部(例如https)
        fmt.Fprintf(w, "Host: %s\n", r.URL.Host)     //回傳url的主機名(ex:localhost)
		fmt.Fprintf(w, "Path: %s\n", r.URL.Path)     //回傳客戶端請求送到的路徑
        
                //r.URL.Query()會回傳一個map，存了所有跟URL有關的資訊
		for key, value := range r.URL.Query() {      
			fmt.Fprintf(w, "Key: %s, Value: %s\n", key, value)
		}

		//r.Header系列操作可以取得並修改跟request Header有關的資訊
		fmt.Fprintf(w, "----------Header data----------\n")
        
                //取得指定key的值(只回傳第一個)
		fmt.Fprintf(w, "[Get] Origin value: %s\n", r.Header.Get("Origin"))       
        
                //取得指定key的值(直接回傳slice)
		fmt.Fprintf(w, "[Values] Origin value: %s\n", r.Header.Values("Origin")) 
        
                //將指定欄位修改成傳入的value，若不存在則創建一個
		r.Header.Set("Origin", "The_Value0")              
        
                //創建一個指定欄位，並賦予其值
		r.Header.Add("Origin", "The_Value1")                                   
        
                //刪除所有的指定欄位及其值
		r.Header.Del("Origin")                                                   

                //r.Header的資料結構近似於 map[string][]string，可以遍歷
		fmt.Fprintf(w, "\nAll information:\n") 
		for key, values := range r.Header {
			fmt.Fprintf(w, "Key: %s Value: ", key)
			fmt.Fprint(w, values)
			fmt.Fprintf(w, "\n")
		}

		fmt.Fprintf(w, "----------RemoteAddr data----------\n")
        
                //回傳客戶端的IP位址
		fmt.Fprintf(w, "client IP address: %s\n", r.RemoteAddr)

		//在經過r.ParseForm()解析後，可以存取從客戶端傳過來的表單資訊
		fmt.Fprintf(w, "----------Form data----------\n")
        
                //取得指定key的值(只回傳第一個)
		fmt.Fprintf(w, "[Get] username: %s\n", r.Form.Get("username"))
        
                //將指定欄位修改成傳入的value，若不存在則創建一個
		r.Form.Set("username", "Daniel")         
        
                //在指定欄位後append 傳入的value，若不存在則創建一個
		r.Form.Add("username", "Daniel")                               
        
                //刪除所有的指定欄位及其值
		r.Form.Del("username")
                      

                //r.Form的資料結構近似於map[string][]string，，可以遍歷
		fmt.Fprintf(w, "\nAll information:\n")
		for key, values := range r.Form {
			fmt.Fprintf(w, "Key: %s Value: ", key)
			fmt.Fprint(w, values)
			fmt.Fprintf(w, "\n")
		}

		fmt.Fprintf(w, "----------Cookie----------\n\n")

		fmt.Fprintf(w, "----------user input----------\n")
		fmt.Fprintln(w, "username:", r.Form["username"])
		fmt.Fprintln(w, "password:", r.Form["password"])
	}
}

func main() {
	http.HandleFunc("/", login)
	err := http.ListenAndServe(":8000", nil)
	if err != nil {
		println("error!")
	}
}

```

(執行結果，沒截到user input的部分)
![](https://hackmd.io/_uploads/SyJClsEuh.png)

## http.ReponseWritter
* 可以操作要回傳給客戶端的封包內容，例如修改header內容、回傳網站給客戶端、回傳文字給客戶端等等
```go
package main

import (
	"fmt"
	"html/template"
	"net/http"
)

func login(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	if r.Method == "GET" {
		t, _ := template.ParseFiles("login.gtpl") //將server端網頁回傳給客戶端的方法
		t.Execute(w, nil)
	} else if r.Method == "POST" {
        
                //設置回應狀態碼(例如404)等，不會直接影響客戶端顯示的內容
		w.WriteHeader(http.StatusNotFound)
        
                //將指定欄位修改成傳入的value，若不存在則創建一個
		w.Header().Set("Content-Type", "application/json")
        
                //在指定欄位後append 傳入的value，若不存在則創建一個
		w.Header().Add("Content-Type", "hi")          
        
                //刪除所有的指定欄位及其值
		w.Header().Del("Content-Type")                     
        
                //直接寫入文字內容給客戶端的方法
		fmt.Fprintf(w, "username: %s\n", r.Form["username"])
		fmt.Fprintf(w, "password: %s\n", r.Form["password"])
	}
}

func main() {
	http.HandleFunc("/", login)
	http.ListenAndServe(":8000", nil)
}

```

## 各種前端Form形式
* 以下是各種form形式的使用方法與在後端的體現
```html
<html>
<head>
<title></title>
</head>
<body>
<!-- action 是這個表單按下送出後會跳轉至的url，method 是送出表單給後端的方法-->
<!--需上傳檔案時，form的屬性需加上 enctype="multipart/form-data"-->
<form action="/" method="post" enctype="multipart/form-data">
    <div>
        <!--單行的文字輸入框-->
        <label>帳號:</label>
        <input type="text" name="username">
    </div>
    <div>
        <!--單行的文字輸入框(密碼版本)-->
        <label>密碼:</label>
        <input type="password" name="password">
    </div>
    <div>
        <!--單選表單，name相同且type="radio"的所有input框框皆會被視為是同一個單選題-->
        <!--選擇某個選項(<input>)後，回傳給後端的值將會是該選項中value的值-->
        <label>性別:</label>
        <input type="radio" name="gender" value="male">
        <label>男</label>
        <input type="radio" name="gender" value="female">
        <label>女</label>
        <input type="radio" name="gender" value="other">
        <label>其他</label>

        <!--補充，如果這樣設計，點擊選項旁的文字也可以有效果
        
        <input type="radio" name="gender" id="Male" value="male">
        <label for="Male">男</label>
        -->
    </div>
    <div>
        <!--下拉式選單，可以用select+option來實作-->
        <!--選擇某個option後，回傳給後端的值將會是該option中value的值-->
        <label>最高學歷</label>
        <select name="school">
            <option value="elementary-high-school">小學或以下</option>
            <option value="junior-high-school">國中</option>
            <option value="high-school">高中</option>
            <option value="college">大學</option>
            <option value="graduate School">研究所</option>
        </select>
    </div>
    <div>
        <!--多選表單，name相同且type="checkbox"的所有input框框皆會被視為是同一個多選題-->
        <!--選擇某些選項(<input>)後，回傳給後端的值將會是這些選項中value的值，存在slice中-->
        <label>喜歡的食物:</label>
        <input type="checkbox" name="food" value="apple">
        <label>蘋果</label>
        <input type="checkbox" name="food" value="grape">
        <label>葡萄</label>
        <input type="checkbox" name="food" value="banana">
        <label>香蕉</label>
        
    </div>
    <div>
        <!--多行文字框的表單-->
        <label>備註:</label><br>
        <textarea name="note"></textarea>
    </div>
    <div>
        <!--檔案上傳欄位的表單-->
        <label>上傳檔案</label>
        <input type="file" name="upfile">
    </div>
    <button type="submit">送出</button>
    <button type="reset">清空</button>
    
</form>
</body>
</html>
```
![](https://hackmd.io/_uploads/BkqtRKPu3.png =50%x)
![](https://hackmd.io/_uploads/ryXCIbrun.png)

## echo 框架
* Golang的Echo是一個開源的輕量級Web框架，具有高效、簡潔、擴展性佳等優點

基本架構的範例
```go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4" //引入echo框架
)

// echo.Context中存放了request跟response
// Echo的Handler function 預設會回傳一個error變數，存放錯誤訊息
func home(c echo.Context) error {
    
        //回傳字串形式的response跟status code給客戶端
	return c.String(http.StatusOK, "Hello, World!")
}

func main() {
	e := echo.New()  //建立一個Echo的物件
	e.GET("/", home) //設定當客戶端請求的url為"/"且方法為"GET"時的Handler function

	//e.Logger是Echo物件的日誌記錄器
	//Fatal()是該紀錄器中的一個method
	//當括號內的程式發生錯誤時，Fatal() method會記住錯誤訊息並強制終止程式
	e.Logger.Fatal(e.Start(":8000")) //啟動伺服器，並設定在8000阜監聽客戶端請求
}

```
## echo---routing
* routing的概念就像是python django中的url.py檔案，作用是當客戶端針對某個url發送了一個請求後，routing便會根據之前註冊的內容跳轉到對應的Handler function
* routing path 的Match-any功能:
    >e.GET("/user/*", login)
    
    "*" 可以代表路徑中的0或更多字元，故這樣寫會使以下url與之匹配
    1. /user/
    2. /users/hi/
    3. /users/hi/files/
    
    用法類似的還可以將"*" 替換成":id"，可以將:id代表的那部分網址解析出來(:id 稱作 path parameter)
    
範例:
```go
package main

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

func login1(c echo.Context) error {
	return c.String(http.StatusOK, "login1")
}

func login2(c echo.Context) error {
	return c.String(http.StatusOK, "login2")
}

func login3(c echo.Context) error {
	return c.String(http.StatusOK, "login3")
}

func main() {
	e := echo.New()

	//註冊Handler function
	//參數意義: (url路徑 , Handler function)
	e.GET("/", login1) //註冊當使用GET方法向"/"這個url送請求時，會呼叫login函式處理
	//e.POST("/", login1) //註冊當使用POST方法向"/"這個url送請求時，會呼叫login函式處理
	//e.Any("/", login1) //註冊當使用任何方法向"/"這個url送請求時，會呼叫login函式處理

	//命名這段註冊關係
	//e.GET("/", login1).Name = "first_GET"

	//當輸入的網址同時匹配多個route時，優先配對順序跟程式碼順序無關，只跟url格式有關
	//換句話說，"Routes can be written in any order"
	e.GET("/order/*", login3)     //3rd (使用"*"替代0或更多個字元)
	e.GET("/order/:id", login2)   //2nd (使用":id"替代0或更多個字元，並可被解析)
	e.GET("/order/first", login1) //1st (寫出完整路徑)

	e.Start(":8000")
}
```

## echo---request
* 若c是echo.Context物件，則c.Request()會回傳 *http.Request 物件，便可利用先前提到的各種方法提取資料等等；但要記得打上c.Request().ParseForm()解析一下
* 以下著重介紹echo框架獨有的方法，欲達成相同目的也可利用c.Request()取得*http.Request 物件再進行操作

範例:
```go
package main

import (
	"fmt"
	"io/ioutil"
	"net/http"

	"github.com/labstack/echo/v4"
)

func login(c echo.Context) error {
	if c.Request().Method == "GET" {
		//----------quary parameter----------
		//quary parameter 是使用get方法送出表單資料時，在網址後的"?"附上的參數
		//ex: http://localhost:8000/?username=Jason&password=asdf
        
                //可以使用這個方法取得username(找不到回傳空字串)
		name := c.QueryParam("username")
		if name != "" {
			return c.String(http.StatusOK, "hello, "+name)
			//當輸入網址: http://localhost:8000/?username=Jason 時
			//會跳轉到不同介面
		}

		//----------path parameter----------
		//path parameter是路徑中帶有的可變變數(:id)
		path_id := c.Param("id")
		if path_id != "" {
			return c.String(http.StatusOK, "path_id: "+path_id)
			//當輸入網址: http://localhost:8000/Jason 時
			//會跳轉到不同介面
		}

		return c.File("login.html")
	} else if c.Request().Method == "POST" {
		res := fmt.Sprintln("----------get form value----------")
		//使用echo 內建的函式的話，會自動解析資料，不用手動打上ParseForm()
        
                //回傳name="food"的表單資料的第一項(string)
		res += fmt.Sprintln("food:", c.FormValue("food"))

		//c.FormParams()會回傳兩個參數，分別是r.Form(已解析完畢)跟error
		//r.Form的資料結構近似於map[string][]string，，可以遍歷
		val, _ := c.FormParams()
        
                //回傳name="food"的表單資料(slice)全部
		res += fmt.Sprintln("food:", val["food"])

		res += fmt.Sprintln("----------get txt file----------")
		//從Formfile中取得客戶端上傳的檔案
		//傳入的參數是表單中對應的name，回傳multipart.FileHeader的指標
		fp, _ := c.FormFile("upfile")
		file, _ := fp.Open()            //開檔
		data, _ := ioutil.ReadAll(file) //讀檔
		content := string(data)         //將data []byte資料格式轉型成string
		res += fmt.Sprintln(content)    //寫進回傳給客戶端的string中

		return c.String(http.StatusOK, res)
	}
	return c.String(http.StatusOK, "error")
}

func main() {
	e := echo.New()
	e.GET("/", login)
	e.GET("/:id", login) //帶有一個parameter :id
	e.POST("/", login)
	e.Start(":8000")
}


```

## echo---response
* 若c是echo.Context物件，則c.Response()會回傳 http.ResponseWriter，便可利用先前提到的各種方法處理回傳的資料
* 可以回傳給客戶端字串、網站、可下載的檔案等

範例:
```go
package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/labstack/echo/v4"
)

type person struct {
	Name string `json:"username"` //變數一定要大寫，才能被順利export
	Age  int    `json:"userage"`  //後面的引號區域是struct標籤
	//在將stutct轉化成json格式時，會參考裡面的內容重新命名(可以不加)
}

func root(c echo.Context) error {
	//回傳字串格式的訊息給使用者(類似用Fprintf的方式寫給http.ResponseWriter)
	//參數: (狀態碼，字串內容)
	return c.String(http.StatusOK, "you are in \"/\"")
}
func html(c echo.Context) error {
	//將字串解讀成HTML後，回傳給使用者該網頁
	//參數: (狀態碼，HTML字串內容)
	return c.HTML(http.StatusOK, "<h1>you are in Html</h1>")
}
func json(c echo.Context) error {
	//回傳給使用者json，通常傳入struct轉換
	//參數: (狀態碼，struct)
	//若json過大，推薦使用沒框架版本的方法回傳json
	a := person{Name: "Jason", Age: 20}
	return c.JSON(http.StatusOK, a)
	//return c.JSONPretty(http.StatusOK, a, "  ") //回傳排版過的json
	//return c.JSONBlob(http.StatusOK, encodedJSON) //第二個參數是已經處理好的json

	//與之同系列的還有回傳XML的操作
}
func file(c echo.Context) error {
	//回傳server端的檔案給客戶端(.html等等都可以)
	//只有一個參數，代表檔案的路徑
	return c.File("main.go")
}

func attachment(c echo.Context) error {
	//與file類似，但是會直接讓客戶端下載該檔案，而非顯示給客戶端看
	//參數: (檔案的路徑，給客戶端的檔名)
	return c.Attachment("main.go", "the_file")
}
func inline(c echo.Context) error {
	//類似c.File，但會將檔案以內嵌格式回傳給客戶端，不須額外下載及打開
	//參數: (檔案的路徑，內嵌名)
	return c.Inline("main.go", "Inline")
}
func stream(c echo.Context) error {
	//可以傳送任意資料流的回應
	//參數: (狀態碼，資料流解析格式，資料流來源)
	f, _ := os.Open("cacti.png")
	return c.Stream(http.StatusOK, "image/png", f)
}
func redirect(c echo.Context) error {
	//將客戶端重新導向至指定的url
	//參數: (狀態碼:，指定的url)
	//http.StatusMovedPermanently代表請求的資源已經被永久移動到了一個新的位置
	return c.Redirect(http.StatusMovedPermanently, "/")
}

func before_hook()  { fmt.Println("before hook") }
func after_hook_1() { fmt.Println("after hook_1") }
func after_hook_2() { fmt.Println("after hook_2") }
func hook(c echo.Context) error {
	//hook概念: 可以註冊一系列的函式，在送出response前一刻及後一刻自動觸發
	//有效範為只在這個函式中(別的函式送出response不會觸發)
    
        //註冊before_hook函式，會在在送出response前一刻自動觸發
	c.Response().Before(before_hook)
    
        //註冊after_hook_1函式，會在在送出response後一刻自動觸發
	c.Response().After(after_hook_1)
    
        //可以同時註冊多個，按照註冊時的順序執行
	c.Response().After(after_hook_2)

	fmt.Println("hook function start")
	return c.String(http.StatusOK, "you are in \"/hook\"")

	//輸出:
	// hook function start
	// before hook
	// after hook_1
	// after hook_2
}

func main() {
	e := echo.New()

	e.GET("/", root)
	e.GET("/html", html)
	e.GET("/json", json)
	e.GET("/file", file)
	e.GET("/attachment", attachment)
	e.GET("/inline", inline)
	e.GET("/stream", stream)
	e.GET("/redirect", redirect)
	e.GET("/hook", hook)

	e.Start(":8000")

}
```

## echo---load static data
(v2版本新增內容)
後端常常有許多靜態資料(ex: css 圖片)，echo提供了一個方便的功能，不需要動到html中請求的url，便能順利找到資料

* 觀念釐清: 瀏覽器如何向後端要資料?
    假設瀏覽器現在的拜訪的網址是http://localhost:8000/home/hallo
    這時後端回傳的html中，有一行要求載入static資料的程式
    > \<link rel="stylesheet" href="static/css/common/styles.css" />

    這時，瀏覽器便會向以下這個網址發送一個"請求static資料" 的request
    > http://localhost:8000/home/static/css/common/styles.css


* 使用方式:
    > func (*echo.Echo).Static(pathPrefix string, fsRoot string) *echo.Route
    * pathPrefix: 請求的Prefix url
    * fsRoot: 欲替換成的url string

註: echo.Static只會作用在"請求static資料"的request url，其他請求不會被替換


舉個實際的例子
1. 假設使用者輸入 http://localhost:8000/article/CSES_overview 這個網址，後端會回傳"CSES_overview.html"這個網頁；且這個網頁會請求放在static資料夾中的資料，並使用以下方法請求
    > \<link rel="stylesheet" href="static/css/common/styles.css" />

    ![](https://hackmd.io/_uploads/ryRcXGlq2.png)

2. 使用者的瀏覽器會嘗試向後端的這個網址發送請求取得css
    > http://localhost:8000/article/static/css/common/styles.css 

3. 後端經過echo的Static設定，將請求轉化成
    > http://localhost:8000/static/css/common/styles.css
    原理是將"/article/static" 直接替換成/static
    
    ![](https://hackmd.io/_uploads/Hyhi7Gg93.png)

如此一來便能找到位於"static\css\common\styles.css"的檔案了

### 極重要 static data 的另一種理解
上面的那種方法本地端能正常運作，但放到伺服器上後出現找不到靜態資源的問題，以下提供解決方法，但原理跟上面的衝突，可能上面寫錯了，還待驗證。這邊提供另一種方式來使用echo框架載入靜態資料

main.go設定載入css的方式，注意最前方有沒有斜線
(這邊註解掉下面那行的原因有可能是因為只有外層(不含article資料夾)才有載入static data 的html程式碼，也可能是因為e.Static()最多只能存在一個)
![](https://hackmd.io/_uploads/H1oHgobqn.png)

html 中載入靜態資料的link寫法，注意最前方的斜線，已確定會影響資料能不能成功被載入
![](https://hackmd.io/_uploads/r1oLboWqh.png)

## echo---template_basic
* template的概念是"根據傳入的資料動態生成html"
* 包含template語法的.html檔案需要先被解析(Parse)，接著將資料填入這個被解析過的template中(此步驟稱為渲染(Render))，才能回傳給客戶端看
* 以下程式碼的流程控制是:
    1. 創建一個templates變數，型別是map。其中，key為string型別，代表解析後的template名字；value為*template.Template型別，代表解析後的template。因為可能有很多解析後的template，故使用map的資料結後妥善保存
    2. 使用template.Must(template.ParseFiles("home.html"))來解析位於home.html這個路徑的檔案，並將解析後的結果存在templates中，key為My_home_template；日後便是利用這個key來找到解析後的結果。
    3. e.Renderer = &TemplateRegistry{ templates: templates,} 是用來連結 echo 框架跟剛剛存好的一系列解析後的template的；之後使用echo框架中的Render方法時，echo框架便會間接透過之前定義好的渲染方式完成渲染
    4. 啟動server，客戶端送出請求至根目錄中，routing的對應處理函式為home()
    5. 創建一個map[string]interface{}{}型態的變數，這個變數目的是用來存渲染所需的傳入資料；接著使用key-value的格式定義要渲染template的資料
    6. 使用c.Render()這個method渲染好模板，並回傳給使用者；在第3步中已經連結好TemplateRegistry 跟echo框架了，故echo框架會去找TemplateRegistry的Render() method，來看看渲染的規則。
    7. TemplateRegistry的Render() method中，會先透過傳進來的name找出要使用哪個已解析的模板，接著使用ExecuteTemplate()這個method將資料寫進"Home_HTML"這個指定的區域，這個區域就是在home.html中被{{define "Home_HTML"}}及{{end}}所包含的區域

main.go
```go
package main

import (
	"html/template"
	"io"
	"net/http"

	"github.com/labstack/echo/v4"
)

// 固定用法: 宣告TemplateRegistry這個struct，等等需定義.Render()這個method
type TemplateRegistry struct {
	templates map[string]*template.Template //用來存"解析完畢的template"的map
}

// TemplateRegistry的一個method，描述了模板實際的渲染方式
func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{},
                                  c echo.Context) error {
	tmpl, _ := t.templates[name] //指定要選擇哪個"解析完畢的template"

	//將資料帶入Home_HTML這個區域中渲染
	return tmpl.ExecuteTemplate(w, "Home_HTML", data)
}

// handler function
func home(c echo.Context) error {
	temp := map[string]interface{}{} //用map儲存渲染所需的資料
	temp["name"] = "Jason"           //寫入渲染所需的資料
    
        //ex: 將html中的 {{index . "say"}} 替換成Hello, world
	temp["say"] = "Hello, world"
	temp["trash"] = "duck"           //寫入模板沒用到的data不影響執行

	//使用Render() method回傳給客戶端渲染後的html
	//參數意義: (狀態碼，要使用哪個解析後的template，要代入的資料)
	return c.Render(http.StatusOK, "My_home_template", temp)
}

func main() {
	e := echo.New()

    
        //宣告用來存"解析完畢的template"的map
	templates := make(map[string]*template.Template)

	//解析在home.html這個路徑上的檔案，並將解析後的結果重新命名為My_home_template
	templates["My_home_template"] = template.Must(template.ParseFiles("home.html"))
	e.Renderer = &TemplateRegistry{ //連結echo框架跟TemplateRegistry struct
		templates: templates,
	}

	e.GET("/", home)

	e.Start(":8000")
}
```

home.html
```html
<!--定義Home_HTML 這個指定的區域-->
{{define "Home_HTML"}}
  <!DOCTYPE html>
  <html>
    <head>
      <title>home</title>
    </head>
    <body>
        <!--在Home_HTML這個區域中，定義幾個待渲染的變數-->
        <p>name: {{index . "name"}}</p> <!--後端 temp["name"] 中的值會渲染至此-->
        <p>say: {{index . "say"}}</p>
        <p>murmur: {{index . "murmur"}}</p> <!--找不到後端傳來的資訊則不會顯示-->>
    </body>
  </html>
{{end}}
```

## echo---template_advance
* 示範了如何同時使用不同的已解析template
* 示範了巢狀template(.html中拿了另一個.html當作template，兩個檔案須放在一起解析)

main.go
```go
package main

import (
	"html/template"
	"io"
	"net/http"

	"github.com/labstack/echo/v4"
)

// 固定用法: 宣告TemplateRegistry這個struct，等等需定義.Render()這個method
type TemplateRegistry struct {
	templates map[string]*template.Template
}

// TemplateRegistry的一個method，描述了模板實際的渲染方式
func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{},
                                  c echo.Context) error {
	return t.templates[name].ExecuteTemplate(w, name, data)
}

func home(c echo.Context) error {
	temp := map[string]interface{}{}
	temp["position"] = "Jason's website"
	return c.Render(http.StatusOK, "Home", temp)
}

func about(c echo.Context) error {
	temp := map[string]interface{}{}
	temp["position"] = "about"

	//message.html中待渲染的變數可以直接寫入
	//因為已經解析完畢，將兩個.html檔融合了
	temp["thing"] = "my life"
	return c.Render(http.StatusOK, "About", temp)
}

func main() {
	e := echo.New()

	templates := make(map[string]*template.Template)
	templates["Home"] = template.Must(template.ParseFiles("home.html"))

	//將第二個解析結果重新命名成"About" (可以同時拿多個.html一起解析)
	//以這個例子來說，會將about.html中的 {{template "Message" .}}
	//取代成message.html中的Message區塊
	templates["About"] = template.Must(template.ParseFiles("about.html",
                                                           "message.html"))
	e.Renderer = &TemplateRegistry{
		templates: templates,
	}

	e.GET("/", home)
	e.GET("/about", about)
	e.Start(":8000")
}
```

home.html
```html
<!--定義Home_HTML 這個指定的區域-->
{{define "Home"}}
  <!DOCTYPE html>
  <html>
    <head>
      <title>home</title>
    </head>
    <body>
        <!--在Home_HTML這個區域中，定義幾個待渲染的變數-->
        <h1>welcome to {{index . "position"}}</h1>
    </body>
  </html>
{{end}}
```

about.html
```html
<!--定義Home_HTML 這個指定的區域-->
{{define "About"}}
  <!DOCTYPE html>
  <html>
    <head>
      <title>about</title>
    </head>
    <body>
        <h1>welcome to {{index . "position"}}</h1>
        {{template "Message" .}}
    </body>
  </html>
{{end}}
```

message.html
```html
{{define "Message"}}
<p>there is something about {{index . "thing"}}</p>
{{end}}
```
## echo 打造https server
將 e.Start(":8000") 改成 e.StartTLS(":8000", "cert.pem", "key.pem") 即可

```go
package main

import (
	"github.com/labstack/echo/v4"
)

func home(c echo.Context) error {
	return c.File("home.html")
}

func main() {
	e := echo.New()
	e.Static("/static", "static") //註冊靜態檔案路徑

	e.GET("/", home) //註冊當使用GET方法向"/"這個url送請求時，會呼叫login函式處理

	e.StartTLS(":8000", "cert.pem", "key.pem")
}
```

其中，cert.pem 跟 key.pem 是https憑證所需資料，與程式置於同一個資料夾中
![image](https://hackmd.io/_uploads/rkRsCg1Ha.png)

### 生成憑證
1. 使用 golang 生成憑證 (生成一個未被驗證的憑證測試用)
	* windows 系統
		```
		go run "C:\Program Files\Go\src\crypto\tls\generate_cert.go" --host localhost
		
		# C:\Program Files 是 Go 的安裝目錄
		# --host localhost 指定憑證的伺服器名稱
		```
	* linux 系統
		```
		go run /usr/local/go/src/crypto/tls/generate_cert.go -host 165.227.194.243
		```


## go redis
* redis是一個資料庫系統，特色是將資料存在記憶體而非硬碟中，以加速讀寫過程，必要時再寫入硬碟中即可；電腦關機後，未及時寫入硬碟中的資料將會遺失

* 在開始執行前，先下載Redis-x64-3.0.504.msi並安裝 https://vocus.cc/article/625a7da6fd89780001a2aa2a

* 電腦開啟後，redis server將會在localhost:6379等待請求
* redis server在默認情況下會自動使用預設的資料庫，但可以透過指令更改
* redis database中的資料是以key-value的形式儲存的，Key需為字串型態，而value可以根據不同的目的而有不同形態；無論key對應的value型態是甚麼，key值在整個database中永遠是唯一的
* redis中傳入欲儲存的value是interface{}型態，意味著任何型態的變數皆可被傳入儲存；但放進database中時會自動傳換成String型態
* 針對rdb下了如.Set()等指令後，會回傳類似*redis.StatusCmd型態的物件；這個物件可以用.Err() 和 .Result()來解析
    1. .Err(): 回傳執行指令過程中發生的錯誤，若沒有則為nil
    2. .Result(): 回傳執行指令後的結果及過程中發生的錯誤

## redis---string
* rdb.Set(),rdb.Get()可以用來操作database中value型別為string的資料
* rdb.Del() 可以用來刪除指定key的所有資料
* rdb.Expire()可以用來設定該key的刪除時間

範例:
```go
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9" //引入go redis所需套件
)

func main() {
	//配置連結至資料庫所需的參數
	options := redis.Options{
		Addr:     "localhost:6379", //資料庫所在位址
		Password: "",               // 密碼
		DB:       0,                // 使用的database，0代表預設的database
	}

	//建立程式與資料庫的連結，rdb便是連結的橋樑
	rdb := redis.NewClient(&options)
	ctx := context.Background() //建立一個一個空的背景上下文，可以做平行處理時的溝通之用

	//使用Set() method來加入資料進database中
	//若該key已存在則覆蓋，否則新創一個key-value對
	//參數意義: (固定參數，key值，value值，刪除時間(0代表永不刪除))
	err := rdb.Set(ctx, "name", "Jason", 0).Err()
	if err != nil {
		fmt.Println("insert fail")
	}

	rdb.Set(ctx, "age", "20", time.Second*15) //這筆key-value對將在15秒後刪除
	//rdb.Set(ctx, "weight", "59", 0)

	//使用Del() method來刪除database中指定key的資料
	//result是成功刪除的key-value對的數量
	//參數意義: (固定參數，key值)
	result, _ := rdb.Del(ctx, "name").Result()
	if result == 0 {
		fmt.Println("key not exist")
	}

	//設定該key的有效期限(ex: 將在15秒後刪除)
	//rdb.Expire(ctx, "weight", time.Second*15)

	//使用Get() method來取得database中指定key的資料(data是string型態)
	//若找不到，則err會是redis.Nil
	//參數意義: (固定參數，key值)
	data, err := rdb.Get(ctx, "name").Result()
	if err == redis.Nil {
		fmt.Println("name= not found")
	} else {
		fmt.Println("name=", data)
	}

	data, err = rdb.Get(ctx, "age").Result()
	if err == redis.Nil {
		fmt.Println("age= not found")
	} else {
		fmt.Println("age=", data)
	}

	data, err = rdb.Get(ctx, "weight").Result()
	if err == redis.Nil {
		fmt.Println("weight= not found")
	} else {
		fmt.Println("weight=", data)
	}

	//斷開與database的連接
	rdb.Close()
}

```

## redis---List
* 底層儲存的資料結構為Link list
* 若在List不存在的情況下使用rdb.Lpush()等功能，則會自動創建一個列表
* 若刪除一個本來就不存在的元素，並不會發生任何事
* 可以透過rdb.Lpush()加入各種型態的資料，但皆會被轉成String型態儲存
* rdb.LRange()給定的起始位置跟結束位置是0-index，且回傳的[]string會包含結束位置

範例:
```go
package main

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"
)

func main() {
	options := redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	}
	rdb := redis.NewClient(&options)
	ctx := context.Background()

	//在key="name"的List中插入一個元素至右側
	//參數意義: (固定參數，指定key，欲插入的元素)
	rdb.RPush(ctx, "name", "Jason0")
    
        //欲插入的元素可以一次傳多個
	rdb.RPush(ctx, "name", "Jason1", "Jason2", "Jason3", "Jason4", "Jason5")

	//在key="name"的List中插入一個元素至左側
	//參數意義: (固定參數，指定key，欲插入的元素)
	rdb.LPush(ctx, "name", "Jason6")

	//在key="name"的List中，從右側刪除一個元素
	//使用Result()解析後，val是刪除的那個元素
	//參數意義: (固定參數，指定key)
	val, _ := rdb.RPop(ctx, "name").Result()
	fmt.Println("Rpop:", val)

	//在key="name"的List中，從左側刪除一個元素
	//使用Result()解析後，val是刪除的那個元素
	//參數意義: (固定參數，指定key)
	val, _ = rdb.LPop(ctx, "name").Result()
	fmt.Println("Lpop:", val)

	//在key="name"的List中，替換指定index的元素值
	//使用Result()解析後，val是獲取的元素值
	//參數意義: (固定參數，指定key，指定index，欲替換的值)
	rdb.LSet(ctx, "name", 1, "conny1")

	//在key="name"的List中，獲取指定index的元素值
	//使用Result()解析後，val是獲取的元素值
	//參數意義: (固定參數，指定key，指定index)
	val, _ = rdb.LIndex(ctx, "name", 1).Result()
	fmt.Println("index 1 value:", val)

	//在key="name"的List中，刪除指定value的元素值
	//使用Result()解析後，count是成功刪除的元素個數
	//參數意義: (固定參數，指定key，模式(0代表刪除所有)，指定value)
	count, _ := rdb.LRem(ctx, "name", 0, "conny1").Result()
	fmt.Println("remove element amount:", count)

	////在key="name"的List中，取得一個區間的值
	//使用Result()解析後，ele是回傳的區間([]string型態)
	//參數意義: (固定參數，指定key，起始位置，結束位置)
	ele, _ := rdb.LRange(ctx, "name", 1, 3).Result()
	fmt.Println("ele1=", ele)

	//起始或結束位置為-1的意義是"最後一個位置"
	ele, _ = rdb.LRange(ctx, "name", 0, -1).Result()
	fmt.Println("ele2=", ele)

	//index out of range的處理
	//若起始位置比最後一個元素位置還大，則回傳空的[]string
	ele, _ = rdb.LRange(ctx, "name", 10, 20).Result()
	fmt.Println("ele3=", ele)

	//若結束位置比最後一個元素位置還大，則該結束位置會被視作最後一個元素的位置
	ele, _ = rdb.LRange(ctx, "name", 1, 20).Result()
	fmt.Println("ele4=", ele)

	//取得key="name"的List的長度
	//參數意義: (固定參數，指定key)
	len, _ := rdb.LLen(ctx, "name").Result()
	fmt.Println("len=", len)

	rdb.Del(ctx, "name")

}

```

## redis---set
* 底層儲存的資料結構為hash table，性質為無序，且每個元素最多只出現一次
* 若刪除一個本來就不存在的元素，並不會發生任何事

範例:
```go
package main

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"
)

func main() {
	options := redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	}

	rdb := redis.NewClient(&options)
	ctx := context.Background()

	//加入元素至key="name"的Set中
	//若元素本來就已存在，則不會做任何事
	//參數意義: (固定參數，key值，欲加入的元素)
	rdb.SAdd(ctx, "name", "Jason1")
	rdb.SAdd(ctx, "name", "Jason2", "Jason3", "Jason4") //可以一次加入多個

	//從key="name"的Set中刪除指定的元素
	//若元素本來存在並刪除成功，則count=1，否則count=0
	//參數意義: (固定參數，key值，欲刪除的元素)
	count, _ := rdb.SRem(ctx, "name", "Jason4").Result()
	fmt.Println("delete element amount:", count)

	//確認key="name"的Set中某元素是否存在
	//exist1型態是bool，代表是否存在
	//參數意義: (固定參數，key值，欲確認的元素)
	exist1, _ := rdb.SIsMember(ctx, "name", "Jason2").Result()
	exist2, _ := rdb.SIsMember(ctx, "name", "Jason4").Result()
	fmt.Println("Jason2 exist?", exist1)
	fmt.Println("Jason4 exist?", exist2)

	//取出所有key="name"的Set中的元素
	//members型態是[]string，代表取出的元素們
	//參數意義: (固定參數，key值)
	members, _ := rdb.SMembers(ctx, "name").Result()
	fmt.Println("All members:", members)

	//取得key="name"的Set中的元素數量
	//len即為取得的元素數量
	//參數意義: (固定參數，key值)
	len, _ := rdb.SCard(ctx, "name").Result()
	fmt.Println("len=", len)

	rdb.Del(ctx, "name")
}
```

## redis---Sorted Set
* 與Set的不同之處在於每一筆資料都附帶一個"分數"，故value儲存的形式變成一個redis中內建的struct
* Sorted Set內部儲存方式是由小至大排序的；由於順序固定，可以進行區間操作；index的定義與處裡方式跟List相同
    ```go
    type Z struct {
        Score  float64 // 分數
        Member interface{} // 元素名
    }
    ```
    
範例:
```go
package main

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"
)

func main() {
	options := redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	}
	rdb := redis.NewClient(&options)
	ctx := context.Background()

	//加入一個元素至key="name" 的Sorted Set中(也可加入多個元素)
	//若該Member已存在，則更新其分數(浮點數)
	//參數意義: (固定用法，key值，redis.Z結構)
	rdb.ZAdd(ctx, "name", redis.Z{Score: 10.0, Member: "Jason"})
	rdb.ZAdd(ctx, "name", redis.Z{Score: 3.0, Member: "conny"},
		redis.Z{Score: 9.2, Member: "Jenny"},
             redis.Z{Score: 8.6, Member: "Denial"},
		redis.Z{Score: 100, Member: "PerfectMan"})

	//從key="name" 的Sorted Set中刪除一個指定Member
	//count=1代表成功刪除，否則為0
	//參數意義: (固定用法，key值，欲刪除的Member)
	count, _ := rdb.ZRem(ctx, "name", "PerfectMan").Result()
	fmt.Println("delete element amount:", count)

	//從key="name" 的Sorted Set中查詢指定Member的分數
	//參數意義: (固定用法，key值，欲查詢的Member)
	score, err := rdb.ZScore(ctx, "name", "PerfectMan").Result()
	if err == redis.Nil {
		fmt.Println("PerfectMan not exist")
	} else {
		fmt.Println("score=", score)
	}

	//從key="name" 的Sorted Set中查詢指定Member的排名(從0開始，0代表最小)
	//參數意義: (固定用法，key值，欲查詢的Member)
	rank, err := rdb.ZRank(ctx, "name", "Jason").Result()
	if err == redis.Nil {
		fmt.Println("Jason not exist")
	} else {
		fmt.Println("rank=", rank)
	}

	//從key="name" 的Sorted Set中取得一個區間(指位置)中的所有值
	//使用方法類似List的LRange
	//result是[]string型態
	//參數意義: (固定用法，key值，起始位置，結束位置)
	result, _ := rdb.ZRange(ctx, "name", 0, -1).Result()
	fmt.Println("ZRange:", result)

	////從key="name" 的Sorted Set透過分數區間查詢
	//result是[]string型態
	//參數意義: (固定用法，key值，redis.ZRangeBy變數)
	ask := redis.ZRangeBy{
		Min:    "0",  //最小查詢分數為00
		Max:    "10", //最大查詢分數為10
		Offset: 1,    //跳過1個符合的結果
		Count:  2,    //最多回傳3個結果
	}
	result, _ = rdb.ZRangeByScore(ctx, "name", &ask).Result()
	fmt.Println("ZRangeByScore:", result)

	len, _ := rdb.ZCard(ctx, "name").Result()
	fmt.Println("len=", len)

	rdb.Del(ctx, "name")
}

```

## Redis Hash
* Redis本來就是透過Key找到儲存的資料的(hash的概念)，但現在將儲存的資料(value)也設成hash table；換句話說，此時database中的資料結構近似於
    > map<string,map<string,string>> mp;
* 重複元素的處理規則同一般的hash rable。若原本的key不存在，則新增一個key-value對；若原本的key已存在，則更新其value；

```go
package main

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"
)

func main() {
	options := redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	}
	rdb := redis.NewClient(&options)
	ctx := context.Background()

	//向key="customer1" 的hash table新增一個key-value對
	//參數意義: (固定參數，指定key，hash table的key，hash table的value)
	rdb.HSet(ctx, "customer1", "name", "Jason")

	//可以一次向key="customer1" 的hash table新增多個key-value對(非取代)
	//參數意義: (固定參數，指定key，map[string]interface{}型態的變數)
	add_data := map[string]interface{}{
		"age":    20,
		"weight": 58,
		"temp":   100,
	}
	rdb.HMSet(ctx, "customer1", add_data)

	//從key="customer1" 的hash table中刪除特定key(可以一次刪除多個)
	count, _ := rdb.HDel(ctx, "customer1", "temp").Result()
	fmt.Println("delete element amount:", count)

	//從key="customer1" 的hash table中查詢特定key是否存在及其值
	//參數意義: (固定參數，指定key，指定hash table中的key)
	data, err := rdb.HGet(ctx, "customer1", "name").Result()
	if err == redis.Nil {
		fmt.Println("name not exist")
	} else {
		fmt.Println("name:", data)
	}

	//從key="customer1" 的hash table中取得所有key-value對
	//result是map[string]string型態
	//參數意義: (固定參數，指定key)
	result, _ := rdb.HGetAll(ctx, "customer1").Result()
	fmt.Println(result)

	//取得key="customer1" 的hash table中的key-value對的數量
	//參數意義: (固定參數，指定key)
	len, _ := rdb.HLen(ctx, "customer1").Result()
	fmt.Println("len=", len)

	rdb.Del(ctx, "customer1")
}
```

## redis table
* 若想在資料庫中儲存table，可以用以下程式辦到

範例:
```go
package main

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"
)

// 類似SQL中的定義table
type Customer struct {
	Name   string
	Age    int
	Weight float32
}

// 用來將Customer struct轉成String，以被redis database儲存
func (c *Customer) serialize() string {
	return fmt.Sprintf("%s |%d |%f ", c.Name, c.Age, c.Weight)
}

// 用來將String轉成Customer struct，以解讀其中的內容
func deserializeCustomer(data string) Customer {
	customer := Customer{}
	fmt.Sscanf(data, "%s |%d |%f ", &customer.Name, &customer.Age, &customer.Weight)
	return customer
}

func main() {
	options := redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	}
	rdb := redis.NewClient(&options)
	ctx := context.Background()

	data1 := Customer{
		Name:   "Jason",
		Age:    20,
		Weight: 58.5,
	}
	data2 := Customer{
		Name:   "conny",
		Age:    18,
		Weight: 78.5,
	}
	//插入資料至hash table中
	rdb.HSet(ctx, "customer_info", "customer1", data1.serialize())
	rdb.HSet(ctx, "customer_info", "customer2", data2.serialize())

	//取出儲存的資料
	storeed_data, _ := rdb.HGet(ctx, "customer_info", "customer1").Result()
	fmt.Println("storeed_data:", storeed_data)

	//將String解析成Customer struct
	deserialized_data := deserializeCustomer(storeed_data)

	fmt.Println("name=", deserialized_data.Name)
	fmt.Println("age=", deserialized_data.Age)
	fmt.Println("weight=", deserialized_data.Weight)

}

```

## redis---Subscribe
* 透過redis database，可利用Subscribe機制實現不同支程式之間的溝通
* 頻道名跟一般database中的key值是分開的，故兩者名稱是可以重複的

範例:
```go
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

func f(channel <-chan *redis.Message, ctx context.Context) {
	//無窮迴圈，用於隨時接收頻道傳來的訊息
	for i := range channel {
		fmt.Println(i.Payload)
	}
}

func main() {
	options := redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	}
	rdb := redis.NewClient(&options)
	ctx := context.Background()

	//創建訂閱器，並訂閱mychennel這個頻道
	pubsub := rdb.Subscribe(ctx, "mychennel")

	//在程式結束時關閉這個訂閱器
	defer pubsub.Close()

	//從訂閱器中取得一個channel用來接收消息，並當作go routine的參數傳入
	go f(pubsub.Channel(), ctx)

	for i := 0; i < 10; i++ {
		//送出訊息到指定的頻道
		//參數意義: (固定參數，頻道名，訊息)
		rdb.Publish(ctx, "mychennel", "this is a message!")
		fmt.Println("send!")
		time.Sleep(time.Second)
	}
}

```
## go sql
### 連線至 database 
需要模組
```go
import (
      "fmt"
    _ "github.com/go-sql-driver/mysql
)
```

使用 sql.Open 連線至 database 中
```go
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", os.Getenv("user"), 
os.Getenv("password"), os.Getenv("host"), os.Getenv("port"), os.Getenv("db")))
if err != nil {
	log.Error("無法連線至資料庫:", err)
}
defer db.Close()
```
* os.Getenv("user"): 使用者名稱
* os.Getenv("password"): 使用者密碼
* os.Getenv("host"): database 所在位址
* os.Getenv("port"): database 所在位址的 port
* os.Getenv("db"): 想要連到的 database 名稱

### query
```go
// 查詢資料庫中的表格
SQL_cmd := "SELECT amount FROM carbonmap where year = " + c.QueryParam("year") + " and month = " + c.QueryParam("month") + " and city = '" + c.QueryParam("city") + "'"
rows, err := db.Query(SQL_cmd)
if err != nil {
	log.Error("查詢資料失敗:", err)
}
defer rows.Close()

var ans = ""
for rows.Next() { //逐 row 讀取回傳的資料
	var ( //定義一系列的變數，對應至回傳資料中的 column
		amount int64
	)
	if err := rows.Scan(&amount); err != nil { //將讀取到的資料存入變數中
		log.Error("讀取資料失敗:", err)
	}
	ans += fmt.Sprint(amount)
}
```
* 事先準備好 SQL_cmd 字串
* 使用 db.Query() 並取得回傳的資料結構 rows
* 使用 rows.Next() 搭配 for 迴圈，每一輪讀取一個 row 的資料
* 使用 rows.Scan() 將資料儲存進 go 中的變數


## go gRPC
* gRPC是基於http2.0的溝通方式，可以作用於server端與client端的溝通，目的是讓client端向server端發送請求時，就像在使用自己的資源一樣
* gRPC為了能套用至各種語言上，會先將寫好的程式碼編譯成"中繼檔案"，再由各語言另外安裝的編譯器轉成可被該語言執行的檔案

環境配置:
1. 從以下網址安裝Protocol buffer(版本: protoc-23.3-win64.zip)；負責編譯"中繼檔案"
    https://github.com/protocolbuffers/protobuf/releases

2. 將下載的壓縮檔解壓縮至自己想要的目錄下(這邊我在C槽中創了一個ptotoc的資料夾，並解壓縮在裡面)
    ![](https://hackmd.io/_uploads/rkUuczh_2.png)

3. 配置環境變數為protoc.exe所在的路徑
    ![](https://hackmd.io/_uploads/ByzX9M3_3.png)

4. 在終端機中輸入以下兩行指令安裝編譯器，目的是將"中繼檔案"轉成可被該語言執行的檔案；這兩個檔案會被安裝在GoPath的bin中
    > go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
    > go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
5. 在終端機中輸入以下指令取得GoPath
    > go env GOPATH
    
    ![](https://hackmd.io/_uploads/ryfd7Qhd2.png)


6. 以相同方式配置此環境變數；目的是讓系統找的到將"中繼檔案"編譯成.go檔的方法
    ![](https://hackmd.io/_uploads/rkh3vxpdn.png)

    
7. 接下來試跑官網上的範例。首先在vscode中開啟想要的資料夾，等等下載的範例便會放在此資料夾中；接著輸入以下指令下載範例
    > git clone -b v1.56.1 --depth 1 https://github.com/grpc/grpc-go
    
    ![](https://hackmd.io/_uploads/SJsPS7hdn.png)

8. 接著移動至example helloworld所在的目錄下
    > cd grpc-go/examples/helloworld

9. 執行gRPC server端的程式
    > go run greeter_server/main.go
    
    ![](https://hackmd.io/_uploads/B1BU872un.png)

10. 開啟另一個vscode的視窗，同樣移動至example helloworld所在的目錄下，接著執行gRPC client端的程式，若收到Greeting: Hello world的回覆代表成功
    > go run greeter_client/main.go

    ![](https://hackmd.io/_uploads/rJSyvXnun.png)

## gRPC introduce
以下將簡單介紹如何從零開始創建一個gRPC服務

本次專案的檔案結構如下:
```
想要的專案資料夾
    client(資料夾)
        client.go
    server(資料夾)
        services(資料夾)
            Prod_grpc.pb.go
            Prod.pb.go
            Prod.proto
        server.go
    go.mod
    go.sum
```

### 創建中繼檔案並轉成golang可執行的檔案
1. 來到想要的專案資料夾下，創建server跟client兩個資料夾；接著在server資料夾中再創建一個services的資料夾；接著建立go mod(這邊mod的名稱設成myProject)
![](https://hackmd.io/_uploads/rJxRNLTun.png)

2. 在services的資料夾中創建一個Prod.proto檔案，並貼上下面的程式碼；這個檔案將作為"中繼檔案"

    ```proto
    syntax="proto3"; //表示使用proto3語法
    option go_package = "../services"; //編譯後的.go檔的生成路徑
    package services; //表示編譯後的.go檔在services這個package中

    //定義service，使用下面定義的message實現server端與客戶端之間的溝通
    service ProdService {
        rpc GetProdStock (ProdRequest) returns (ProdResponse) {}
    }
    //定義向server端發送請求的格式
    message ProdRequest {
        int32 ProdId=1;
        //註: 在編譯的時候變數的命名方式會自動被調整為ProdId這種格式
        //例如變數名prod_id會被自動調整為ProdId
    }
    //定義向客戶端發送回應的格式
    message ProdResponse {
        int32 ProdStock=1;
        //註: 在編譯的時候變數的命名方式會自動被調整為ProdStock這種格式
    }
    ```
    ![](https://hackmd.io/_uploads/HycbBIa_h.png)


3. 先移動至services資料夾中，接著使用以下指令編譯中繼檔案，再編譯成.go檔案；執行後會發現services資料夾中多了兩個檔案
    ```
    protoc --go_out=./ Prod.proto
    protoc --go-grpc_out=./ Prod.proto
    ```
    ![](https://hackmd.io/_uploads/H1UHr86_3.png)


4. 點開Prod_grpc.pb.go，會出現找不到路徑的錯誤
    ![](https://hackmd.io/_uploads/S1mYHL6O3.png)

    解決方法為在vscode的終端機中輸入以下指令即可
    > go get -u google.golang.org/grpc
    
    ![](https://hackmd.io/_uploads/rJWoBLT_n.png)

### 創建gRPC server端
在server資料夾中創建server.go檔案並貼上以下程式碼；此檔案執行後將會作為gRPC的server端

```go
package main

import (
	"context"
	"net"

	//代表引入myProject這個go mod 中的services package
	"myProject/server/services"

	"google.golang.org/grpc"
)

// 創建一個struct
// 名字ProdService來自Prod.proto 中所定義的service
type ProdService struct {
	services.UnimplementedProdServiceServer
}

// 此函式是複製Prod_grpc.pb.go中的第49行處
// 在這裡真正定義了service應該要怎麼做(實例)
// 記得把*ProdRequest改成*services.ProdRequest，*ProdResponse也要改
//
// 原形:
//
//	type ProdServiceServer interface {
//		GetProdStock(context.Context, *ProdRequest) (*ProdResponse, error)
//		mustEmbedUnimplementedProdServiceServer()
//	}
func (p *ProdService) GetProdStock(context.Context, *services.ProdRequest) 
(*services.ProdResponse, error) {
	return &services.ProdResponse{ProdStock: 20}, nil
}

func main() {
	rpcServer := grpc.NewServer() //創建一個grpc server

	//註冊剛剛創建的grpc server其處理請求的方式
	//ProdService{} 最後會連結至上面寫好的GetProdStock()中
	services.RegisterProdServiceServer(rpcServer, &ProdService{})

	lis, _ := net.Listen("tcp", "localhost:8000") //設定監聽的位址
	rpcServer.Serve(lis)                          //啟動server
}

```

### 創建gRPC client端
1. 另開一個vscode視窗，並移動到client資料夾中
![](https://hackmd.io/_uploads/rJJ3wUTd2.png)

2. 新建一個client.go的檔案，並將以下程式碼貼上去
    ```go
    package main

    import (
        "context"
        "fmt"
        "myProject/server/services"

        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
    )

    func main() {
        //建立連接，第一個參數是server端的位址
        //第二個參數是設定不使用證書連接
        connection, _ := grpc.Dial("localhost:8000", grpc.WithTransportCredentials(
            insecure.NewCredentials()))
        defer connection.Close() //結束時斷開連接

        client := services.NewProdServiceClient(connection) //新增一個客戶
        ctx := context.Background()                         //固定參數

        //客戶在向server端發送請求時，就好像使用自己的method一樣
        //設定傳給server參數ProdId為10
        result, _ := client.GetProdStock(ctx, &services.ProdRequest{ProdId: 10})

        //印出結果，剛剛在GetProdStock中定義無論如何都將ProdStock的值設成20後回傳
        //故輸出永遠是20
        fmt.Println(result.ProdStock) //印出結果

    }
    ```
### 開始執行
先執行server.go，再執行client.go，若client.go輸出20代表成功
![](https://hackmd.io/_uploads/BJv8Y86On.png)



## 小技巧
### 重要規則
* 大寫字母開頭的變數是可匯出的，也就是其它套件可以讀取的，是公有變數；小寫字母開頭的就是不可匯出的，是私有變數。
* 大寫字母開頭的函式也是一樣，相當於 class 中的帶 public 關鍵詞的公有函式；小寫字母開頭的就是有 private 關鍵詞的私有函式。
### 分組宣告
```go
//可以像這樣寫在一起比較方便
import(
    "fmt"
    "os"
)

const(
    i = 100
    pi = 3.1415
    prefix = "Go_"
)

var(
    i int
    pi float32
    prefix string
)
```
### 零值
* 指的是變數未初始化前的預設值
```go
int     0
int8    0
int32   0
int64   0
uint    0x0
rune    0 //rune 的實際型別是 int32
byte    0x0 // byte 的實際型別是 uint8
float32 0 //長度為 4 byte
float64 0 //長度為 8 byte
bool    false
string  ""
```

## goto 語法
* golang內建了類似setjump與longjump的操作，名為goto，只能在函式內使用，且不可跨函式，不會還原任何東西
* 可以用break跳出標籤生效區域

範例:
```go
package main

import "fmt"

func main() {
	counter := 0
hi: //相當於setjump，設定標籤
	fmt.Println(counter)
	counter += 1
	if counter <= 10 {
		goto hi //相當於longjump，會跳回setjump設定的標籤處
	}
}
```
```go
package main

import "fmt"

func main() {

hi: //相當於setjump，設定標籤
	for i := 0; i < 100000000; i++ {
		fmt.Println(i)
		if i >= 10 {
			break hi //強制離開hi這個區域
		}
	}
}

```
### Panic 和 Recover
* golang的錯誤處理機制
* panic會使程式中斷，而且會一路中斷回根節點(仍會執行defer中的內容)
* 可以讓進入 panic 狀態的 goroutine 恢復過來

### main() 與 init()
* 只有main package有一個main()函式
* 每個packet中建議只有一個init()函式，用來進行初始化操作
![](https://hackmd.io/_uploads/SkAnBTg_3.png)

### import
* 載入自己寫的模組的方式範例
    > import “shorturl/model” 
    > //載入位於 gopath/src/shorturl/model 模組

* 省略套件名
```go
import(
     . "fmt"
 )
```
* 別名
```go
import(
     f "fmt" //將fmt重新命名成f
 )
```

### 自訂型別(typedef)
* 定義成別名後，就脫離了原來的型別的控制範圍，對別名新增method等也不會影響到原來的型別
> type 別名 原來的型別
> ex: type ages int

### 檢測型態
* 因為interface{} 可以代表任何型態，故需要一種檢測當前變數是甚麼型態的方法

```go
var temp interface{} = "abc"
	_, ok := temp.(int)
	if ok {
		fmt.Println("temp[0] is int")
	} else {
		fmt.Println("temp[0] is not int")
	}
```

## 知識性內容
### web 工作方式
![](https://hackmd.io/_uploads/rJzRPbXO2.png)
1. 當輸入網址後，瀏覽器(客戶端)會請求 DNS 伺服器，將域名(網址)解析成對應的IP
2. 取得IP後，透過這個IP找到伺服器，要求建立TCP連線
3. 建立TCP連線，向該伺服器發起http request封包
4. 伺服器端收到http請求封包，會處理該請求封包；處理完後，伺服器端會發送HTTP Response封包給使用者
5. 關閉TCP連線

### HTTP 協議
* HTTP 是一種讓 Web 伺服器與瀏覽器(客戶端)透過 Internet 傳送與接收資料的協議，建立在 TCP 協議之上
* 屬於一個請求、回應協議--客戶端發出一個請求，伺服器回應這個請求
* HTTP 協議是無狀態的，同一個客戶端的這次請求和上次請求沒有對應關係，但可用 Cookie 機制來維護連線的可持續狀態
* 從 HTTP/1.1 起，預設都開啟了 Keep-Alive 保持連線特性，簡單地說，當一個網頁開啟完成後，客戶端和伺服器之間用於傳輸 HTTP 資料的 TCP 連線不會關閉，如果客戶端再次存取這個伺服器上的網頁，會繼續使用這一條已經建立的 TCP 連線。Keep-Alive 不會永久保持連線，它有一個保持時間，可以在不同伺服器軟體（如 Apache）中設定這個時間。

* 請求封包:
    ```
    GET /domains/example/ HTTP/1.1        //請求行: 請求方法 請求 URI HTTP 協議/協議版本
    Host：www.iana.org                //伺服器端的主機名
    User-Agent：Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4            //瀏覽器資訊
    Accept：text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8    //客戶端能接收的 MIME

    Accept-Encoding：gzip,deflate,sdch        //是否支援流壓縮
    Accept-Charset：UTF-8,*;q=0.5        //客戶端字元編碼集
    //空行，用於分割請求頭和訊息體
    //訊息體，請求資源參數，例如 POST 傳遞的參數
    ```
* 回應封包
    ```
    HTTP/1.1 200 OK                        //狀態行
    Server: nginx/1.0.8                    //伺服器使用的 WEB 軟體名及版本
    Date:Date: Tue, 30 Oct 2012 04:14:25 GMT        //傳送時間
    Content-Type: text/html                //伺服器傳送資訊的型別
    Transfer-Encoding: chunked            //表示傳送 HTTP 套件是分段發的
    Connection: keep-alive                //保持連線狀態
    Content-Length: 90                    //主體內容長度
    //空行 用來分割訊息頭和主體
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"... //訊息體
    ```
* 請求封包跟回應封包都包含了Header跟body兩個部分；Header通常存有這個封包的基本訊息，例如請求的目標主機名稱、用戶端代理程式的相關資訊等等；而body則存了
### 客戶端與web server的溝通
![](https://hackmd.io/_uploads/Hy5MhfQ_3.png)
1. server建立listen socket，等待客戶端的連線請求到來
2. 客戶端發起連線請求，listen socket接受後，得到Client Socket；Client Socket將負責接收客戶端的請求與回應客戶端
3. 從Client Socket中取得請求的相關資訊(協議頭，提交的資料等)，並交由Handler處理
4. Handler處理完後，將要交給客戶端的資訊回傳給Client Socket
5. Client Socket回傳相關資訊給客戶端