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

2. 輸入 "main" 後enter即可

## 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相同


* 深入探討各程式的實作方式

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

* 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

* .env 檔案內容

依賴模組
```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的部分)

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


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

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

如此一來便能找到位於"static\css\common\styles.css"的檔案了
### 極重要 static data 的另一種理解
上面的那種方法本地端能正常運作,但放到伺服器上後出現找不到靜態資源的問題,以下提供解決方法,但原理跟上面的衝突,可能上面寫錯了,還待驗證。這邊提供另一種方式來使用echo框架載入靜態資料
main.go設定載入css的方式,注意最前方有沒有斜線
(這邊註解掉下面那行的原因有可能是因為只有外層(不含article資料夾)才有載入static data 的html程式碼,也可能是因為e.Static()最多只能存在一個)

html 中載入靜態資料的link寫法,注意最前方的斜線,已確定會影響資料能不能成功被載入

## 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憑證所需資料,與程式置於同一個資料夾中

### 生成憑證
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的資料夾,並解壓縮在裡面)

3. 配置環境變數為protoc.exe所在的路徑

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

6. 以相同方式配置此環境變數;目的是讓系統找的到將"中繼檔案"編譯成.go檔的方法

7. 接下來試跑官網上的範例。首先在vscode中開啟想要的資料夾,等等下載的範例便會放在此資料夾中;接著輸入以下指令下載範例
> git clone -b v1.56.1 --depth 1 https://github.com/grpc/grpc-go

8. 接著移動至example helloworld所在的目錄下
> cd grpc-go/examples/helloworld
9. 執行gRPC server端的程式
> go run greeter_server/main.go

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

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

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這種格式
}
```

3. 先移動至services資料夾中,接著使用以下指令編譯中繼檔案,再編譯成.go檔案;執行後會發現services資料夾中多了兩個檔案
```
protoc --go_out=./ Prod.proto
protoc --go-grpc_out=./ Prod.proto
```

4. 點開Prod_grpc.pb.go,會出現找不到路徑的錯誤

解決方法為在vscode的終端機中輸入以下指令即可
> go get -u google.golang.org/grpc

### 創建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資料夾中

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代表成功

## 小技巧
### 重要規則
* 大寫字母開頭的變數是可匯出的,也就是其它套件可以讀取的,是公有變數;小寫字母開頭的就是不可匯出的,是私有變數。
* 大寫字母開頭的函式也是一樣,相當於 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()函式,用來進行初始化操作

### 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 工作方式

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的溝通

1. server建立listen socket,等待客戶端的連線請求到來
2. 客戶端發起連線請求,listen socket接受後,得到Client Socket;Client Socket將負責接收客戶端的請求與回應客戶端
3. 從Client Socket中取得請求的相關資訊(協議頭,提交的資料等),並交由Handler處理
4. Handler處理完後,將要交給客戶端的資訊回傳給Client Socket
5. Client Socket回傳相關資訊給客戶端