--- 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回傳相關資訊給客戶端