Go 學習筆記(6): 寫一個網頁吧!

我自己是PHP 後端工程師,常常在寫網頁、常碰Javascript , html , css 等等,所以,在學這個Go的過程當中,也來學習用go寫一個網頁吧!

Go 與網頁常常有用到的一個套件就叫做 net/http , 他同時也可以做網頁的爬蟲,等等讓我娓娓道來~

網頁版HelloWorld !

package main
import (
  "log"
  "net/http"
)

func viewHandler(writer http.ResponseWriter, request *http.Request){
  message := []byte("Hello World")
  _,err := writer.Write(message)
  if err != nil {
    log.Fatal(err)
  }
  
}

func main(){
  http.HandleFunc("/hello",viewHandler)
  err := http.ListenAndServe("localhost:8080",nil)
  log.Fatal(err)
}

Template

如果網頁只是簡單的HelloWorld 怎麼行?在Go 裡面還有另外一個套件:html/template , 而使用Execute 的第二個參數可以將資料塞入樣板中。而樣板中很重要的是可以使用{{.}} 等符號

package main
import (
  "log"
  "net/http"
  "html/template"
)

type TemplateData struct {
	Name string
}

func viewHandler(writer http.ResponseWriter, request *http.Request){
  html, err := template.ParseFiles("view.html")
 
  if err != nil {
    log.Fatal(err)
  }
  data := TemplateData{Name:"Jimmy"}
  err = html.Execute(writer,data) 
 
 if err != nil {
    log.Fatal(err)
  }
}

func main(){
  http.HandleFunc("/",viewHandler)
  err := http.ListenAndServe("localhost:8080",nil)
  log.Fatal(err)
}

上面我們有一段template.ParseFiles("view.html") ,而view.html 就像以下這樣寫

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>Hello World</h1>
    <h2>Name: {{.Name}}</h2>
</body>
</html>

接下來,讓這裡筆記一下樣板中常見的樣式

  1. {{.Name}} : 塞入名稱為Name
  2. {{ if .Paid }} … {{ end }}: if paid 為 true , 才能執行if 的區塊
  3. {{ range .Charges }} … {{ . }}… {{ end}}: for 迴圈的區塊,根據Charges 裡面的項目依序列出資料

最後,寫個Todolist 吧!

完整程式碼請參考我的github : https://github.com/r567tw/golang-simple-todolist

首先是主程式:

package main

import (
	"bufio"
	"html/template"
	"log"
	"net/http"
	"os"
	"fmt"
)

type TodoList struct {
	Todos []string
}

func getTodos(fileName string) []string {
	var todos []string
	file, err := os.Open(fileName)
	if os.IsNotExist(err) {
		return nil
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		todos = append(todos, scanner.Text())
	}
	return todos
}

func viewHandler(writer http.ResponseWriter, request *http.Request) {
	html, err := template.ParseFiles("view.html")
	todos := getTodos("todolist.txt")
	if err != nil {
		log.Fatal(err)
	}

	todolist := TodoList{
		Todos:  todos,
	}

	err = html.Execute(writer, todolist)

	if err != nil {
		log.Fatal(err)
	}
}

func todoCreateHandler(writer http.ResponseWriter, request *http.Request) {
	task := request.FormValue("task")

	file, _ := os.OpenFile("todolist.txt", os.O_WRONLY | os.O_APPEND | os.O_CREATE , os.FileMode(0600))
	fmt.Fprintln(file, task)
	defer file.Close()
	http.Redirect(writer, request, "/", http.StatusFound)
}

func main() {

	http.HandleFunc("/", viewHandler)
	http.HandleFunc("/todo/create", todoCreateHandler)

	err := http.ListenAndServe("localhost:8080", nil)
	log.Fatal(err)
}

我將Todo 存在檔案todolist.txt 當中

之後就是簡單地建立一下我的呈現畫面,因為我將創建的表單也放進同一頁,所以我就只要單一個檔案:view.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>Golang Simple Todo list</h1>

    <form action="/todo/create" method="post">
        <input type="text" name="task">
        <button>Create Todo</button>
    </form>
    <ul>
        {{range .Todos}}
            <li><p>{{.}}</p></li>
        {{end}}
    </ul>
</body>
</html>
小君曰:我要學習的還很多咧

Go 學習筆記(5): 非同步

學習Go 也到了一段時間了,來學習這個部分:多執行緒

如同我之前寫道其實如果要做很大量的東西應該要用非同步的方式… 在Go裡面,並發被叫做「Goroutine」,在深入淺出Go 這本書的定義:提供並發,暫停一個工作以啟動另一個工作。而且在有些情境下他們允許並行,同時執行不同的工作

如果你想要用goroutine 其實就在function 前面加一個go 的關鍵字

func main(){
    go a()
    go b()
}

然而,由於我們使用到這個非同步的概念,所以我們無法期待method 可以馬上回傳結果,那是Go 去控制工作的進行與安排,所以Go裡面便多了一個概念:Channels,其實就加一個chan 這個詞就好

var myChannel chan float64
myChannel = make(chan float64)

這時候我們上面得程式可以改寫成這樣

func a(myChannel chan string){
    myChannel <- "a"
}

func main(){
   myChannel := make(chan string) 
   go a(myChannel)
   fmt.Println(<-myChannel)
}

但要注意的是,如果我們make 沒有加第二個參數,那麼我們的Channel 就是個無緩衝的channel ,有就意味著我們必須要每次將資料塞進channel 的時候,每一次都要拉出來,否則會造成channel 被鎖定。

那麼,就讓底下示範一下有緩衝的channel 吧

func a(myChannel chan string){
    myChannel <- "a"
    myChannel <- "b"
}

func main(){
   myChannel := make(chan string,3) 
   go a(myChannel)
   fmt.Println(<-myChannel)
}
小君曰:對於非同步的這一塊,我需要學習的還很多.....

Go 學習筆記(4) – 檔案

寫程式難免都需要與檔案做互動, 甚者還需要與資料庫溝通。這篇文章將紀錄Go 怎麼與檔案互動的部分……

首先是讀取檔案!

package main

import (
    "bufio"
    "os"
    "fmt"
)

func main(){
    var numbers []string
    file ,_ := os.Open("input.txt") // 使用open 開啟檔案
    scanner := bufio.NewScanner(file) // 必須建立Scanner 這裡讓我有點想起C
    for scanner.Scan(){ // 這裡的意思是逐行讀取
      content := scanner.Text()
	  numbers = append(numbers,content)
	  if scanner.Err() != nil {
	     fmt.Println("scan error")
	  }
    }
	defer file.Close() //defer 會確保函式一定被調用,即便程式提早結束
	fmt.Println(numbers)
}

之後,就來示範怎麼寫入檔案吧?

package main

import (
	"os"
	"fmt"
)

func main() {
	file, _ := os.Create("output.txt")
	// file.WriteString("Hello Write Content")
	_, err := file.Write([]byte("Here is a string...."))
	if (err != nil){
		fmt.Println(err)
		panic("error")
	}
	defer file.Close()
}

然後附加檔案的話只要將上面建檔案的改一下即可

package main

import (
	"os"
	"fmt"
)

func main() {
        // 這裡改成openfile 而且要加後面的flag 和數字參數即可
	file, _ := os.OpenFile("output.txt",os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	// file.WriteString("Hello Write Content")
	_, err := file.Write([]byte("Here is a string...."))
	if (err != nil){
		fmt.Println(err)
		panic("error")
	}
	defer file.Close()
}
小君曰:這次耍廢點.....

Go 學習筆記(3): 做套件給別人用

來到學習Go 的第三篇文章,在深入淺出Go 裡面有介紹套件的開發方式,要把他放到GOPath 裡面… 不過,那好像是很久以前Go的版本了… 現在,出現了go.mod 和 go.sum , 似乎就沒有那麼麻煩…

這裡簡介簡單兩個指令:
1. go mod init {package name} 這個指令會建立go.mod 的檔案
2. go mod tidy 這個指令會去找你目前寫的go 檔案所引用的package, 去修改go.mod

go.mod 有點像是composer.json 和 package.json 記錄著目前所引用的package
而 go.sum 是個天書,像composer.lock 和 package.lock 去鎖住目前的版本…

然後開一個Go 的Package 有點簡單,但寫與維護應該很複雜,而且我也有點還是搞不太清楚還在學習中…. 網路上和書中的資源實在有點讓我混亂…… 可能等我再稍微釐清一下我再回來更新這一篇文章…. 請大家勿參考此文章,但如果有指教請大家多多教導我~

總之,我就在github 上面開了一個簡易的greeting repo

只要你記得,可以匯出的參數與方法是要大寫開頭,就可以輕輕鬆鬆自己寫一個package 啦

然後在我go run/go install / go build 的時候就能跑了… 真的好妙….

另外啊,可以用註解當成文件,然後人家跑go doc {套件名} /go doc -all {套件名}就可以看到怎麼使用套件,真的蠻方便的,但有一些注意事項

  1. package 的註解必須要在package這個關鍵字以後
  2. function / 變數等等需要在註解前面增加與他同名的名稱
// Package greeting echo Hello or echo Hi message.
package greeting

// Ha return "Ha"
const Ha = "Ha"

// Hello return Hello message.
/* test */
func Hello() string {
	return "Hello"
}

// Hi return Hi message.
func Hi() string {
	return "Hi"
}
小君曰:天哪... 這篇完全寫得好差

原來可以這麼寫(12): 我❤️Golang

最近處理兩個大功能,一個是要建立websocket server 讓交易流程去串(開發官網購買)(但說真的交易流程用websocket 真的有點怪怪的?就我的認知 websocket 的部分應該是在一個很即時的情境,但用在這種只是為了獲得通知的目的確實有點怪怪的… 但說真的我也沒有辦法提出更好更優的方案,作為一位只能聽命行事的超基層也只能照做QAQ)

另外是要做有關於批次匯入的功能…. 呵呵以前就有做過類似Excel 匯入的功能,我深知道那是一個巨大無比的坑,坑是在一開始的規劃,你既然開放前面使用者可以用Excel 檔匯入,那你就得面對使用者的Excel檔可能有千起百怪的樣式、無法驗證或無法預期的輸入…套句我之前有位主管說的話:你要防使用者想防賊一樣啊!

總之,這個功能其實去年有做一下下,但後來被靠北中止了….現在又復活,當然給他們凹一下,仔細把流程討論清楚先,然後再做…. (其實正確的流程及我想表達的意思是:PM/SA 你們給我生出流程圖和規格書啊!

然而,在這兩大功能夾擊下,我算是很平安的度過及學習、開發,並沒有像之前趣聽小說匯入被搞得人仰馬翻、混亂不已….在這個過程中,修修幾個bug、調整幾段程式碼…. 其中一項是調整拆帳報表的功能

原本,我是用Laravel 匯出拆帳報表,並用上 Laravel-excel 這個套件,殊不知測試環境都弄好好的,但到了正式就完蛋(匯不出來)

因此,我發現當前端送 request 直接製作檔案的這個版本實在有點….糟糕。

後來規劃出了第二套版本:就是當前端送出request 後丟到queue後再慢慢做… 也是一樣用到剛剛說的那個套件,那個套件可以很輕易的用queue的方式製作excel 檔。

測試環境一樣好好的,但正式環境還是爆掉…. 後來發現只要調大我們機器的CPU 就可以匯出了….,不然就是我真的功力不精無法好好使用這個套件囧!

但是!每次調大機器CPU 實在不是什麼好方法…. 而同事告訴我:不應該堅持用laravel/php處理這種CPU bound的事項…. 於是….我採用的AWS Lambda 方案,而Lambda 用Go 開發….

結果是:原本用Laravel Queue要跑兩三十分鐘的報表居然奇蹟的在5~15秒內搞定

然後再寫的過程當中也越來越喜歡Go這門語言。不過我知道我要學習的還太多,這在我使用Go上面只是剛開始而已。

謝謝後端團隊的大家讓我可以任性的使用看看這門語言,並且在我有問題的時候願意幫助我^^

於是乎,這個月的技術分享會就分享我使用Golang的心得了……

額外加碼:

最近docker 使用上常常有那個build 失敗的image 和container bla bla 的,最近看到一個很好用的清除指令紀錄一下:docker system prune

小君曰:想要轉職成Golang 工程師。。。

Go學習筆記(2): make a game

來到學習Go 的第二篇文章了,來學習一些控制流以及一些基礎東西吧!

條件式

在 Golang 寫條件式很簡單,就是寫個if 加個大括號就好

// 這裡請記得 "{" 和 if 要同一行,之前自由慣了常常被這件事情雷到!
if true {
    ......
} else if false {
    .....
}

// 或者你可以使用 if  更短的初始化陳述句
if count := 5; count>4 {
   fmt.Println("count is ",count)
}

另外有一個比較特別的是,if 區塊裡面的區域變數(使用到:=)是不能使用在if 外面的喔~

請讓我用以下程式碼來做進一步陳述

詳細可以去看 這裏:https://github.com/r567tw/go-practice/blob/master/HelloWorld/if.go

package main

import (
  "fmt"
)

// var x = 999 // go 裡面也可以宣告一個超越main範圍以外的超全域變數, 但不建議這麼做

func main() {
	fmt.Println("Hello World")
	x := 10

	if x > 0 {
		y := 10
		// x := 100 // 這個等同在裡面宣告新的位置x , 所以外面的x仍然是10
		x = 100 // 這個會污染外面宣告的x , 所以外面的x 會等於 100
		fmt.Printf("x = %d\n", x) // x =100
		fmt.Printf("%d in if statement\n", y) //10 in if statement
	}

	// fmt.Sprintf("%d out of if statement", y) // error
	fmt.Printf("x = %d out of if statement\n", x) // x = 100 out of if statement
}

值得再拿出來說嘴的是,x變數在外面的宣告,如果在if 陳述句範圍裡面使用單純的= ,在main 範圍內的x 是會被更動的,可是如果使用:= , main 裡面的x 不會被影響。

if number,err := strconv.ParseFloat("3.14",64); err != nil{
    log.Fatal(err)
}

fmt.Println(number) // <= 這裡會出現錯誤,因為number 屬於if 區塊裡面的範圍,go 語言的if 裡面變數不得共享

Switch 陳述句

switch rand.Intn(3)+1 {
    case 1 : ...
    case 2 : ...
    case 3 : ...
    default : ...
}

迴圈

話不多說, show you the code

for x:= 0; x<=6; x++ {
    ......
}

// 或者只是條件式得處理
x:=0
for x<=6 {
    x++
}

// 迴圈裡面也可以使用 continue 和 break ...

和 if 一樣,使用到:= 所宣告的變數是沒辦法使用在for 外面的。

function

package main

import (
	"fmt"
)

type bigger = func(int) bool // function 也可以作為型態的一種!

func main() {
	handleFn()

	anonymous := func() {
		fmt.Println("anonymous function")
	} // go 也支援匿名funciton
	anonymous()

	origin := []int{1, 2, 3, 4, 5}
	change := filter(origin, func(el int) bool {
		return el > 3
	})
	fmt.Println(change)
}

func handleFn() {
	fmt.Println("test function")
}

func filter(data []int, big bigger) []int {
	filtered := []int{}
	for _, element := range data {
		if big(element) {
			filtered = append(filtered, element)
		}
	}
	return filtered
}

Make A Game !

接下來,讓我們試著應用以上這些東西,來寫個簡單的猜數字遊戲吧!

package main

import (
	"bufio"
	"fmt"
	"os"
	"math/rand"
	"strconv"
	"strings"
	"time"
)

func main() {
	r := rand.New(rand.NewSource(time.Now().UnixNano())) // 這一行帶入現在的時間,好讓每一次遊戲隨機產生的數字都不一樣, 原來亂數的原理其實是有一個小技巧和規則的
	result := r.Intn(100) // 其實這裡可以隨機產生數字
	
	loop := true // 設定遊戲開始的條件
	for (loop) {
		fmt.Printf("Please Enter a number(1-100): ")
		reader := bufio.NewReader(os.Stdin) // 其實這裏就是類似python 的input 而已
		input, _ := reader.ReadString('\n')
		number,_ := strconv.Atoi(strings.TrimSpace(input)) // 一定要用trimspace, 否則 strconv轉出來的數字不一定取得出來
		switch {
			case (result < number):
				fmt.Printf("smaller than %d\n",number)
			case (result > number):
				fmt.Printf("bigger than %d\n",number)
			case (result == number):
				fmt.Println("Bingo")
				fmt.Printf("result is %d\n",result)
				loop = false
				break
			default:
				loop = true
		}
	}

	fmt.Println("Game is over!")
}

有興趣看程式碼的可以來這裡:https://github.com/r567tw/go-practice/blob/master/makeGame/main.go

小君曰:猜數字遊戲好像可以作為每個程式語言的入門磚,相對於前端的Todo list 呵呵

Go學習筆記(1): HelloWorld

在之前寫到今年的計劃當中,我就說到我想要學Go 語言。同時我自己也買了一本有關於Go 的書:深入淺出Go , 希望藉此督促自己有個比較完整性的學習……

以下是我讀這本書以及學習的心得與筆記,可能有點無聊,高手請跳過、不過如果有看到錯的也請不吝指正!

首先,你要先去安裝好Go…….

程式組成

Go 語言的組成通常有三個部分:
1. 套件子句(package main)
2. import 相關陳述句 (import "fmt")
3. 主要的程式碼 (func main(){......})

Hello World 程式!

讓我們先建立一個名為hello.go 的檔案… 然後在裡面寫這些東西…

package main
import "fmt"

func main(){
	fmt.Println("Hello World")
	// 這裡請務必使用 " 否則很容易跳出 invalid character literal (more than one character) 的問題
}

接下來讓我們對這個檔案做go run hello.go 就可以看到 Hello World 的字眼啦!

Go 的資料型態類別

  1. 字串:用雙引號所框起來的任意數量字元
  2. 符文(runes) : 用單引號所匡著的單一字元, ex. ‘A’ , ‘B’
  3. boolean (bool)
  4. numbers
    1. float32 , float64
    2. int8 ,int16 ,int32 ,int64
    3. uint
    4. uint8 , uint16 ,uint32 ,uint64
  5. byte (檔案專用)

tips: 可以透過 reflect 這個套件裡面的TypeOf 方法得知資料的型別

package main

import (
  "fmt"
  "reflect"
)

func main() {
	fmt.Println(reflect.TypeOf("Hello World")) //string
	fmt.Println(reflect.TypeOf(true)) // bool
}

宣告變數

  • var q int
  • var q int = 4
  • p,q = 4,5 (居然有像python 一樣的多重賦值!)
  • p :=4 (快速寫法, 連型別都不用!)

命名規則

  1. 開頭必須是字母
  2. 如果開頭字母是大寫,表示他是可以被匯出的

陣列

在Go 裡面要宣告陣列,請用以下的code 得形式

// 第一種
var todos [2]string
todos[0]= "learning go !"
todos[1]= "use go to write an app"
// 第二種
var grades [3]int = [3]int{90,98,93}

// 第三種
heights := [3]int{90,98,93}

// go 裡面的foreach , 陣列/map資料型態都適用!
for index,note := range notes{
    fmt.Println(index,note)
}

切片

這是我在深入淺出Go 這本書裡面的某一個章節,他裡面寫道Go 宣告切片就像是宣告陣列變數, 只是不需要指定大小!for example:

var mySlice []int
mySlice = make([]int,7) // 設定七個數字的切片
fmt.Printf("%v",mySlice) //[0 0 0 0 0 0 0]
// 增加
newSlice := append(mySlice, 5,9) // 回傳新的, 增加完的切片
fmt.Printf("%v",newSlice) //[0 0 0 0 0 0 0 5 9]

錯誤處理

說真的,最近在學著Go 都覺得他的語言調性和其他語言差很多,像是if/for 的區域範圍變數無法用在if/for 後面(但之前的宣告可以使用)、陣列的宣告是很獨樹一幟的他也不像傳統程式語言那種try…catch 的敘述,而是你要分成你自己去處理或者直接error 中斷給你看這樣

像是這樣, 你必須用參數去接下可能會error 的地方,然後用if 去判斷, 控制壞掉之後的流程這樣。詳細你可以參考此連結:https://michaelchen.tech/golang-programming/error-handling/

或者你也可以使用 panic 這個關鍵字 或 用 recover 這個關鍵字讓他從panic 的狀態中恢復, 另外也筆記一下 defer 可以添加到任何地方,用來暫緩該調用直到目前程式結束

詳細可以參考此網址:https://openhome.cc/Gossip/Go/DeferPanicRecover.html

 file, err := os.Open("file.txt")         
 if err != nil {                         
     ....         
 }  
defer file.Close()                                    

Maps

有點像是python 裡的 dictionary , 或者 php 的 association array……

var myMap map[string]float64 // 宣告出以字串型態為index, float64型態的值
myMap = make(map[string]float64)

// 又或者可以做更簡單的宣告
myMap := make(map[string]float64)


// 如何把東西放進去...
myMap["Jimmy"] = 12.4
myMap["Bob"] = 15.3

// 如果已經知道要建立怎麼樣的map
myMap := map[string]float64{"Jimmy":12.4 , "Bob":15.3}

// 另外如果取得一個沒有被指派的index 會根據型態而回傳不同的值
// 數字:0
// 字串:""
// * 如果沒有make 它則會是一個nil 的 map, 而nil 的 map 無法被指派值
// 可以視情況給予第二個參數,好讓map 可以判斷是否有這個index
var value string
var exist bool
value , exist = myMap["Andy"] // return 0, false

// 移除
myMap["Jim"] = 999
delete(myMap,"Jim")

結構 struct

var myStruct struct {
    name string
    grade int
}
myStruct.name = "Jimmy"
myStruct.grade = 10

// 自訂型別, 前面使用type 這個關鍵字
type myStruct struct {
    name string
    grade int
}

// 自訂型別也能加入method
func (m myStruct) hello() string {
    return "hello"
}
// 自訂型別也可以加入getter /setter 封裝裡面的資料結構
func (m *myStruct) SetName(name string){
   m.name = name
}

func (m *myStruct) Name() string {
   return m.name
}


// 透過指標存取結構
func applyDiscount(s *subscriber){
    s.rate = 4.99
}

func main() {
    var s subscriber
    applyDiscount(&s)
}

結構裡面可以有另外一個結構,而Go另外還支援了匿名結構欄位,不用特別設定名稱也可以直接帶進去結構裡

type Employee struct {
    Name string
    Salary float64
    Address
}

type Address struct {
     ......
}

介面 interface

在 Go 裡面也有介面的概念,定義某些特定得值與某些特定的行為。

type myInterface interface {
   methodOne()
   methodTwo(float64)
   method() string
}

測試

在Go 裡面,我們可以使用testing 這個套件,首先是要讓我們在同樣的套件底下建立一個_test 結尾的Go 檔案,話不多說,show you some code !

func TestFunction(t *testing.T){
    ......(略)
    if ...(略)
    t.Errorf("......")
}

function programming

在Go 裡面, function 本身也能夠被視為變數, 型別處理,像是

// 此範例來自深入淺出Go p.439
func callFunction(passedFunction func()){
  passedFunction()
}

func callTwice(passedFunction func()){
  passedFunction()
  passedFunction()
}

func callWithArguments(passedFunction func(string, bool)){
  passedFunction("this sentence is",false)
}

func printReturnValue(passedFunction func() string){
  fmt.Println(passedfunction())
}

func functionA(){
  fmt.Println("function called")
}

func functionB() string{
    fmt.Println("function called")
    return "Returning from function"
}

func functionC(a string , b bool){
    fmt.Println("function called")
    fmt.Println(a,b)
}

func sayHi(){
  fmt.Println("Hi")
}

func main(){
    var myFunction = func()
    myFunction = sayHi
    myFunction()

    callFunction(functionA)
    callTwice(functionA)
    callWithArguments(functionC)
    printReturnValue(functionB)
}

相關指令

  • go build :編譯成二進位
  • go run :編譯及執行
  • go fmt :格式化原始碼
  • go version :go 的版本
  • go test : 測試go
  • go doc : 說明文件
小君曰:來學Go 啦, go !