Add more term fixes and markdown format fixes
This commit is contained in:
@@ -118,16 +118,18 @@ Linux 系統使用者可透過在 Terminal 中執行命令`arch`(即`uname -m`)
|
||||
### GVM
|
||||
|
||||
gvm 是第三方開發的 Go 多版本管理工具,類似 ruby 裡面的 rvm 工具。使用起來相當的方便,安裝 gvm 使用如下命令:
|
||||
```sh
|
||||
|
||||
```sh
|
||||
bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
|
||||
```
|
||||
安裝完成後我們就可以安裝 go 了:
|
||||
```sh
|
||||
|
||||
安裝完成後我們就可以安裝 go 了:
|
||||
|
||||
```sh
|
||||
gvm install go1.8.3
|
||||
gvm use go1.8.3
|
||||
```
|
||||
|
||||
也可以使用下面的命令,省去每次呼叫 gvm use 的麻煩:
|
||||
gvm use go1.8.3 --default
|
||||
|
||||
@@ -135,33 +137,38 @@ gvm use go1.8.3
|
||||
|
||||
### apt-get
|
||||
Ubuntu 是目前使用最多的 Linux 桌面系統,使用`apt-get`命令來管理軟體套件,我們可以透過下面的命令來安裝 Go,為了以後方便,應該把 `git` `mercurial` 也安裝上:
|
||||
```sh
|
||||
|
||||
```sh
|
||||
sudo apt-get install python-software-properties
|
||||
sudo add-apt-repository ppa:gophers/go
|
||||
sudo apt-get update
|
||||
sudo apt-get install golang-stable git-core mercurial
|
||||
```
|
||||
### wget
|
||||
```sh
|
||||
|
||||
### wget
|
||||
|
||||
```sh
|
||||
wget https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
|
||||
sudo tar -xzf go1.8.3.linux-amd64.tar.gz -C /usr/local
|
||||
```
|
||||
|
||||
配置環境變數:
|
||||
```sh
|
||||
|
||||
```sh
|
||||
export GOROOT=/usr/local/go
|
||||
export GOBIN=$GOROOT/bin
|
||||
export PATH=$PATH:$GOBIN
|
||||
export GOPATH=$HOME/gopath (可選設定)
|
||||
```
|
||||
|
||||
或者使用:
|
||||
|
||||
```sh
|
||||
sudo vim /etc/profile
|
||||
```
|
||||
|
||||
並新增下面的內容:
|
||||
|
||||
```sh
|
||||
export GOROOT=/usr/local/go
|
||||
export GOBIN=$GOROOT/bin
|
||||
@@ -170,9 +177,11 @@ export GOPATH=$HOME/gopath (可選設定)
|
||||
```
|
||||
|
||||
重新載入 profile 檔案
|
||||
|
||||
```sh
|
||||
source /etc/profile
|
||||
```
|
||||
|
||||
### homebrew
|
||||
homebrew 是 Mac 系統下面目前使用最多的管理軟體的工具,目前已支援 Go,可以透過命令直接安裝 Go,為了以後方便,應該把 `git` `mercurial` 也安裝上:
|
||||
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
*(注:這個不是 Go 安裝目錄。下面以筆者的工作目錄為範例,如果你想不一樣請把 GOPATH 替換成你的工作目錄。)*
|
||||
|
||||
在類別 Unix 環境下大概這樣設定:
|
||||
|
||||
```sh
|
||||
export GOPATH=/home/apple/mygo
|
||||
```
|
||||
為了方便,應該建立以上資料夾,並且上一行加入到 `.bashrc` 或者 `.zshrc` 或者自己的 `sh` 的配置檔案中。
|
||||
|
||||
Windows 設定如下,建立一個環境變數名稱叫做 GOPATH:
|
||||
|
||||
```sh
|
||||
GOPATH=c:\mygo
|
||||
```
|
||||
@@ -40,6 +42,7 @@ GOPATH 下的 src 目錄就是接下來開發程式的主要目錄,所有的
|
||||
|
||||
|
||||
下面我就以 mymath 為例來講述如何編寫套件,執行如下程式碼
|
||||
|
||||
```sh
|
||||
cd $GOPATH/src
|
||||
mkdir mymath
|
||||
@@ -68,6 +71,7 @@ func Sqrt(x float64) float64 {
|
||||
2、在任意的目錄執行如下程式碼`go install mymath`
|
||||
|
||||
安裝完之後,我們可以進入如下目錄
|
||||
|
||||
```sh
|
||||
cd $GOPATH/pkg/${GOOS}_${GOARCH}
|
||||
//可以看到如下檔案
|
||||
@@ -103,11 +107,13 @@ func main() {
|
||||
可以看到這個的 package 是`main`,import 裡面呼叫的套件是`mymath`,這個就是相對於`$GOPATH/src`的路徑,如果是多階層目錄,就在 import 裡面引入多階層目錄,如果你有多個 GOPATH,也是一樣,Go 會自動在多個`$GOPATH/src`中尋找。
|
||||
|
||||
如何編譯程式呢?進入該應用目錄,然後執行`go build`,那麼在該目錄下面會產生一個 mathapp 的可執行檔案
|
||||
|
||||
```sh
|
||||
./mathapp
|
||||
```
|
||||
|
||||
輸出如下內容
|
||||
|
||||
```sh
|
||||
Hello, world. Sqrt(2) = 1.414213562373095
|
||||
```
|
||||
|
||||
@@ -222,6 +222,7 @@ VSCode 程式碼設定可用於 Go 擴充套件。這些都可以在使用者的
|
||||
```
|
||||
|
||||
接著安裝相依套件支援(網路不穩定,請直接到 Github [Golang](https://github.com/golang) 下載再移動到相關目錄):
|
||||
|
||||
```Go
|
||||
go get -u -v github.com/nsf/gocode
|
||||
go get -u -v github.com/rogpeppe/godef
|
||||
@@ -243,9 +244,10 @@ VSCode 還有一項很強大的功能就是斷點除錯,結合 [delve](https:/
|
||||
go get -v -u github.com/peterh/liner github.com/derekparker/delve/cmd/dlv
|
||||
|
||||
brew install go-delve/delve/delve(mac 可選)
|
||||
|
||||
```
|
||||
|
||||
如果有問題再來一遍:
|
||||
|
||||
```Go
|
||||
go get -v -u github.com/peterh/liner github.com/derekparker/delve/cmd/dlv
|
||||
```
|
||||
@@ -285,6 +287,7 @@ Atom 是 Github 基於 Electron 和 web 技術建構的開源編輯器, 是一
|
||||
go-plus 是 Atom 上面的一款開源的 go 語言開發環境的的外掛
|
||||
|
||||
它需要依賴下面的 go 語言工具:
|
||||
|
||||
```Go
|
||||
1.autocomplete-go :gocode 的程式碼自動提示
|
||||
2.gofmt :使用 goftm,goimports,goturns
|
||||
@@ -293,8 +296,8 @@ Atom 是 Github 基於 Electron 和 web 技術建構的開源編輯器, 是一
|
||||
5.navigator-godef:godef
|
||||
6.tester-goo :go test
|
||||
7.gorename :rename
|
||||
|
||||
```
|
||||
|
||||
在 Atom 中的 Preference 中可以找到 install 選單,輸入 go-plus,然後點選安裝(install)
|
||||
|
||||
就會開始安裝 go-plus , go-plus 外掛會自動安裝對應的依賴外掛,如果沒有安裝對應的 go 的類別函式庫會自動執行: go get 安裝。
|
||||
@@ -342,8 +345,8 @@ Plugin 'gmarik/Vundle.vim'
|
||||
" All of your Plugins must be added before the following line
|
||||
call vundle#end() " required
|
||||
filetype plugin indent on " required
|
||||
|
||||
```
|
||||
|
||||
2.安裝 Vim-go
|
||||
|
||||
修改~/.vimrc,在 vundle#begin 和 vundle#end 間增加一行:
|
||||
@@ -357,6 +360,7 @@ Plugin 'fatih/vim-go'
|
||||
|
||||
3.安裝 YCM(Your Complete Me)進行自動自動完成
|
||||
在~/.vimrc 中新增一行:
|
||||
|
||||
```sh
|
||||
|
||||
Plugin 'Valloric/YouCompleteMe'
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
這就像一個傳統,在學習大部分語言之前,你先學會如何編寫一個可以輸出`hello world`的程式。
|
||||
|
||||
準備好了嗎?Let's Go!
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
@@ -7,26 +7,26 @@
|
||||
Go 語言裡面定義變數有多種方式。
|
||||
|
||||
使用 `var` 關鍵字是 Go 最基本的定義變數方式,與 C 語言不同的是 Go 把變數型別放在變數名後面:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
//定義一個名稱為“variableName”,型別為"type"的變數
|
||||
var variableName type
|
||||
```
|
||||
定義多個變數
|
||||
```Go
|
||||
|
||||
```Go
|
||||
//定義三個型別都是“type”的變數
|
||||
var vname1, vname2, vname3 type
|
||||
```
|
||||
定義變數並初始化值
|
||||
```Go
|
||||
|
||||
```Go
|
||||
//初始化“variableName”的變數為“value”值,型別是“type”
|
||||
var variableName type = value
|
||||
```
|
||||
同時初始化多個變數
|
||||
```Go
|
||||
|
||||
```Go
|
||||
/*
|
||||
定義三個型別都是"type"的變數,並且分別初始化為相應的值
|
||||
vname1 為 v1,vname2 為 v2,vname3 為 v3
|
||||
@@ -34,8 +34,8 @@ var variableName type = value
|
||||
var vname1, vname2, vname3 type= v1, v2, v3
|
||||
```
|
||||
你是不是覺得上面這樣的定義有點繁瑣?沒關係,因為 Go 語言的設計者也發現了,有一種寫法可以讓它變得簡單一點。我們可以直接忽略型別宣告,那麼上面的程式碼變成這樣了:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
/*
|
||||
定義三個變數,它們分別初始化為相應的值
|
||||
vname1 為 v1,vname2 為 v2,vname3 為 v3
|
||||
@@ -44,8 +44,8 @@ var vname1, vname2, vname3 type= v1, v2, v3
|
||||
var vname1, vname2, vname3 = v1, v2, v3
|
||||
```
|
||||
你覺得上面的還是有些繁瑣?好吧,我也覺得。讓我們繼續簡化:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
/*
|
||||
定義三個變數,它們分別初始化為相應的值
|
||||
vname1 為 v1,vname2 為 v2,vname3 為 v3
|
||||
@@ -60,8 +60,8 @@ vname1, vname2, vname3 := v1, v2, v3
|
||||
_, b := 34, 35
|
||||
|
||||
Go 對於已宣告但未使用的變數會在編譯階段報錯,比如下面的程式碼就會產生一個錯誤:宣告了 `i` 但未使用。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
func main() {
|
||||
@@ -73,15 +73,15 @@ func main() {
|
||||
所謂常數,也就是在程式編譯階段就確定下來的值,而程式在執行時無法改變該值。在 Go 程式中,常數可定義為數值、布林值或字串等型別。
|
||||
|
||||
它的語法如下:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
const constantName = value
|
||||
//如果需要,也可以明確指定常數的型別:
|
||||
const Pi float32 = 3.1415926
|
||||
```
|
||||
下面是一些常數宣告的例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
const Pi = 3.1415926
|
||||
const i = 10000
|
||||
const MaxThread = 10
|
||||
@@ -95,8 +95,8 @@ Go 常數和一般程式語言不同的是,可以指定相當多的小數位
|
||||
### Boolean
|
||||
|
||||
在 Go 中,布林值的型別為`bool`,值是 `true` 或`false`,預設為`false`。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
//範例程式碼
|
||||
var isActive bool // 全域性變數宣告
|
||||
var enabled, disabled = true, false // 忽略型別的宣告
|
||||
@@ -126,8 +126,8 @@ func test() {
|
||||
浮點數的型別有 `float32` 和`float64`兩種(沒有 `float` 型別),預設是`float64`。
|
||||
|
||||
這就是全部嗎?No!Go 還支援複數。它的預設型別是`complex128`(64 位實數+64 位虛數)。如果需要小一些的,也有`complex64`(32 位實數+32 位虛數)。複數的形式為`RE + IMi`,其中 `RE` 是實數部分,`IM`是虛數部分,而最後的 `i` 是虛數單位。下面是一個使用複數的例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var c complex64 = 5+5i
|
||||
//output: (5+5i)
|
||||
fmt.Printf("Value is: %v", c)
|
||||
@@ -136,8 +136,8 @@ fmt.Printf("Value is: %v", c)
|
||||
### 字串
|
||||
|
||||
我們在上一節中講過,Go 中的字串都是採用`UTF-8`字符集編碼。字串是用一對雙引號(`""`)或反引號(`` ` `` `` ` ``)括起來定義,它的型別是`string`。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
//範例程式碼
|
||||
var frenchHello string // 宣告變數為字串的一般方法
|
||||
var emptyString string = "" // 宣告了一個字串變數,初始化為空字串
|
||||
@@ -148,15 +148,15 @@ func test() {
|
||||
}
|
||||
```
|
||||
在 Go 中字串是不可變的,例如下面的程式碼編譯時會報錯:cannot assign to s[0]
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var s string = "hello"
|
||||
s[0] = 'c'
|
||||
|
||||
```
|
||||
但如果真的想要修改怎麼辦呢?下面的程式碼可以實現:
|
||||
```Go
|
||||
|
||||
但如果真的想要修改怎麼辦呢?下面的程式碼可以實現:
|
||||
|
||||
```Go
|
||||
s := "hello"
|
||||
c := []byte(s) // 將字串 s 轉換為 []byte 型別
|
||||
c[0] = 'c'
|
||||
@@ -165,16 +165,16 @@ fmt.Printf("%s\n", s2)
|
||||
```
|
||||
|
||||
Go 中可以使用`+`運算子來連線兩個字串:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
s := "hello,"
|
||||
m := " world"
|
||||
a := s + m
|
||||
fmt.Printf("%s\n", a)
|
||||
```
|
||||
修改字串也可寫為:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
s := "hello"
|
||||
s = "c" + s[1:] // 字串雖不能更改,但可進行切片(slice)操作
|
||||
fmt.Printf("%s\n", s)
|
||||
@@ -191,8 +191,8 @@ fmt.Printf("%s\n", s)
|
||||
|
||||
### 錯誤型別
|
||||
Go 內建有一個 `error` 型別,專門用來處理錯誤資訊,Go 的 `package` 裡面還專門有一個套件 `errors` 來處理錯誤:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
err := errors.New("emit macho dwarf: elf header corrupted")
|
||||
if err != nil {
|
||||
fmt.Print(err)
|
||||
@@ -213,8 +213,8 @@ if err != nil {
|
||||
在 Go 語言中,同時宣告多個常數、變數,或者匯入多個套件時,可採用分組的方式進行宣告。
|
||||
|
||||
例如下面的程式碼:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
import "fmt"
|
||||
import "os"
|
||||
|
||||
@@ -227,8 +227,8 @@ var pi float32
|
||||
var prefix string
|
||||
```
|
||||
可以分組寫成如下形式:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
import(
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -249,8 +249,8 @@ var(
|
||||
### iota 列舉
|
||||
|
||||
Go 裡面有一個關鍵字 `iota`,這個關鍵字用來宣告 `enum` 的時候採用,它預設開始值是 0,const 中每增加一行加 1:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -293,13 +293,13 @@ Go 之所以會那麼簡潔,是因為它有一些預設的行為:
|
||||
|
||||
### array
|
||||
`array`就是陣列,它的定義方式如下:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var arr [n]type
|
||||
```
|
||||
在 `[n]type` 中,`n`表示陣列的長度,`type`表示儲存元素的型別。對陣列的操作和其它語言類似,都是透過 `[]` 來進行讀取或賦值:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var arr [10]int // 宣告了一個 int 型別的陣列
|
||||
arr[0] = 42 // 陣列下標是從 0 開始的
|
||||
arr[1] = 13 // 賦值操作
|
||||
@@ -309,8 +309,8 @@ fmt.Printf("The last element is %d\n", arr[9]) //回傳未賦值的最後一個
|
||||
由於長度也是陣列型別的一部分,因此 `[3]int` 與`[4]int`是不同的型別,陣列也就不能改變長度。陣列之間的賦值是值的賦值,即當把一個陣列作為參數傳入函式的時候,傳入的其實是該陣列的副本,而不是它的指標。如果要使用指標,那麼就需要用到後面介紹的 `slice` 型別了。
|
||||
|
||||
陣列可以使用另一種 `:=` 來宣告
|
||||
```Go
|
||||
|
||||
```Go
|
||||
a := [3]int{1, 2, 3} // 宣告了一個長度為 3 的 int 陣列
|
||||
|
||||
b := [10]int{1, 2, 3} // 宣告了一個長度為 10 的 int 陣列,其中前三個元素初始化為 1、2、3,其它預設為 0
|
||||
@@ -318,8 +318,8 @@ b := [10]int{1, 2, 3} // 宣告了一個長度為 10 的 int 陣列,其中前
|
||||
c := [...]int{4, 5, 6} // 可以省略長度而採用`...`的方式,Go 會自動根據元素個數來計算長度
|
||||
```
|
||||
也許你會說,我想陣列裡面的值還是陣列,能實現嗎?當然囉,Go 支援巢狀陣列,即多維陣列。比如下面的程式碼就宣告了一個二維陣列:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
// 宣告了一個二維陣列,該陣列以兩個陣列作為元素,其中每個陣列中又有 4 個 int 型別的元素
|
||||
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
|
||||
|
||||
@@ -338,19 +338,19 @@ easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
|
||||
在很多應用場景中,陣列並不能滿足我們的需求。在初始定義陣列時,我們並不知道需要多大的陣列,因此我們就需要“動態陣列”。在 Go 裡面這種資料結構叫`slice`
|
||||
|
||||
`slice`並不是真正意義上的動態陣列,而是一個參考型別。`slice`總是指向一個底層`array`,`slice`的宣告也可以像 `array` 一樣,只是不需要長度。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
// 和宣告 array 一樣,只是少了長度
|
||||
var fslice []int
|
||||
```
|
||||
接下來我們可以宣告一個`slice`,並初始化資料,如下所示:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
slice := []byte {'a', 'b', 'c', 'd'}
|
||||
```
|
||||
`slice`可以從一個陣列或一個已經存在的 `slice` 中再次宣告。`slice`透過 `array[i:j]` 來取得,其中 `i` 是陣列的開始位置,`j`是結束位置,但不包含`array[j]`,它的長度是`j-i`。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
// 宣告一個含有 10 個元素元素型別為 byte 的陣列
|
||||
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
|
||||
|
||||
@@ -380,8 +380,8 @@ slice 有一些簡便的操作
|
||||
- 如果從一個陣列裡面直接取得`slice`,可以這樣`ar[:]`,因為預設第一個序列是 0,第二個是陣列的長度,即等價於`ar[0:len(ar)]`
|
||||
|
||||
下面這個例子展示了更多關於 `slice` 的操作:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
// 宣告一個陣列
|
||||
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
|
||||
// 宣告兩個 slice
|
||||
@@ -405,8 +405,8 @@ bSlice = aSlice[:] // bSlice 包含所有 aSlice 的元素: d,e,f,g
|
||||
- 一個指標,指向陣列中 `slice` 指定的開始位置
|
||||
- 長度,即 `slice` 的長度
|
||||
- 最大長度,也就是 `slice` 開始位置到陣列的最後位置的長度
|
||||
```Go
|
||||
|
||||
```Go
|
||||
Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
|
||||
Slice_a := Array_a[2:5]
|
||||
```
|
||||
@@ -427,8 +427,8 @@ bSlice = aSlice[:] // bSlice 包含所有 aSlice 的元素: d,e,f,g
|
||||
但當 `slice` 中沒有剩餘空間(即`(cap-len) == 0`)時,此時將動態分配新的陣列空間。回傳的 `slice` 陣列指標將指向這個空間,而原陣列的內容將保持不變;其它參考此陣列的 `slice` 則不受影響。
|
||||
|
||||
從 Go1.2 開始 slice 支援了三個參數的 slice,之前我們一直採用這種方式在 slice 或者 array 基礎上來取得一個 slice
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var array [10]int
|
||||
slice := array[2:4]
|
||||
```
|
||||
@@ -445,8 +445,8 @@ slice := array[2:4]
|
||||
`map`也就是 Python 中字典的概念,它的格式為`map[keyType]valueType`
|
||||
|
||||
我們看下面的程式碼,`map`的讀取和設定也類似 `slice` 一樣,透過 `key` 來操作,只是 `slice` 的`index`只能是`int`型別,而 `map` 多了很多型別,可以是`int`,可以是 `string` 及所有完全定義了 `==` 與`!=`操作的型別。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
// 宣告一個 key 是字串,值為 int 的字典,這種方式的宣告需要在使用之前使用 make 初始化
|
||||
var numbers map[string]int
|
||||
// 另一種 map 的宣告方式
|
||||
@@ -473,7 +473,6 @@ fmt.Println("第三個數字是: ", numbers["three"]) // 讀取資料
|
||||
透過 `delete` 刪除 `map` 的元素:
|
||||
|
||||
```Go
|
||||
|
||||
// 初始化一個字典
|
||||
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
|
||||
// map 有兩個回傳值,第二個回傳值,如果不存在 key,那麼 ok 為 false,如果存在 ok 為 true
|
||||
@@ -485,17 +484,17 @@ if ok {
|
||||
}
|
||||
|
||||
delete(rating, "C") // 刪除 key 為 C 的元素
|
||||
|
||||
```
|
||||
上面說過了,`map`也是一種參考型別,如果兩個 `map` 同時指向一個底層,那麼一個改變,另一個也相應的改變:
|
||||
```Go
|
||||
|
||||
上面說過了,`map`也是一種參考型別,如果兩個 `map` 同時指向一個底層,那麼一個改變,另一個也相應的改變:
|
||||
|
||||
```Go
|
||||
m := make(map[string]string)
|
||||
m["Hello"] = "Bonjour"
|
||||
m1 := m
|
||||
m1["Hello"] = "Salut" // 現在 m["hello"]的值已經是 Salut 了
|
||||
|
||||
```
|
||||
|
||||
### make、new 操作
|
||||
|
||||
`make`用於內建型別(`map`、`slice` 和`channel`)的記憶體分配。`new`用於各種型別的記憶體分配。
|
||||
@@ -518,8 +517,8 @@ m1["Hello"] = "Salut" // 現在 m["hello"]的值已經是 Salut 了
|
||||
## 零值
|
||||
關於“零值”,所指並非是空值,而是一種“變數未填充前”的預設值,通常為 0。
|
||||
此處羅列 部分類型 的 “零值”
|
||||
```Go
|
||||
|
||||
```Go
|
||||
int 0
|
||||
int8 0
|
||||
int32 0
|
||||
@@ -531,8 +530,8 @@ float32 0 //長度為 4 byte
|
||||
float64 0 //長度為 8 byte
|
||||
bool false
|
||||
string ""
|
||||
|
||||
```
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一章: [你好,Go](<02.1.md>)
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
`if`也許是各種程式語言中最常見的了,它的語法概括起來就是:如果滿足條件就做某事,否則做另一件事。
|
||||
|
||||
Go 裡面 `if` 條件判斷語句中不需要括號,如下程式碼所示
|
||||
```Go
|
||||
|
||||
```Go
|
||||
if x > 10 {
|
||||
fmt.Println("x is greater than 10")
|
||||
} else {
|
||||
@@ -15,8 +15,8 @@ if x > 10 {
|
||||
}
|
||||
```
|
||||
Go 的 `if` 還有一個強大的地方就是條件判斷語句裡面允許宣告一個變數,這個變數的作用域只能在該條件邏輯區塊內,其他地方就無法使用,如下所示
|
||||
```Go
|
||||
|
||||
```Go
|
||||
// 計算取得值 x,然後根據 x 回傳的大小,判斷是否大於 10。
|
||||
if x := computedValue(); x > 10 {
|
||||
fmt.Println("x is greater than 10")
|
||||
@@ -28,8 +28,8 @@ if x := computedValue(); x > 10 {
|
||||
fmt.Println(x)
|
||||
```
|
||||
多個條件的時候如下所示:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
if integer == 3 {
|
||||
fmt.Println("The integer is equal to 3")
|
||||
} else if integer < 3 {
|
||||
@@ -41,8 +41,8 @@ if integer == 3 {
|
||||
### goto
|
||||
|
||||
Go 有 `goto` 語句——請明智地使用它。用 `goto` 跳轉到必須在當前函式內定義的標籤。例如假設這樣一個迴圈:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func myFunc() {
|
||||
i := 0
|
||||
Here: //這行的第一個詞,以冒號結束作為標籤
|
||||
@@ -55,8 +55,8 @@ Here: //這行的第一個詞,以冒號結束作為標籤
|
||||
|
||||
### for
|
||||
Go 裡面最強大的一個控制邏輯就是`for`,它既可以用來迴圈讀取資料,又可以當作 `while` 來控制邏輯,還能迭代操作。它的語法如下:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
for expression1; expression2; expression3 {
|
||||
//...
|
||||
}
|
||||
@@ -64,8 +64,8 @@ for expression1; expression2; expression3 {
|
||||
`expression1`、`expression2`和 `expression3` 都是表示式,其中 `expression1` 和`expression3`是變數宣告或者函式呼叫回傳值之類別的,`expression2`是用來條件判斷,`expression1`在迴圈開始之前呼叫,`expression3`在每輪迴圈結束之時呼叫。
|
||||
|
||||
一個例子比上面講那麼多更有用,那麼我們看看下面的例子吧:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -83,24 +83,24 @@ func main(){
|
||||
|
||||
|
||||
有些時候如果我們忽略 `expression1` 和`expression3`:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
sum := 1
|
||||
for ; sum < 1000; {
|
||||
sum += sum
|
||||
}
|
||||
```
|
||||
其中 `;` 也可以省略,那麼就變成如下的程式碼了,是不是似曾相識?對,這就是 `while` 的功能。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
sum := 1
|
||||
for sum < 1000 {
|
||||
sum += sum
|
||||
}
|
||||
```
|
||||
在迴圈裡面有兩個關鍵操作 `break` 和`continue` ,`break`操作是跳出當前迴圈,`continue`是跳過本次迴圈。當巢狀過深的時候,`break`可以配合標籤使用,即跳轉至標籤所指定的位置,詳細參考如下例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
for index := 10; index>0; index-- {
|
||||
if index == 5{
|
||||
break // 或者 continue
|
||||
@@ -113,8 +113,8 @@ for index := 10; index>0; index-- {
|
||||
`break`和 `continue` 還可以跟著標號,用來跳到多重迴圈中的外層迴圈
|
||||
|
||||
`for`配合 `range` 可以用於讀取 `slice` 和`map`的資料:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
for k,v:=range map {
|
||||
fmt.Println("map's key:",k)
|
||||
fmt.Println("map's val:",v)
|
||||
@@ -122,17 +122,17 @@ for k,v:=range map {
|
||||
```
|
||||
由於 Go 支援 “多值回傳”, 而對於“宣告而未被呼叫”的變數, 編譯器會報錯, 在這種情況下, 可以使用 `_` 來丟棄不需要的回傳值
|
||||
例如
|
||||
```Go
|
||||
|
||||
```Go
|
||||
for _, v := range map{
|
||||
fmt.Println("map's val:", v)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### switch
|
||||
有些時候你需要寫很多的`if-else`來實現一些邏輯處理,這個時候程式碼看上去就很醜很冗長,而且也不易於以後的維護,這個時候 `switch` 就能很好的解決這個問題。它的語法如下
|
||||
```Go
|
||||
|
||||
```Go
|
||||
switch sExpr {
|
||||
case expr1:
|
||||
some instructions
|
||||
@@ -145,8 +145,8 @@ default:
|
||||
}
|
||||
```
|
||||
`sExpr`和`expr1`、`expr2`、`expr3`的型別必須一致。Go 的 `switch` 非常靈活,表示式不必是常數或整數,執行的過程從上至下,直到找到匹配項;而如果 `switch` 沒有表示式,它會匹配`true`。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
i := 10
|
||||
switch i {
|
||||
case 1:
|
||||
@@ -160,8 +160,8 @@ default:
|
||||
}
|
||||
```
|
||||
在第 5 行中,我們把很多值聚合在了一個 `case` 裡面,同時,Go 裡面 `switch` 預設相當於每個 `case` 最後帶有`break`,匹配成功後不會自動向下執行其他 case,而是跳出整個`switch`, 但是可以使用 `fallthrough` 強制執行後面的 case 程式碼。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
integer := 6
|
||||
switch integer {
|
||||
case 4:
|
||||
@@ -184,18 +184,18 @@ default:
|
||||
}
|
||||
```
|
||||
上面的程式將輸出
|
||||
```Go
|
||||
|
||||
```Go
|
||||
The integer was <= 6
|
||||
The integer was <= 7
|
||||
The integer was <= 8
|
||||
default case
|
||||
|
||||
```
|
||||
|
||||
## 函式
|
||||
函式是 Go 裡面的核心設計,它透過關鍵字 `func` 來宣告,它的格式如下:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
|
||||
//這裡是處理邏輯程式碼
|
||||
//回傳多個值
|
||||
@@ -213,8 +213,8 @@ func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
|
||||
- 如果有回傳值, 那麼必須在函式的外層新增 return 語句
|
||||
|
||||
下面我們來看一個實際應用函式的例子(用來計算 Max 值)
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -246,8 +246,8 @@ func main() {
|
||||
Go 語言比 C 更先進的特性,其中一點就是函式能夠回傳多個值。
|
||||
|
||||
我們直接上程式碼看例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -268,8 +268,8 @@ func main() {
|
||||
}
|
||||
```
|
||||
上面的例子我們可以看到直接回傳了兩個參數,當然我們也可以命名回傳參數的變數,這個例子裡面只是用了兩個型別,我們也可以改成如下這樣的定義,然後回傳的時候不用帶上變數名,因為直接在函式裡面初始化了。但如果你的函式是匯出的(首字母大寫),官方建議:最好命名回傳值,因為不命名回傳值,雖然使得程式碼更加簡潔了,但是會造成產生的文件可讀性差。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func SumAndProduct(A, B int) (add int, Multiplied int) {
|
||||
add = A+B
|
||||
Multiplied = A*B
|
||||
@@ -328,8 +328,8 @@ func main() {
|
||||
那你也許會問了,如果真的需要傳這個 `x` 本身,該怎麼辦呢?
|
||||
|
||||
這就牽扯到了所謂的指標。我們知道,變數在記憶體中是存放於一定地址上的,修改變數實際是修改變數地址處的記憶體。只有 `add1` 函式知道 `x` 變數所在的地址,才能修改 `x` 變數的值。所以我們需要將 `x` 所在地址`&x`傳入函式,並將函式的參數的型別由 `int` 改為`*int`,即改為指標型別,才能在函式中修改 `x` 變數的值。此時參數仍然是按 copy 傳遞的,只是 copy 的是一個指標。請看下面的例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -359,8 +359,8 @@ func main() {
|
||||
|
||||
### defer
|
||||
Go 語言中有種不錯的設計,即延遲(defer)語句,你可以在函式中新增多個 defer 語句。當函式執行到最後時,這些 defer 語句會按照逆序執行,最後該函式回傳。特別是當你在進行一些開啟資源的操作時,遇到錯誤需要提前回傳,在回傳前你需要關閉相應的資源,不然很容易造成資源洩露等問題。如下程式碼所示,我們一般寫開啟一個資源是這樣操作的:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func ReadWrite() bool {
|
||||
file.Open("file")
|
||||
// 做一些工作
|
||||
@@ -379,8 +379,8 @@ func ReadWrite() bool {
|
||||
}
|
||||
```
|
||||
我們看到上面有很多重複的程式碼,Go 的 `defer` 有效解決了這個問題。使用它後,不但程式碼量減少了很多,而且程式變得更優雅。在 `defer` 後指定的函式會在函式退出前呼叫。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func ReadWrite() bool {
|
||||
file.Open("file")
|
||||
defer file.Close()
|
||||
@@ -394,8 +394,8 @@ func ReadWrite() bool {
|
||||
}
|
||||
```
|
||||
如果有很多呼叫`defer`,那麼 `defer` 是採用後進先出模式,所以如下程式碼會輸出`4 3 2 1 0`
|
||||
```Go
|
||||
|
||||
```Go
|
||||
for i := 0; i < 5; i++ {
|
||||
defer fmt.Printf("%d ", i)
|
||||
}
|
||||
@@ -407,8 +407,8 @@ for i := 0; i < 5; i++ {
|
||||
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
|
||||
|
||||
函式作為型別到底有什麼好處呢?那就是可以把這個型別的函式當做值來傳遞,請看下面的例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -463,8 +463,8 @@ Recover
|
||||
>是一個內建的函式,可以讓進入 `panic` 狀態的 `goroutine` 恢復過來。`recover`僅在延遲函式中有效。在正常的執行過程中,呼叫 `recover` 會回傳`nil`,並且沒有其它任何效果。如果當前的 `goroutine` 陷入 `panic` 狀態,呼叫 `recover` 可以捕獲到 `panic` 的輸入值,並且恢復正常的執行。
|
||||
|
||||
下面這個函式示範了如何在過程中使用`panic`
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var user = os.Getenv("USER")
|
||||
|
||||
func init() {
|
||||
@@ -474,8 +474,8 @@ func init() {
|
||||
}
|
||||
```
|
||||
下面這個函式檢查作為其參數的函式在執行時是否會產生`panic`:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func throwsPanic(f func()) (b bool) {
|
||||
defer func() {
|
||||
if x := recover(); x != nil {
|
||||
@@ -500,15 +500,15 @@ Go 程式會自動呼叫`init()`和`main()`,所以你不需要在任何地方
|
||||
|
||||
### import
|
||||
我們在寫 Go 程式碼的時候經常用到 import 這個命令用來匯入套件檔案,而我們經常看到的方式參考如下:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
import(
|
||||
"fmt"
|
||||
)
|
||||
```
|
||||
然後我們程式碼裡面可以透過如下的方式呼叫
|
||||
```Go
|
||||
|
||||
```Go
|
||||
fmt.Println("hello world")
|
||||
```
|
||||
上面這個 fmt 是 Go 語言的標準函式庫,其實是去 `GOROOT` 環境變數指定目錄下去載入該模組,當然 Go 的 import 還支援如下兩種方式來載入自己寫的模組:
|
||||
@@ -548,8 +548,8 @@ fmt.Println("hello world")
|
||||
3. _操作
|
||||
|
||||
這個操作經常是讓很多人難以理解的一個運算子,請看下面這個 import
|
||||
```Go
|
||||
|
||||
```Go
|
||||
import (
|
||||
"database/sql"
|
||||
_ "github.com/ziutek/mymysql/godrv"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 2.4 struct 型別
|
||||
## struct
|
||||
Go 語言中,也和 C 或者其他語言一樣,我們可以宣告新的型別,作為其它型別的屬性或欄位的容器。例如,我們可以建立一個自訂型別 `person` 代表一個人的實體。這個實體擁有屬性:姓名和年齡。這樣的型別我們稱之`struct`。如下程式碼所示:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
@@ -13,8 +13,8 @@ type person struct {
|
||||
- 一個 int 型別的欄位 age,用來儲存使用者年齡這個屬性
|
||||
|
||||
如何使用 struct 呢?請看下面的程式碼
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type person struct {
|
||||
name string
|
||||
age int
|
||||
@@ -41,8 +41,8 @@ fmt.Printf("The person's name is %s", P.name) // 訪問 P 的 name 屬性.
|
||||
P := new(person)
|
||||
|
||||
下面我們看一個完整的使用 struct 的例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -94,8 +94,8 @@ func main() {
|
||||
當匿名欄位是一個 struct 的時候,那麼這個 struct 所擁有的全部欄位都被隱含的引入了當前定義的這個 struct。
|
||||
|
||||
讓我們來看一個例子,讓上面說的這些更具體化
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -141,14 +141,14 @@ func main() {
|
||||
圖 2.7 struct 組合,Student 組合了 Human struct 和 string 基本型別
|
||||
|
||||
我們看到 Student 訪問屬性 age 和 name 的時候,就像訪問自己所有用的欄位一樣,對,匿名欄位就是這樣,能夠實現欄位的繼承。是不是很酷啊?還有比這個更酷的呢,那就是 student 還能訪問 Human 這個欄位作為欄位名。請看下面的程式碼,是不是更酷了。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
mark.Human = Human{"Marcus", 55, 220}
|
||||
mark.Human.age -= 1
|
||||
```
|
||||
透過匿名訪問和修改欄位相當的有用,但是不僅僅是 struct 欄位哦,所有的內建型別和自訂型別都是可以作為匿名欄位的。請看下面的例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -194,8 +194,8 @@ func main() {
|
||||
Go 裡面很簡單的解決了這個問題,最外層的優先訪問,也就是當你透過`student.phone`訪問的時候,是訪問 student 裡面的欄位,而不是 human 裡面的欄位。
|
||||
|
||||
這樣就允許我們去過載透過匿名欄位繼承的一些欄位,當然如果我們想訪問過載後對應匿名型別裡面的欄位,可以透過匿名欄位名來訪問。請看下面的例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# 2.5 物件導向
|
||||
|
||||
前面兩章我們介紹了函式和 struct,那你是否想過函式當作 struct 的欄位一樣來處理呢?今天我們就講解一下函式的另一種形態,帶有接收者的函式,我們稱為`method`
|
||||
|
||||
## method
|
||||
現在假設有這麼一個場景,你定義了一個 struct 叫做長方形,你現在想要計算他的面積,那麼按照我們一般的思路應該會用下面的方式來實現
|
||||
```Go
|
||||
|
||||
現在假設有這麼一個場景,你定義了一個 struct 叫做長方形,你現在想要計算他的面積,那麼按照我們一般的思路應該會用下面的方式來實現
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -51,8 +53,8 @@ method 的語法如下:
|
||||
func (r ReceiverType) funcName(parameters) (results)
|
||||
|
||||
下面我們用最開始的例子用 method 來實現:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -108,13 +110,13 @@ func main() {
|
||||
>值得說明的一點是,圖示中 method 用虛線標出,意思是此處方法的 Receiver 是以值傳遞,而非參考傳遞,是的,Receiver 還可以是指標, 兩者的差別在於, 指標作為 Receiver 會對實體物件的內容發生操作,而普通型別作為 Receiver 僅僅是以副本作為操作物件,並不對原實體物件發生操作。後文對此會有詳細論述。
|
||||
|
||||
那是不是 method 只能作用在 struct 上面呢?當然不是囉,他可以定義在任何你自訂的型別、內建型別、struct 等各種型別上面。這裡你是不是有點迷糊了,什麼叫自訂型別,自訂型別不就是 struct 嘛,不是這樣的哦,struct 只是自訂型別裡面一種比較特殊的型別而已,還有其他自訂型別宣告,可以透過如下這樣的宣告來實現。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type typeName typeLiteral
|
||||
```
|
||||
請看下面這個宣告自訂型別的程式碼
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type ages int
|
||||
|
||||
type money float32
|
||||
@@ -133,8 +135,8 @@ m := months {
|
||||
好了,讓我們回到`method`
|
||||
|
||||
你可以在任何的自訂型別中定義任意多的`method`,接下來讓我們看一個複雜一點的例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -226,6 +228,7 @@ func main() {
|
||||
上面的程式碼透過文字描述出來之後是不是很簡單?我們一般解決問題都是透過問題的描述,去寫相應的程式碼實現。
|
||||
|
||||
### 指標作為 receiver
|
||||
|
||||
現在讓我們回過頭來看看 SetColor 這個 method,它的 receiver 是一個指向 Box 的指標,是的,你可以使用*Box。想想為啥要使用指標而不是 Box 本身呢?
|
||||
|
||||
我們定義 SetColor 的真正目的是想改變這個 Box 的顏色,如果不傳 Box 的指標,那麼 SetColor 接受的其實是 Box 的一個 copy,也就是說 method 內對於顏色值的修改,其實只作用於 Box 的 copy,而不是真正的 Box。所以我們需要傳入指標。
|
||||
@@ -251,8 +254,8 @@ func main() {
|
||||
### method 繼承
|
||||
|
||||
前面一章我們學習了欄位的繼承,那麼你也會發現 Go 的一個神奇之處,method 也是可以繼承的。如果匿名欄位實現了一個 method,那麼包含這個匿名欄位的 struct 也能呼叫該 method。讓我們來看下面這個例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -286,10 +289,12 @@ func main() {
|
||||
sam.SayHi()
|
||||
}
|
||||
```
|
||||
### method 重寫
|
||||
上面的例子中,如果 Employee 想要實現自己的 SayHi,怎麼辦?簡單,和匿名欄位衝突一樣的道理,我們可以在 Employee 上面定義一個 method,重寫了匿名欄位的方法。請看下面的例子
|
||||
```Go
|
||||
|
||||
### method 重寫
|
||||
|
||||
上面的例子中,如果 Employee 想要實現自己的 SayHi,怎麼辦?簡單,和匿名欄位衝突一樣的道理,我們可以在 Employee 上面定義一個 method,重寫了匿名欄位的方法。請看下面的例子
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
@@ -332,7 +337,9 @@ func main() {
|
||||
上面的程式碼設計的是如此的美妙,讓人不自覺的為 Go 的設計驚歎!
|
||||
|
||||
透過這些內容,我們可以設計出基本的物件導向的程式了,但是 Go 裡面的物件導向是如此的簡單,沒有任何的私有、公有關鍵字,透過大小寫來實現(大寫開頭的為公有,小寫開頭的為私有),方法也同樣適用這個原則。
|
||||
|
||||
## links
|
||||
|
||||
* [目錄](<preface.md>)
|
||||
* 上一章: [struct 型別](<02.4.md>)
|
||||
* 下一節: [interface](<02.6.md>)
|
||||
|
||||
@@ -15,6 +15,7 @@ Go 語言裡面設計最精妙的應該算 interface,它讓物件導向,內
|
||||
上面這些方法的組合稱為 interface(被物件 Student 和 Employee 實現)。例如 Student 和 Employee 都實現了 interface:SayHi 和 Sing,也就是這兩個物件是該 interface 型別。而 Employee 沒有實現這個 interface:SayHi、Sing 和 BorrowMoney,因為 Employee 沒有實現 BorrowMoney 這個方法。
|
||||
### interface 型別
|
||||
interface 型別定義了一組方法,如果某個物件實現了某個介面的所有方法,則此物件就實現了此介面。詳細的語法參考下面這個例子
|
||||
|
||||
```Go
|
||||
|
||||
type Human struct {
|
||||
@@ -98,6 +99,7 @@ type ElderlyGent interface {
|
||||
因為 m 能夠持有這三種類型的物件,所以我們可以定義一個包含 Men 型別元素的 slice,這個 slice 可以被賦予實現了 Men 介面的任意結構的物件,這個和我們傳統意義上面的 slice 有所不同。
|
||||
|
||||
讓我們來看一下下面這個例子:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -185,6 +187,7 @@ func main() {
|
||||
### 空 interface
|
||||
|
||||
空 interface(interface{})不包含任何的 method,正因為如此,所有的型別都實現了空 interface。空 interface 對於描述起不到任何的作用(因為它不包含任何的 method),但是空 interface 在我們需要儲存任意型別的數值的時候相當有用,因為它可以儲存任意型別的數值。它有點類似於 C 語言的 void*型別。
|
||||
|
||||
```Go
|
||||
|
||||
// 定義 a 為空介面
|
||||
@@ -200,6 +203,7 @@ a = s
|
||||
interface 的變數可以持有任意實現該 interface 型別的物件,這給我們編寫函式(包括 method)提供了一些額外的思考,我們是不是可以透過定義 interface 參數,讓函式接受各種型別的參數。
|
||||
|
||||
舉個例子:fmt.Println 是我們常用的一個函式,但是你是否注意到它可以接受任意型別的資料。開啟 fmt 的原始碼檔案,你會看到這樣一個定義:
|
||||
|
||||
```Go
|
||||
|
||||
type Stringer interface {
|
||||
@@ -207,6 +211,7 @@ type Stringer interface {
|
||||
}
|
||||
```
|
||||
也就是說,任何實現了 String 方法的型別都能作為參數被 fmt.Println 呼叫,讓我們來試一試
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -232,6 +237,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
現在我們再回顧一下前面的 Box 範例,你會發現 Color 結構也定義了一個 method:String。其實這也是實現了 fmt.Stringer 這個 interface,即如果需要某個型別能被 fmt 套件以特殊的格式輸出,你就必須實現 Stringer 這個介面。如果沒有實現這個介面,fmt 將以預設的方式輸出。
|
||||
|
||||
```Go
|
||||
|
||||
//實現同樣的功能
|
||||
@@ -250,6 +256,7 @@ fmt.Println("The biggest one is", boxes.BiggestsColor())
|
||||
如果 element 裡面確實儲存了 T 型別的數值,那麼 ok 回傳 true,否則回傳 false。
|
||||
|
||||
讓我們透過一個例子來更加深入的理解。
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -297,6 +304,7 @@ fmt.Println("The biggest one is", boxes.BiggestsColor())
|
||||
- switch 測試
|
||||
|
||||
最好的講解就是程式碼例子,現在讓我們重寫上面的這個實現
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -346,6 +354,7 @@ fmt.Println("The biggest one is", boxes.BiggestsColor())
|
||||
Go 裡面真正吸引人的是它內建的邏輯語法,就像我們在學習 Struct 時學習的匿名欄位,多麼的優雅啊,那麼相同的邏輯引入到 interface 裡面,那不是更加完美了。如果一個 interface1 作為 interface2 的一個嵌入欄位,那麼 interface2 隱式的包含了 interface1 裡面的 method。
|
||||
|
||||
我們可以看到原始碼套件 container/heap 裡面有這樣的一個定義
|
||||
|
||||
```Go
|
||||
|
||||
type Interface interface {
|
||||
@@ -355,6 +364,7 @@ type Interface interface {
|
||||
}
|
||||
```
|
||||
我們看到 sort.Interface 其實就是嵌入欄位,把 sort.Interface 的所有 method 給隱式的包含進來了。也就是下面三個方法:
|
||||
|
||||
```Go
|
||||
|
||||
type Interface interface {
|
||||
@@ -368,6 +378,7 @@ type Interface interface {
|
||||
}
|
||||
```
|
||||
另一個例子就是 io 套件下面的 io.ReadWriter ,它包含了 io 套件下面的 Reader 和 Writer 兩個 interface:
|
||||
|
||||
```Go
|
||||
|
||||
// io.ReadWriter
|
||||
@@ -380,18 +391,21 @@ type ReadWriter interface {
|
||||
Go 語言實現了反射,所謂反射就是能檢查程式在執行時的狀態。我們一般用到的套件是 reflect 套件。如何運用 reflect 套件,官方的這篇文章詳細的講解了 reflect 套件的實現原理,[laws of reflection](http://golang.org/doc/articles/laws_of_reflection.html)
|
||||
|
||||
使用 reflect 一般分成三步,下面簡要的講解一下:要去反射是一個型別的值(這些值都實現了空 interface),首先需要把它轉化成 reflect 物件(reflect.Type 或者 reflect.Value,根據不同的情況呼叫不同的函式)。這兩種取得方式如下:
|
||||
|
||||
```Go
|
||||
|
||||
t := reflect.TypeOf(i) //得到型別的 Meta 資料,透過 t 我們能取得型別定義裡面的所有元素
|
||||
v := reflect.ValueOf(i) //得到實際的值,透過 v 我們取得儲存在裡面的值,還可以去改變值
|
||||
```
|
||||
轉化為 reflect 物件之後我們就可以進行一些操作了,也就是將 reflect 物件轉化成相應的值,例如
|
||||
|
||||
```Go
|
||||
|
||||
tag := t.Elem().Field(0).Tag //取得定義在 struct 裡面的標籤
|
||||
name := v.Elem().Field(0).String() //取得儲存在第一個欄位裡面的值
|
||||
```
|
||||
取得反射值能回傳相應的型別和數值
|
||||
|
||||
```Go
|
||||
|
||||
var x float64 = 3.4
|
||||
@@ -401,6 +415,7 @@ fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
|
||||
fmt.Println("value:", v.Float())
|
||||
```
|
||||
最後,反射的話,那麼反射的欄位必須是可修改的,我們前面學習過傳值和傳參考,這個裡面也是一樣的道理。反射的欄位必須是可讀寫的意思是,如果下面這樣寫,那麼會發生錯誤
|
||||
|
||||
```Go
|
||||
|
||||
var x float64 = 3.4
|
||||
@@ -408,6 +423,7 @@ v := reflect.ValueOf(x)
|
||||
v.SetFloat(7.1)
|
||||
```
|
||||
如果要修改相應的值,必須這樣寫
|
||||
|
||||
```Go
|
||||
|
||||
var x float64 = 3.4
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
goroutine 是 Go 並行設計的核心。goroutine 說到底其實就是[協程](https://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B) (Coroutine),但是它比執行緒更小,十幾個 goroutine 可能體現在底層就是五六個執行緒,Go 語言內部幫你實現了這些 goroutine 之間的記憶體共享。執行 goroutine 只需極少的棧記憶體(大概是 4~5KB),當然會根據相應的資料伸縮。也正因為如此,可同時執行成千上萬個併發任務。goroutine 比 thread 更易用、更高效、更輕便。
|
||||
|
||||
goroutine 是透過 Go 的 runtime 管理的一個執行緒管理器。goroutine 透過 `go` 關鍵字實現了,其實就是一個普通的函式。
|
||||
|
||||
```Go
|
||||
|
||||
go hello(a, b, c)
|
||||
```
|
||||
透過關鍵字 go 就啟動了一個 goroutine。我們來看一個例子
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -55,6 +57,7 @@ func main() {
|
||||
|
||||
## channels
|
||||
goroutine 執行在相同的地址空間,因此訪問共享記憶體必須做好同步。那麼 goroutine 之間如何進行資料的通訊呢,Go 提供了一個很好的通訊機制 channel。channel 可以與 Unix shell 中的雙向管道做類別比:可以透過它傳送或者接收值。這些值只能是特定的型別:channel 型別。定義一個 channel 時,也需要定義傳送到 channel 的值的型別。注意,必須使用 make 建立 channel:
|
||||
|
||||
```Go
|
||||
|
||||
ci := make(chan int)
|
||||
@@ -62,13 +65,15 @@ cs := make(chan string)
|
||||
cf := make(chan interface{})
|
||||
```
|
||||
channel 透過運算子`<-`來接收和傳送資料
|
||||
|
||||
```Go
|
||||
|
||||
ch <- v // 傳送 v 到 channel ch.
|
||||
v := <-ch // 從 ch 中接收資料,並賦值給 v
|
||||
|
||||
```
|
||||
|
||||
我們把這些應用到我們的例子中來:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -98,6 +103,7 @@ func main() {
|
||||
|
||||
## Buffered Channels
|
||||
上面我們介紹了預設的非快取型別的 channel,不過 Go 也允許指定 channel 的緩衝大小,很簡單,就是 channel 可以儲存多少元素。ch:= make(chan bool, 4),建立了可以儲存 4 個元素的 bool 型 channel。在這個 channel 中,前 4 個元素可以無阻塞的寫入。當寫入第 5 個元素時,程式碼將會阻塞,直到其他 goroutine 從 channel 中讀取一些元素,騰出空間。
|
||||
|
||||
```Go
|
||||
|
||||
ch := make(chan type, value)
|
||||
@@ -105,6 +111,7 @@ ch := make(chan type, value)
|
||||
當 value = 0 時,channel 是無緩衝阻塞讀寫的,當 value > 0 時,channel 有緩衝、是非阻塞的,直到寫滿 value 個元素才阻塞寫入。
|
||||
|
||||
我們看一下下面這個例子,你可以在自己本機測試一下,修改相應的 value 值
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -124,6 +131,7 @@ func main() {
|
||||
## Range 和 Close
|
||||
|
||||
上面這個例子中,我們需要讀取兩次 c,這樣不是很方便,Go 考慮到了這一點,所以也可以透過 range,像操作 slice 或者 map 一樣操作快取型別的 channel,請看下面的例子
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -160,6 +168,7 @@ func main() {
|
||||
我們上面介紹的都是隻有一個 channel 的情況,那麼如果存在多個 channel 的時候,我們該如何操作呢,Go 裡面提供了一個關鍵字`select`,透過 `select` 可以監聽 channel 上的資料流動。
|
||||
|
||||
`select` 預設是阻塞的,只有當監聽的 channel 中有傳送或接收可以進行時才會執行,當多個 channel 都準備好的時候,select 會隨機選擇其中一個執行。
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -192,6 +201,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
在 `select` 裡面還有 default 語法,`select`其實就是類似 switch 的功能,default 就是當監聽的 channel 都沒有準備好的時候,預設執行的(select 不再阻塞等待 channel)。
|
||||
|
||||
```Go
|
||||
|
||||
select {
|
||||
@@ -203,6 +213,7 @@ default:
|
||||
```
|
||||
## 超時
|
||||
有時候會出現 goroutine 阻塞的情況,那麼我們如何避免整個程式進入阻塞的情況呢?我們可以利用 select 來設定超時,透過如下的方式實現:
|
||||
|
||||
```Go
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 2.8 總結
|
||||
|
||||
這一章我們主要介紹了 Go 語言的一些語法,透過語法我們可以發現 Go 是多麼的簡單,只有二十五個關鍵字。讓我們再來回顧一下這些關鍵字都是用來幹什麼的。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
break default func interface select
|
||||
case defer go map struct
|
||||
chan else goto package switch
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
前面小節已經介紹了 Web 是基於 http 協議的一個服務,Go 語言裡面提供了一個完善的 net/http 套件,透過 http 套件可以很方便的建立起來一個可以執行的 Web 服務。同時使用這個套件能很簡單地對 Web 的路由,靜態檔案,模版,cookie 等資料進行設定和操作。
|
||||
|
||||
## http 套件建立 Web 伺服器
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -34,8 +34,8 @@ func main() {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面這個程式碼,我們 build 之後,然後執行 web.exe,這個時候其實已經在 9090 埠監聽 http 連結請求了。
|
||||
|
||||
在瀏覽器輸入`http://localhost:9090`
|
||||
|
||||
@@ -36,6 +36,7 @@ Handler:處理請求和產生回傳資訊的處理邏輯
|
||||
前面小節的程式碼裡面我們可以看到,Go 是透過一個函式 `ListenAndServe` 來處理這些事情的,這個底層其實這樣處理的:初始化一個 server 物件,然後呼叫了`net.Listen("tcp", addr)`,也就是底層用 TCP 協議建立了一個服務,然後監聽我們設定的埠。
|
||||
|
||||
下面程式碼來自 Go 的 http 套件的原始碼,透過下面的程式碼我們可以看到整個的 http 處理過程:
|
||||
|
||||
```Go
|
||||
|
||||
func (srv *Server) Serve(l net.Listener) error {
|
||||
@@ -67,8 +68,8 @@ func (srv *Server) Serve(l net.Listener) error {
|
||||
go c.serve()
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
監聽之後如何接收客戶端的請求呢?上面程式碼執行監聽埠之後,呼叫了`srv.Serve(net.Listener)`函式,這個函式就是處理接收客戶端的請求資訊。這個函式裡面起了一個`for{}`,首先透過 Listener 接收請求,其次建立一個 Conn,最後單獨開了一個 goroutine,把這個請求的資料當做參數扔給這個 conn 去服務:`go c.serve()`。這個就是高併發體現了,使用者的每一次請求都是在一個新的 goroutine 去服務,相互不影響。
|
||||
|
||||
那麼如何具體分配到相應的函式來處理請求呢?conn 首先會解析 request:`c.readRequest()`,然後取得相應的 handler:`handler := c.server.Handler`,也就是我們剛才在呼叫函式 `ListenAndServe` 時候的第二個參數,我們前面例子傳遞的是 nil,也就是為空,那麼預設取得`handler = DefaultServeMux`,那麼這個變數用來做什麼的呢?對,這個變數就是一個路由器,它用來匹配 url 跳轉到其相應的 handle 函式,那麼這個我們有設定過嗎 ? 有,我們呼叫的程式碼裡面第一句不是呼叫了`http.HandleFunc("/", sayhelloName)`嘛。這個作用就是註冊了請求`/`的路由規則,當請求 uri 為"/",路由就會轉到函式 sayhelloName,DefaultServeMux 會呼叫 ServeHTTP 方法,這個方法內部其實就是呼叫 sayhelloName 本身,最後透過寫入 response 的資訊反饋到客戶端。
|
||||
|
||||
@@ -8,6 +8,7 @@ Go 的 http 有兩個核心功能:Conn、ServeMux
|
||||
與我們一般編寫的 http 伺服器不同, Go 為了實現高併發和高效能, 使用了 goroutines 來處理 Conn 的讀寫事件, 這樣每個請求都能保持獨立,相互不會阻塞,可以高效的回應網路事件。這是 Go 高效的保證。
|
||||
|
||||
Go 在等待客戶端請求裡面是這樣寫的:
|
||||
|
||||
```Go
|
||||
|
||||
c, err := srv.newConn(rw)
|
||||
@@ -15,14 +16,15 @@ if err != nil {
|
||||
continue
|
||||
}
|
||||
go c.serve()
|
||||
|
||||
```
|
||||
|
||||
這裡我們可以看到客戶端的每次請求都會建立一個 Conn,這個 Conn 裡面儲存了該次請求的資訊,然後再傳遞到對應的 handler,該 handler 中便可以讀取到相應的 header 資訊,這樣保證了每個請求的獨立性。
|
||||
|
||||
## ServeMux 的自訂
|
||||
我們前面小節講述 conn.server 的時候,其實內部是呼叫了 http 套件預設的路由器,透過路由器把本次請求的資訊傳遞到了後端的處理函式。那麼這個路由器是怎麼實現的呢?
|
||||
|
||||
它的結構如下:
|
||||
|
||||
```Go
|
||||
|
||||
type ServeMux struct {
|
||||
@@ -30,8 +32,8 @@ type ServeMux struct {
|
||||
m map[string]muxEntry // 路由規則,一個 string 對應一個 mux 實體,這裡的 string 就是註冊的路由表示式
|
||||
hosts bool // 是否在任意的規則中帶有 host 資訊
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
下面看一下 muxEntry
|
||||
|
||||
```Go
|
||||
@@ -42,17 +44,19 @@ type muxEntry struct {
|
||||
|
||||
pattern string //匹配字串
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接著看一下 Handler 的定義
|
||||
|
||||
```Go
|
||||
|
||||
type Handler interface {
|
||||
ServeHTTP(ResponseWriter, *Request) // 路由實現器
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Handler 是一個介面,但是前一小節中的 `sayhelloName` 函式並沒有實現 ServeHTTP 這個介面,為什麼能新增呢?原來在 http 套件裡面還定義了一個型別`HandlerFunc`,我們定義的函式 `sayhelloName` 就是這個 HandlerFunc 呼叫之後的結果,這個型別預設就實現了 ServeHTTP 這個介面,即我們呼叫了 HandlerFunc(f),強制型別轉換 f 成為 HandlerFunc 型別,這樣 f 就擁有了 ServeHTTP 方法。
|
||||
|
||||
```Go
|
||||
|
||||
type HandlerFunc func(ResponseWriter, *Request)
|
||||
@@ -63,6 +67,7 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
}
|
||||
```
|
||||
路由器裡面儲存好了相應的路由規則之後,那麼具體的請求又是怎麼分發的呢?請看下面的程式碼,預設的路由器實現了`ServeHTTP`:
|
||||
|
||||
```Go
|
||||
|
||||
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
@@ -78,6 +83,7 @@ func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
如上所示路由器接收到請求之後,如果是`*`那麼關閉連結,不然呼叫`mux.Handler(r)`回傳對應設定路由的處理 Handler,然後執行`h.ServeHTTP(w, r)`
|
||||
|
||||
也就是呼叫對應路由的 handler 的 ServerHTTP 介面,那麼 mux.Handler(r)怎麼處理的呢?
|
||||
|
||||
```Go
|
||||
|
||||
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
|
||||
@@ -112,6 +118,7 @@ func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
|
||||
透過上面這個介紹,我們了解了整個路由過程,Go 其實支援外部實現的路由器 `ListenAndServe`的第二個參數就是用以配置外部路由器的,它是一個 Handler 介面,即外部路由器只要實現了 Handler 介面就可以,我們可以在自己實現的路由器的 ServeHTTP 裡面實現自訂路由功能。
|
||||
|
||||
如下程式碼所示,我們自己實現了一個簡易的路由器
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
上面提交表單到伺服器的`/login`,當用戶輸入資訊點選登入之後,會跳轉到伺服器的路由 `login` 裡面,我們首先要判斷這個是什麼方式傳遞過來,POST 還是 GET 呢?
|
||||
|
||||
http 套件裡面有一個很簡單的方式就可以取得,我們在前面 web 的例子的基礎上來看看怎麼處理 login 頁面的 form 資料
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -65,8 +65,8 @@ func main() {
|
||||
log.Fatal("ListenAndServe: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
透過上面的程式碼我們可以看出取得請求方法是透過`r.Method`來完成的,這是個字串型別的變數,回傳 GET, POST, PUT 等 method 資訊。
|
||||
|
||||
login 函式中我們根據`r.Method`來判斷是顯示登入介面還是處理登入邏輯。當 GET 方式請求時顯示登入介面,其他方式請求時則處理登入邏輯,如查詢資料庫、驗證登入資訊等。
|
||||
@@ -90,8 +90,8 @@ login 函式中我們根據`r.Method`來判斷是顯示登入介面還是處理
|
||||
圖 4.2 伺服器端列印接收到的資訊
|
||||
|
||||
`request.Form`是一個 url.Values 型別,裡面儲存的是對應的類似 `key=value` 的資訊,下面展示了可以對 form 資料進行的一些操作:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
v := url.Values{}
|
||||
v.Set("name", "Ava")
|
||||
v.Add("friend", "Jess")
|
||||
@@ -101,8 +101,8 @@ v.Add("friend", "Zoe")
|
||||
fmt.Println(v.Get("name"))
|
||||
fmt.Println(v.Get("friend"))
|
||||
fmt.Println(v["friend"])
|
||||
|
||||
```
|
||||
|
||||
>**Tips**:
|
||||
>Request 本身也提供了 FormValue()函式來取得使用者提交的參數。如 r.Form["username"]也可寫成 r.FormValue("username")。呼叫 r.FormValue 時會自動呼叫 r.ParseForm,所以不必提前呼叫。r.FormValue 只會回傳同名參數中的第一個,若參數不存在則回傳空字串。
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
## 必填欄位
|
||||
你想要確保從一個表單元素中得到一個值,例如前面小節裡面的使用者名稱,我們如何處理呢?Go 有一個內建函式 `len` 可以取得字串的長度,這樣我們就可以透過 len 來取得資料的長度,例如:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
if len(r.Form["username"][0])==0{
|
||||
//為空的處理
|
||||
}
|
||||
@@ -18,8 +18,8 @@ if len(r.Form["username"][0])==0{
|
||||
你想要確保一個表單輸入框中取得的只能是數字,例如,你想透過表單取得某個人的具體年齡是 50 歲還是 10 歲,而不是像“一把年紀了”或“年輕著呢”這種描述
|
||||
|
||||
如果我們是判斷正整數,那麼我們先轉化成 int 型別,然後進行處理
|
||||
```Go
|
||||
|
||||
```Go
|
||||
getint,err:=strconv.Atoi(r.Form.Get("age"))
|
||||
if err!=nil{
|
||||
//數字轉化出錯了,那麼可能就不是數字
|
||||
@@ -31,8 +31,8 @@ if getint >100 {
|
||||
}
|
||||
```
|
||||
還有一種方式就是正則匹配的方式
|
||||
```Go
|
||||
|
||||
```Go
|
||||
if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
|
||||
return false
|
||||
}
|
||||
@@ -43,8 +43,8 @@ if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
|
||||
|
||||
## 中文
|
||||
有時候我們想透過表單元素取得一個使用者的中文名字,但是又為了保證取得的是正確的中文,我們需要進行驗證,而不是使用者隨便的一些輸入。對於中文我們目前有兩種方式來驗證,可以使用 `unicode` 套件提供的 `func Is(rangeTab *RangeTable, r rune) bool` 來驗證,也可以使用正則方式來驗證,這裡使用最簡單的正則方式,如下程式碼所示
|
||||
```Go
|
||||
|
||||
```Go
|
||||
if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {
|
||||
return false
|
||||
}
|
||||
@@ -53,28 +53,28 @@ if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {
|
||||
我們期望透過表單元素取得一個英文值,例如我們想知道一個使用者的英文名,應該是 astaxie,而不是 asta 謝。
|
||||
|
||||
我們可以很簡單的透過正則驗證資料:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 電子郵件地址
|
||||
你想知道使用者輸入的一個 Email 地址是否正確,透過如下這個方式可以驗證:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
|
||||
fmt.Println("no")
|
||||
}else{
|
||||
fmt.Println("yes")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 手機號碼
|
||||
你想要判斷使用者輸入的手機號碼是否正確,透過正則也可以驗證:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
|
||||
return false
|
||||
}
|
||||
@@ -92,8 +92,8 @@ if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")
|
||||
</select>
|
||||
```
|
||||
那麼我們可以這樣來驗證
|
||||
```Go
|
||||
|
||||
```Go
|
||||
slice:=[]string{"apple","pear","banana"}
|
||||
|
||||
v := r.Form.Get("fruit")
|
||||
@@ -113,8 +113,8 @@ return false
|
||||
<input type="radio" name="gender" value="2">女
|
||||
```
|
||||
那我們也可以類似下拉選單的做法一樣
|
||||
```Go
|
||||
|
||||
```Go
|
||||
slice:=[]string{"1","2"}
|
||||
|
||||
for _, v := range slice {
|
||||
@@ -133,8 +133,8 @@ return false
|
||||
<input type="checkbox" name="interest" value="tennis">網球
|
||||
```
|
||||
對於複選框我們的驗證和單選有點不一樣,因為接收到的資料是一個 slice
|
||||
```Go
|
||||
|
||||
```Go
|
||||
slice:=[]string{"football","basketball","tennis"}
|
||||
a:=Slice_diff(r.Form["interest"],slice)
|
||||
if a == nil{
|
||||
@@ -150,8 +150,8 @@ return false
|
||||
,使用者在日程表中安排 8 月份的第 45 天開會,或者提供未來的某個時間作為生日。
|
||||
|
||||
Go 裡面提供了一個 time 的處理套件,我們可以把使用者的輸入年月日轉化成相應的時間,然後進行邏輯判斷
|
||||
```Go
|
||||
|
||||
```Go
|
||||
t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||
fmt.Printf("Go launched at %s\n", t.Local())
|
||||
```
|
||||
@@ -159,8 +159,8 @@ fmt.Printf("Go launched at %s\n", t.Local())
|
||||
|
||||
## 身份證號碼
|
||||
如果我們想驗證表單輸入的是否是身份證,透過正則也可以方便的驗證,但是身份證有 15 位和 18 位,我們兩個都需要驗證
|
||||
```Go
|
||||
|
||||
```Go
|
||||
//驗證 15 位身份證,15 位的是全部數字
|
||||
if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
|
||||
return false
|
||||
@@ -170,8 +170,8 @@ if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
|
||||
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {
|
||||
return false
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面列出了我們一些常用的伺服器端的表單元素驗證,希望透過這個引匯入門,能夠讓你對 Go 的資料驗證有所了解,特別是 Go 裡面的正則處理。
|
||||
|
||||
## links
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
|
||||
我們看 4.1 小節的例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //輸出到伺服器端
|
||||
fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))
|
||||
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //輸出到客戶端
|
||||
@@ -27,8 +27,8 @@ template.HTMLEscape(w, []byte(r.Form.Get("username"))) //輸出到客戶端
|
||||
圖 4.3 Javascript 過濾之後的輸出
|
||||
|
||||
Go 的 html/template 套件預設幫你過濾了 html 標籤,但是有時候你只想要輸出這個`<script>alert()</script>`看起來正常的資訊,該怎麼處理?請使用 text/template。請看下面的例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
import "text/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
@@ -39,8 +39,8 @@ err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>
|
||||
Hello, <script>alert('you have been pwned')</script>!
|
||||
|
||||
或者使用 template.HTML 型別
|
||||
```Go
|
||||
|
||||
```Go
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
@@ -53,8 +53,8 @@ err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pw
|
||||
轉換成`template.HTML`後,變數的內容也不會被轉義
|
||||
|
||||
轉義的例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
import "html/template"
|
||||
...
|
||||
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<input type="submit" value="登陸">
|
||||
```
|
||||
我們在模版裡面增加了一個隱藏欄位`token`,這個值我們透過 MD5(時戳) 來取得唯一值,然後我們把這個值儲存到伺服器端(session 來控制,我們將在第六章講解如何儲存),以方便表單提交時比對判定。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("method:", r.Method) //取得請求的方法
|
||||
if r.Method == "GET" {
|
||||
|
||||
@@ -25,8 +25,8 @@ text/plain 空格轉換為 "+" 加號,但不對特殊字元編碼。
|
||||
</html>
|
||||
```
|
||||
在伺服器端,我們增加一個 handlerFunc:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
http.HandleFunc("/upload", upload)
|
||||
|
||||
// 處理/upload 邏輯
|
||||
@@ -87,8 +87,8 @@ type FileHeader struct {
|
||||
## 客戶端上傳檔案
|
||||
|
||||
我們上面的例子示範了如何透過表單上傳檔案,然後在伺服器端處理檔案,其實 Go 支援模擬客戶端表單功能支援檔案上傳,詳細用法請看如下範例:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -149,8 +149,8 @@ func main() {
|
||||
filename := "./astaxie.pdf"
|
||||
postFile(filename, target_url)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面的例子詳細展示了客戶端如何向伺服器上傳一個檔案的例子,客戶端透過 multipart.Write 把檔案的文字流寫入一個快取中,然後呼叫 http 的 Post 方法把快取傳到伺服器。
|
||||
|
||||
>如果你還有其他普通欄位例如 username 之類別的需要同時寫入,那麼可以呼叫 multipart 的 WriteField 方法寫很多其他類似的欄位。
|
||||
|
||||
@@ -5,8 +5,8 @@ Go 與 PHP 不同的地方是 Go 官方沒有提供資料庫驅動,而是為
|
||||
這個存在於 database/sql 的函式是用來註冊資料庫驅動的,當第三方開發者開發資料庫驅動時,都會實現 init 函式,在 init 裡面會呼叫這個`Register(name string, driver driver.Driver)`完成本驅動的註冊。
|
||||
|
||||
我們來看一下 mymysql、sqlite3 的驅動裡面都是怎麼呼叫的:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
//https://github.com/mattn/go-sqlite3 驅動
|
||||
func init() {
|
||||
sql.Register("sqlite3", &SQLiteDriver{})
|
||||
@@ -21,8 +21,8 @@ func init() {
|
||||
}
|
||||
```
|
||||
我們看到第三方資料庫驅動都是透過呼叫這個函式來註冊自己的資料庫驅動名稱以及相應的 driver 實現。在 database/sql 內部透過一個 map 來儲存使用者定義的相應驅動。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var drivers = make(map[string]driver.Driver)
|
||||
|
||||
drivers[name] = driver
|
||||
@@ -42,15 +42,15 @@ drivers[name] = driver
|
||||
|
||||
## driver.Driver
|
||||
Driver 是一個數據函式庫驅動的介面,他定義了一個 method: Open(name string),這個方法回傳一個數據函式庫的 Conn 介面。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Driver interface {
|
||||
Open(name string) (Conn, error)
|
||||
}
|
||||
```
|
||||
回傳的 Conn 只能用來進行一次 goroutine 的操作,也就是說不能把這個 Conn 應用於 Go 的多個 goroutine 裡面。如下程式碼會出現錯誤
|
||||
```Go
|
||||
|
||||
```Go
|
||||
...
|
||||
go goroutineA (Conn) //執行查詢操作
|
||||
go goroutineB (Conn) //執行插入操作
|
||||
@@ -62,8 +62,8 @@ go goroutineB (Conn) //執行插入操作
|
||||
|
||||
## driver.Conn
|
||||
Conn 是一個數據函式庫連線的介面定義,他定義了一系列方法,這個 Conn 只能應用在一個 goroutine 裡面,不能使用在多個 goroutine 裡面,詳情請參考上面的說明。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Conn interface {
|
||||
Prepare(query string) (Stmt, error)
|
||||
Close() error
|
||||
@@ -78,8 +78,8 @@ Begin 函式回傳一個代表交易處理的 Tx,透過它你可以進行查
|
||||
|
||||
## driver.Stmt
|
||||
Stmt 是一種準備好的狀態,和 Conn 相關聯,而且只能應用於一個 goroutine 中,不能應用於多個 goroutine。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Stmt interface {
|
||||
Close() error
|
||||
NumInput() int
|
||||
@@ -98,8 +98,8 @@ Query 函式執行 Prepare 準備好的 sql,傳入需要的參數執行 select
|
||||
|
||||
## driver.Tx
|
||||
交易處理一般就兩個過程,提交或者回復 (Rollback)。資料庫驅動裡面也只需要實現這兩個函式就可以
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Tx interface {
|
||||
Commit() error
|
||||
Rollback() error
|
||||
@@ -109,8 +109,8 @@ type Tx interface {
|
||||
|
||||
## driver.Execer
|
||||
這是一個 Conn 可選擇實現的介面
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Execer interface {
|
||||
Exec(query string, args []Value) (Result, error)
|
||||
}
|
||||
@@ -119,8 +119,8 @@ type Execer interface {
|
||||
|
||||
## driver.Result
|
||||
這個是執行 Update/Insert 等操作回傳的結果介面定義
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Result interface {
|
||||
LastInsertId() (int64, error)
|
||||
RowsAffected() (int64, error)
|
||||
@@ -132,8 +132,8 @@ RowsAffected 函式回傳 query 操作影響的資料條目數。
|
||||
|
||||
## driver.Rows
|
||||
Rows 是執行查詢回傳的結果集介面定義
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Rows interface {
|
||||
Columns() []string
|
||||
Close() error
|
||||
@@ -149,8 +149,8 @@ Next 函式用來回傳下一條資料,把資料賦值給 dest。dest 裡面
|
||||
|
||||
## driver.RowsAffected
|
||||
RowsAffected 其實就是一個 int64 的別名,但是他實現了 Result 介面,用來底層實現 Result 的表示方式
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type RowsAffected int64
|
||||
|
||||
func (RowsAffected) LastInsertId() (int64, error)
|
||||
@@ -159,13 +159,13 @@ func (v RowsAffected) RowsAffected() (int64, error)
|
||||
```
|
||||
## driver.Value
|
||||
Value 其實就是一個空介面,他可以容納任何的資料
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Value interface{}
|
||||
```
|
||||
drive 的 Value 是驅動必須能夠操作的 Value,Value 要麼是 nil,要麼是下面的任意一種
|
||||
```Go
|
||||
|
||||
```Go
|
||||
int64
|
||||
float64
|
||||
bool
|
||||
@@ -175,8 +175,8 @@ time.Time
|
||||
```
|
||||
## driver.ValueConverter
|
||||
ValueConverter 介面定義了如何把一個普通的值轉化成 driver.Value 的介面
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type ValueConverter interface {
|
||||
ConvertValue(v interface{}) (Value, error)
|
||||
}
|
||||
@@ -189,8 +189,8 @@ type ValueConverter interface {
|
||||
|
||||
## driver.Valuer
|
||||
Valuer 介面定義了回傳一個 driver.Value 的方式
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Valuer interface {
|
||||
Value() (Value, error)
|
||||
}
|
||||
@@ -201,8 +201,8 @@ type Valuer interface {
|
||||
|
||||
## database/sql
|
||||
database/sql 在 database/sql/driver 提供的介面基礎上定義了一些更高階的方法,用以簡化資料庫操作,同時內部還建議性地實現一個 conn pool。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type DB struct {
|
||||
driver driver.Driver
|
||||
dsn string
|
||||
|
||||
@@ -34,8 +34,8 @@ CREATE TABLE `userdetail` (
|
||||
)
|
||||
```
|
||||
如下範例將示範如何使用 database/sql 介面對資料庫表進行增刪改查操作
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -30,8 +30,8 @@ CREATE TABLE `userdetail` (
|
||||
);
|
||||
```
|
||||
看下面 Go 程式是如何操作資料庫表資料 : 增刪改查
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -36,12 +36,11 @@ CREATE TABLE userdetail
|
||||
profile character varying(100)
|
||||
)
|
||||
WITH(OIDS=FALSE);
|
||||
|
||||
```
|
||||
|
||||
看下面這個 Go 如何操作資料庫表資料 : 增刪改查
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -32,8 +32,8 @@ beego orm 支援 go get 方式安裝,是完全按照 Go Style 的方式來實
|
||||
|
||||
## 如何初始化
|
||||
首先你需要 import 相應的資料庫驅動套件、database/sql 標準介面套件以及 beego orm 套件,如下所示:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/astaxie/beego/orm"
|
||||
@@ -81,6 +81,7 @@ orm.RegisterDriver("mysql", orm.DR_MySQL)
|
||||
orm.RegisterDataBase("default", "mysql", "root:zxxx@/test?charset=utf8")
|
||||
```
|
||||
Sqlite 配置:
|
||||
|
||||
```Go
|
||||
//匯入驅動
|
||||
//_ "github.com/mattn/go-sqlite3"
|
||||
@@ -97,15 +98,14 @@ orm.RegisterDataBase("default", "sqlite3", "./datas/test.db")
|
||||
beego orm:
|
||||
|
||||
```Go
|
||||
|
||||
func main() {
|
||||
o := orm.NewOrm()
|
||||
}
|
||||
```
|
||||
|
||||
簡單範例:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -162,27 +162,27 @@ func main() {
|
||||
SetMaxIdleConns
|
||||
|
||||
根據資料庫的別名,設定資料庫的最大空閒連線
|
||||
```Go
|
||||
|
||||
```Go
|
||||
orm.SetMaxIdleConns("default", 30)
|
||||
```
|
||||
SetMaxOpenConns
|
||||
|
||||
根據資料庫的別名,設定資料庫的最大資料庫連線 (go >= 1.2)
|
||||
```Go
|
||||
|
||||
```Go
|
||||
orm.SetMaxOpenConns("default", 30)
|
||||
```
|
||||
|
||||
目前 beego orm 支援列印除錯,你可以透過如下的程式碼實現除錯
|
||||
```Go
|
||||
|
||||
```Go
|
||||
orm.Debug = true
|
||||
```
|
||||
|
||||
接下來我們的例子採用前面的資料庫表 User,現在我們建立相應的 struct
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Userinfo struct {
|
||||
Uid int `PK` //如果表的主鍵不是 id,那麼需要加上 pk 註釋,明確的說這個欄位是主鍵
|
||||
Username string
|
||||
@@ -221,14 +221,14 @@ func init() {
|
||||
orm.RegisterModel(new(Userinfo),new(User), new(Profile), new(Tag))
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
>注意一點,beego orm 針對駝峰命名會自動幫你轉化成下劃線欄位,例如你定義了 Struct 名字為`UserInfo`,那麼轉化成底層實現的時候是`user_info`,欄位命名也遵循該規則。
|
||||
|
||||
## 插入資料
|
||||
下面的程式碼示範了如何插入一條記錄,可以看到我們操作的是 struct 物件,而不是原生的 sql 語句,最後透過呼叫 Insert 介面將資料儲存到資料庫。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
o := orm.NewOrm()
|
||||
var user User
|
||||
user.Name = "zxxx"
|
||||
@@ -245,15 +245,15 @@ if err == nil {
|
||||
同時插入多個物件:InsertMulti
|
||||
|
||||
類似 sql 語句
|
||||
```Go
|
||||
|
||||
```Go
|
||||
insert into table (name, age) values("slene", 28),("astaxie", 30),("unknown", 20)
|
||||
```
|
||||
第一個參數 bulk 為並列插入的數量,第二個為物件的 slice
|
||||
|
||||
回傳值為成功插入的數量
|
||||
```Go
|
||||
|
||||
```Go
|
||||
users := []User{
|
||||
{Name: "slene"},
|
||||
{Name: "astaxie"},
|
||||
@@ -267,8 +267,8 @@ bulk 為 1 時,將會順序插入 slice 中的資料
|
||||
|
||||
## 更新資料
|
||||
繼續上面的例子來示範更新操作,現在 user 的主鍵已經有值了,此時呼叫 Insert 介面,beego orm 內部會自動呼叫 update 以進行資料的更新而非插入操作。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
o := orm.NewOrm()
|
||||
user := User{Uid: 1}
|
||||
if o.Read(&user) == nil {
|
||||
@@ -279,8 +279,8 @@ if o.Read(&user) == nil {
|
||||
}
|
||||
```
|
||||
Update 預設更新所有的欄位,可以更新指定的欄位:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
// 只更新 Name
|
||||
o.Update(&user, "Name")
|
||||
// 指定多個欄位
|
||||
@@ -293,8 +293,8 @@ o.Update(&user, "Name")
|
||||
beego orm 的查詢介面比較靈活,具體使用請看下面的例子
|
||||
|
||||
例子 1,根據主鍵取得資料:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
o := orm.NewOrm()
|
||||
var user User
|
||||
|
||||
@@ -312,8 +312,8 @@ if err == orm.ErrNoRows {
|
||||
```
|
||||
|
||||
例子 2:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
o := orm.NewOrm()
|
||||
var user User
|
||||
|
||||
@@ -322,15 +322,15 @@ qs.Filter("id", 1) // WHERE id = 1
|
||||
qs.Filter("profile__age", 18) // WHERE profile.age = 18
|
||||
```
|
||||
例子 3,WHERE IN 查詢條件:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
qs.Filter("profile__age__in", 18, 20)
|
||||
// WHERE profile.age IN (18, 20)
|
||||
|
||||
```
|
||||
例子 4,更加複雜的條件:
|
||||
```Go
|
||||
|
||||
例子 4,更加複雜的條件:
|
||||
|
||||
```Go
|
||||
qs.Filter("profile__age__in", 18, 20).Exclude("profile__lt", 1000)
|
||||
// WHERE profile.age IN (18, 20) AND NOT profile_id < 1000
|
||||
|
||||
@@ -339,15 +339,15 @@ qs.Filter("profile__age__in", 18, 20).Exclude("profile__lt", 1000)
|
||||
可以透過如下介面取得多條資料,請看範例
|
||||
|
||||
例子 1,根據條件 age>17,取得 20 位置開始的 10 條資料的資料
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var allusers []User
|
||||
qs.Filter("profile__age__gt", 17)
|
||||
// WHERE profile.age > 17
|
||||
```
|
||||
例子 2,limit 預設從 10 開始,取得 10 條資料
|
||||
```Go
|
||||
|
||||
```Go
|
||||
qs.Limit(10, 20)
|
||||
// LIMIT 10 OFFSET 20 注意跟 SQL 反過來的
|
||||
```
|
||||
@@ -370,8 +370,8 @@ Delete 操作會對反向關係進行操作,此例中 Post 擁有一個到 Use
|
||||
## 關聯查詢
|
||||
|
||||
有些應用卻需要用到連線查詢,所以現在 beego orm 提供了一個簡陋的實現方案:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Post struct {
|
||||
Id int `orm:"auto"`
|
||||
Title string `orm:"size(100)"`
|
||||
@@ -381,23 +381,23 @@ type Post struct {
|
||||
var posts []*Post
|
||||
qs := o.QueryTable("post")
|
||||
num, err := qs.Filter("User__Name", "slene").All(&posts)
|
||||
|
||||
```
|
||||
|
||||
上面程式碼中我們看到了一個 struct 關聯查詢
|
||||
|
||||
|
||||
|
||||
## Group By 和 Having
|
||||
針對有些應用需要用到 group by 的功能,beego orm 也提供了一個簡陋的實現
|
||||
```Go
|
||||
|
||||
```Go
|
||||
qs.OrderBy("id", "-profile__age")
|
||||
// ORDER BY id ASC, profile.age DESC
|
||||
|
||||
qs.OrderBy("-profile__age", "profile")
|
||||
// ORDER BY profile.age DESC, profile_id ASC
|
||||
|
||||
```
|
||||
|
||||
上面的程式碼中出現了兩個新介面函式
|
||||
|
||||
GroupBy:用來指定進行 groupby 的欄位
|
||||
@@ -410,7 +410,6 @@ Having:用來指定 having 執行的時候的條件
|
||||
簡單範例:
|
||||
|
||||
```Go
|
||||
|
||||
o := orm.NewOrm()
|
||||
var r orm.RawSeter
|
||||
r = o.Raw("UPDATE user SET name = ? WHERE name = ?", "testing", "slene")
|
||||
|
||||
@@ -17,8 +17,8 @@ Go 目前支援 redis 的驅動有如下
|
||||
- https://github.com/simonz05/godis
|
||||
|
||||
我以 redigo 驅動為例來示範如何進行資料的操作:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -100,8 +100,8 @@ func main() {
|
||||
https://github.com/astaxie/goredis
|
||||
|
||||
接下來的以我自己 fork 的這個 redis 驅動為例來示範如何進行資料的操作
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -132,8 +132,8 @@ func main() {
|
||||
}
|
||||
client.Del("l")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我們可以看到操作 redis 非常的方便,而且我實際專案中應用下來效能也很高。client 的命令和 redis 的命令基本保持一致。所以和原生態操作 redis 非常類似。
|
||||
|
||||
## mongoDB
|
||||
@@ -155,8 +155,8 @@ go get gopkg.in/mgo.v2
|
||||
```
|
||||
|
||||
下面我將示範如何透過 Go 來操作 mongoDB:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -197,8 +197,8 @@ func main() {
|
||||
|
||||
fmt.Println("Phone:", result.Phone)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我們可以看出來 mgo 的操作方式和 beedb 的操作方式幾乎類似,都是基於 struct 的操作方式,這個就是 Go Style。
|
||||
|
||||
|
||||
|
||||
@@ -35,13 +35,13 @@ cookie 是有時間限制的,根據生命期不同分成兩種:會話 cookie
|
||||
|
||||
### Go 設定 cookie
|
||||
Go 語言中透過 net/http 套件中的 SetCookie 來設定:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
http.SetCookie(w ResponseWriter, cookie *Cookie)
|
||||
```
|
||||
w 表示需要寫入的 response,cookie 是一個 struct,讓我們來看一下 cookie 物件是怎麼樣的
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Cookie struct {
|
||||
Name string
|
||||
Value string
|
||||
@@ -59,11 +59,11 @@ type Cookie struct {
|
||||
Raw string
|
||||
Unparsed []string // Raw text of unparsed attribute-value pairs
|
||||
}
|
||||
|
||||
```
|
||||
我們來看一個例子,如何設定 cookie
|
||||
```Go
|
||||
|
||||
我們來看一個例子,如何設定 cookie
|
||||
|
||||
```Go
|
||||
expiration := time.Now()
|
||||
expiration = expiration.AddDate(1, 0, 0)
|
||||
cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration}
|
||||
@@ -72,14 +72,14 @@ http.SetCookie(w, &cookie)
|
||||
|
||||
### Go 讀取 cookie
|
||||
上面的例子示範了如何設定 cookie 資料,我們這裡來示範一下如何讀取 cookie
|
||||
```Go
|
||||
|
||||
```Go
|
||||
cookie, _ := r.Cookie("username")
|
||||
fmt.Fprint(w, cookie)
|
||||
```
|
||||
還有另外一種讀取方式
|
||||
```Go
|
||||
|
||||
```Go
|
||||
for _, cookie := range r.Cookies() {
|
||||
fmt.Fprint(w, cookie.Name)
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ session 的基本原理是由伺服器為每個會話維護一份資訊資料,
|
||||
### Session 管理器
|
||||
|
||||
定義一個全域性的 session 管理器
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Manager struct {
|
||||
cookieName string // private cookiename
|
||||
lock sync.Mutex // protects session
|
||||
@@ -48,11 +48,11 @@ func NewManager(provideName, cookieName string, maxLifeTime int64) (*Manager, er
|
||||
}
|
||||
return &Manager{provider: provider, cookieName: cookieName, maxLifeTime: maxLifeTime}, nil
|
||||
}
|
||||
|
||||
```
|
||||
Go 實現整個的流程應該也是這樣的,在 main 套件中建立一個全域性的 session 管理器
|
||||
```Go
|
||||
|
||||
Go 實現整個的流程應該也是這樣的,在 main 套件中建立一個全域性的 session 管理器
|
||||
|
||||
```Go
|
||||
var globalSessions *session.Manager
|
||||
//然後在 init 函式中初始化
|
||||
func init() {
|
||||
@@ -60,8 +60,8 @@ func init() {
|
||||
}
|
||||
```
|
||||
我們知道 session 是儲存在伺服器端的資料,它可以以任何的方式儲存,比如儲存在記憶體、資料庫或者檔案中。因此我們抽象出一個 Provider 介面,用以表徵 session 管理器底層儲存結構。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Provider interface {
|
||||
SessionInit(sid string) (Session, error)
|
||||
SessionRead(sid string) (Session, error)
|
||||
@@ -75,8 +75,8 @@ type Provider interface {
|
||||
- SessionGC 根據 maxLifeTime 來刪除過期的資料
|
||||
|
||||
那麼 Session 介面需要實現什麼樣的功能呢?有過 Web 開發經驗的讀者知道,對 Session 的處理基本就 設定值、讀取值、刪除值以及取得當前 sessionID 這四個操作,所以我們的 Session 介面也就實現這四個操作。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Session interface {
|
||||
Set(key, value interface{}) error // set session value
|
||||
Get(key interface{}) interface{} // get session value
|
||||
@@ -87,7 +87,6 @@ type Session interface {
|
||||
>以上設計思路來源於 database/sql/driver,先定義好介面,然後具體的儲存 session 的結構實現相應的介面並註冊後,相應功能這樣就可以使用了,以下是用來隨需註冊儲存 session 的結構的 Register 函式的實現。
|
||||
|
||||
```Go
|
||||
|
||||
var provides = make(map[string]Provider)
|
||||
|
||||
// Register makes a session provide available by the provided name.
|
||||
@@ -108,7 +107,6 @@ func Register(name string, provider Provider) {
|
||||
Session ID 是用來識別訪問 Web 應用的每一個使用者,因此必須保證它是全域性唯一的(GUID),下面程式碼展示了如何滿足這一需求:
|
||||
|
||||
```Go
|
||||
|
||||
func (manager *Manager) sessionId() string {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
@@ -119,8 +117,8 @@ func (manager *Manager) sessionId() string {
|
||||
```
|
||||
### session 建立
|
||||
我們需要為每個來訪使用者分配或取得與他相關連的 Session,以便後面根據 Session 資訊來驗證操作。SessionStart 這個函式就是用來檢測是否已經有某個 Session 與當前來訪使用者發生了關聯,如果沒有則建立之。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
@@ -138,8 +136,8 @@ func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (se
|
||||
}
|
||||
```
|
||||
我們用前面 login 操作來示範 session 的運用:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
r.ParseForm()
|
||||
@@ -157,8 +155,8 @@ func login(w http.ResponseWriter, r *http.Request) {
|
||||
SessionStart 函式回傳的是一個滿足 Session 介面的變數,那麼我們該如何用他來對 session 資料進行操作呢?
|
||||
|
||||
上面的例子中的程式碼`session.Get("uid")`已經展示了基本的讀取資料的操作,現在我們再來看一下詳細的操作:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func count(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
createtime := sess.Get("createtime")
|
||||
@@ -185,8 +183,8 @@ func count(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
### session 重置
|
||||
我們知道,Web 應用中有使用者退出這個操作,那麼當用戶退出應用的時候,我們需要對該使用者的 session 資料進行刪除操作,上面的程式碼已經示範了如何使用 session 重置操作,下面這個函式就是實現了這個功能:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
//Destroy sessionid
|
||||
func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
|
||||
cookie, err := r.Cookie(manager.cookieName)
|
||||
@@ -201,19 +199,18 @@ func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### session 刪除
|
||||
我們來看一下 Session 管理器如何來管理刪除,只要我們在 Main 啟動的時候啟動:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func init() {
|
||||
go globalSessions.GC()
|
||||
}
|
||||
```
|
||||
|
||||
```Go
|
||||
|
||||
func (manager *Manager) GC() {
|
||||
manager.lock.Lock()
|
||||
defer manager.lock.Unlock()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 6.3 session 儲存
|
||||
上一節我們介紹了 Session 管理器的實現原理,定義了儲存 session 的介面,這小節我們將範例一個基於記憶體的 session 儲存介面的實現,其他的儲存方式,讀者可以自行參考範例來實現,記憶體的實現請看下面的例子程式碼
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package memory
|
||||
|
||||
import (
|
||||
@@ -114,16 +114,16 @@ func init() {
|
||||
}
|
||||
```
|
||||
上面這個程式碼實現了一個記憶體儲存的 session 機制。透過 init 函式註冊到 session 管理器中。這樣就可以方便的呼叫了。我們如何來呼叫該引擎呢?請看下面的程式碼
|
||||
```Go
|
||||
|
||||
```Go
|
||||
import (
|
||||
"github.com/astaxie/session"
|
||||
_ "github.com/astaxie/session/providers/memory"
|
||||
)
|
||||
```
|
||||
當 import 的時候已經執行了 memory 函式裡面的 init 函式,這樣就已經註冊到 session 管理器中,我們就可以使用了,透過如下方式就可以初始化一個 session 管理器:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var globalSessions *session.Manager
|
||||
|
||||
//然後在 init 函式中初始化
|
||||
@@ -131,8 +131,8 @@ func init() {
|
||||
globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
|
||||
go globalSessions.GC()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一節: [Go 如何使用 session](<06.2.md>)
|
||||
|
||||
@@ -4,8 +4,8 @@ session 劫持是一種廣泛存在的比較嚴重的安全威脅,在 session
|
||||
本節將透過一個範例來示範會話劫持,希望透過這個範例,能讓讀者更好地理解 session 的本質。
|
||||
## session 劫持過程
|
||||
我們寫了如下的程式碼來展示一個 count 計數器:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func count(w http.ResponseWriter, r *http.Request) {
|
||||
sess := globalSessions.SessionStart(w, r)
|
||||
ct := sess.Get("countnum")
|
||||
@@ -18,11 +18,11 @@ func count(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
t.Execute(w, sess.Get("countnum"))
|
||||
}
|
||||
|
||||
```
|
||||
count.gtpl 的程式碼如下所示:
|
||||
```Go
|
||||
|
||||
count.gtpl 的程式碼如下所示:
|
||||
|
||||
```Go
|
||||
Hi. Now count:{{.}}
|
||||
```
|
||||
然後我們在瀏覽器裡面重新整理可以看到如下內容:
|
||||
@@ -58,8 +58,8 @@ Enter 後,你將看到如下內容:
|
||||
其中一個解決方案就是 sessionID 的值只允許 cookie 設定,而不是透過 URL 重置方式設定,同時設定 cookie 的 httponly 為 true,這個屬性是設定是否可透過客戶端指令碼訪問這個設定的 cookie,第一這個可以防止這個 cookie 被 XSS 讀取從而引起 session 劫持,第二 cookie 設定不會像 URL 重置方式那麼容易取得 sessionID。
|
||||
|
||||
第二步就是在每個請求裡面加上 token,實現類似前面章節裡面講的防止 form 重複提交類似的功能,我們在每個請求裡面加上一個隱藏的 token,然後每次驗證這個 token,從而保證使用者的請求都是唯一性。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
h := md5.New()
|
||||
salt:="astaxie%^7&8888"
|
||||
io.WriteString(h,salt+time.Now().String())
|
||||
@@ -68,12 +68,12 @@ if r.Form["token"]!=token{
|
||||
//提示登入
|
||||
}
|
||||
sess.Set("token",token)
|
||||
|
||||
```
|
||||
|
||||
### 間隔產生新的 SID
|
||||
還有一個解決方案就是,我們給 session 額外設定一個建立時間的值,一旦過了一定的時間,我們刪除這個 sessionID,重新產生新的 session,這樣可以一定程度上防止 session 劫持的問題。
|
||||
```Go
|
||||
|
||||
```Go
|
||||
createtime := sess.Get("createtime")
|
||||
if createtime == nil {
|
||||
sess.Set("createtime", time.Now().Unix())
|
||||
|
||||
@@ -22,15 +22,15 @@ XML 作為一種資料交換和資訊傳遞的格式已經十分普及。而隨
|
||||
|
||||
## 解析 XML
|
||||
如何解析如上這個 XML 檔案呢? 我們可以透過 xml 套件的 `Unmarshal` 函式來達到我們的目的
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
```
|
||||
data 接收的是 XML 資料流,v 是需要輸出的結構,定義為 interface,也就是可以把 XML 轉換為任意的格式。我們這裡主要介紹 struct 的轉換,因為 struct 和 XML 都有類似樹結構的特徵。
|
||||
|
||||
範例程式碼如下:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -74,9 +74,9 @@ func main() {
|
||||
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
```
|
||||
XML 本質上是一種樹形的資料格式,而我們可以定義與之匹配的 go 語言的 struct 型別,然後透過 xml.Unmarshal 來將 xml 中的資料解析成對應的 struct 物件。如上例子輸出如下資料
|
||||
|
||||
XML 本質上是一種樹狀結構,而我們可以定義與之匹配的 go 語言的 struct 型別,然後透過 xml.Unmarshal 來將 xml 中的資料解析成對應的 struct 物件。如上例子輸出如下資料
|
||||
```xml
|
||||
|
||||
{{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}]
|
||||
@@ -89,11 +89,11 @@ XML 本質上是一種樹形的資料格式,而我們可以定義與之匹配
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
}
|
||||
|
||||
```
|
||||
上面的例子中,將 xml 檔案解析成對應的 struct 物件是透過`xml.Unmarshal`來完成的,這個過程是如何實現的?可以看到我們的 struct 定義後面多了一些類似於`xml:"serverName"`這樣的內容,這個是 struct 的一個特性,它們被稱為 struct tag,它們是用來輔助反射的。我們來看一下 `Unmarshal` 的定義:
|
||||
```Go
|
||||
|
||||
上面的例子中,將 xml 檔案解析成對應的 struct 物件是透過`xml.Unmarshal`來完成的,這個過程是如何實現的?可以看到我們的 struct 定義後面多了一些類似於`xml:"serverName"`這樣的內容,這個是 struct 的一個特性,它們被稱為 struct tag,它們是用來輔助反射的。我們來看一下 `Unmarshal` 的定義:
|
||||
|
||||
```Go
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
```
|
||||
我們看到函式定義了兩個參數,第一個是 XML 資料流,第二個是儲存的對應型別,目前支援 struct、slice 和 string,XML 套件內部採用了反射來進行資料的對映,所以 v 裡面的欄位必須是匯出的。`Unmarshal`解析的時候 XML 元素和欄位怎麼對應起來的呢?這是有一個優先順序讀取流程的,首先會讀取 struct tag,如果沒有,那麼就會對應欄位名。必須注意一點的是解析的時候 tag、欄位名、XML 元素都是區分大小寫的的,所以必須一一對應欄位。
|
||||
@@ -131,16 +131,16 @@ Go 語言的反射機制,可以利用這些 tag 資訊來將來自 XML 檔案
|
||||
|
||||
## 輸出 XML
|
||||
假若我們不是要解析如上所示的 XML 檔案,而是產生它,那麼在 go 語言中又該如何實現呢? xml 套件中提供了 `Marshal` 和`MarshalIndent`兩個函式,來滿足我們的需求。這兩個函式主要的區別是第二個函式會增加字首和縮排,函式的定義如下所示:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func Marshal(v interface{}) ([]byte, error)
|
||||
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
|
||||
```
|
||||
兩個函式第一個參數是用來產生 XML 的結構定義型別資料,都是回傳產生的 XML 資料流。
|
||||
|
||||
下面我們來看一下如何輸出如上的 XML:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -172,8 +172,8 @@ func main() {
|
||||
|
||||
os.Stdout.Write(output)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面的程式碼輸出如下資訊:
|
||||
```xml
|
||||
|
||||
@@ -188,8 +188,8 @@ func main() {
|
||||
<serverIP>127.0.0.2</serverIP>
|
||||
</server>
|
||||
</servers>
|
||||
|
||||
```
|
||||
|
||||
和我們之前定義的檔案的格式一模一樣,之所以會有`os.Stdout.Write([]byte(xml.Header))` 這句程式碼的出現,是因為`xml.MarshalIndent`或者`xml.Marshal`輸出的資訊都是不帶 XML 頭的,為了產生正確的 xml 檔案,我們使用了 xml 套件預定義的 Header 變數。
|
||||
|
||||
我們看到 `Marshal` 函式接收的參數 v 是 interface{}型別的,即它可以接受任意型別的參數,那麼 xml 套件,根據什麼規則來產生相應的 XML 檔案呢?
|
||||
@@ -227,8 +227,8 @@ func main() {
|
||||
<first>Asta</first>
|
||||
<last>Xie</last>
|
||||
</name>
|
||||
|
||||
```
|
||||
|
||||
上面我們介紹了如何使用 Go 語言的 xml 套件來編/解碼 XML 檔案,重要的一點是對 XML 的所有操作都是透過 struct tag 來實現的,所以學會對 struct tag 的運用變得非常重要,在文章中我們簡要的列舉了如何定義 tag。更多內容或 tag 定義請參看相應的官方資料。
|
||||
|
||||
## links
|
||||
|
||||
@@ -11,13 +11,13 @@ JSON(Javascript Object Notation)是一種輕量級的資料交換語言,
|
||||
|
||||
### 解析到結構體
|
||||
假如有了上面的 JSON 串,那麼我們如何來解析這個 JSON 串呢?Go 的 JSON 套件中有如下函式
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
```
|
||||
透過這個函式我們就可以實現解析的目的,詳細的解析例子請看如下程式碼:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -60,19 +60,19 @@ func main() {
|
||||
- nil 代表 JSON null.
|
||||
|
||||
現在我們假設有如下的 JSON 資料
|
||||
```Go
|
||||
|
||||
```Go
|
||||
b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)
|
||||
```
|
||||
如果在我們不知道他的結構的情況下,我們把他解析到 interface{} 裡面
|
||||
```Go
|
||||
|
||||
```Go
|
||||
var f interface{}
|
||||
err := json.Unmarshal(b, &f)
|
||||
```
|
||||
這個時候 f 裡面儲存了一個 map 型別,他們的 key 是 string,值儲存在空的 interface{} 裡
|
||||
```Go
|
||||
|
||||
```Go
|
||||
f = map[string]interface{}{
|
||||
"Name": "Wednesday",
|
||||
"Age": 6,
|
||||
@@ -83,13 +83,13 @@ f = map[string]interface{}{
|
||||
}
|
||||
```
|
||||
那麼如何來訪問這些資料呢?透過斷言的方式:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
m := f.(map[string]interface{})
|
||||
```
|
||||
透過斷言之後,你就可以透過如下方式來訪問裡面的資料了
|
||||
```Go
|
||||
|
||||
```Go
|
||||
for k, v := range m {
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
@@ -111,8 +111,8 @@ for k, v := range m {
|
||||
透過上面的範例可以看到,透過 interface{} 與 type assert 的配合,我們就可以解析未知結構的 JSON 數了。
|
||||
|
||||
上面這個是官方提供的解決方案,其實很多時候我們透過型別斷言,操作起來不是很方便,目前 bitly 公司開源了一個叫做 `simplejson` 的套件,在處理未知結構體的 JSON 時相當方便,詳細例子如下所示:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
js, err := NewJson([]byte(`{
|
||||
"test": {
|
||||
"array": [1, "2", 3],
|
||||
@@ -127,19 +127,19 @@ js, err := NewJson([]byte(`{
|
||||
arr, _ := js.Get("test").Get("array").Array()
|
||||
i, _ := js.Get("test").Get("int").Int()
|
||||
ms := js.Get("test").Get("string").MustString()
|
||||
|
||||
```
|
||||
|
||||
可以看到,使用這個函式庫操作 JSON 比起官方套件來說,簡單的多,詳細的請參考如下地址:https://github.com/bitly/go-simplejson
|
||||
|
||||
## 產生 JSON
|
||||
我們開發很多應用的時候,最後都是要輸出 JSON 資料串,那麼如何來處理呢?JSON 套件裡面透過 `Marshal` 函式來處理,函式定義如下:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func Marshal(v interface{}) ([]byte, error)
|
||||
```
|
||||
假設我們還是需要產生上面的伺服器列表資訊,那麼如何來處理呢?請看下面的例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -166,16 +166,16 @@ func main() {
|
||||
}
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
輸出如下內容:
|
||||
```json
|
||||
|
||||
{"Servers":[{"ServerName":"Shanghai_VPN","ServerIP":"127.0.0.1"},{"ServerName":"Beijing_VPN","ServerIP":"127.0.0.2"}]}
|
||||
```
|
||||
我們看到上面的輸出欄位名的首字母都是大寫的,如果你想用小寫的首字母怎麼辦呢?把結構體的欄位名改成首字母小寫的?JSON 輸出的時候必須注意,只有匯出的欄位才會被輸出,如果修改欄位名,那麼就會發現什麼都不會輸出,所以必須透過 struct tag 定義來實現:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Server struct {
|
||||
ServerName string `json:"serverName"`
|
||||
ServerIP string `json:"serverIP"`
|
||||
@@ -196,8 +196,8 @@ type Serverslice struct {
|
||||
|
||||
|
||||
舉例來說:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Server struct {
|
||||
// ID 不會匯出到 JSON 中
|
||||
ID int `json:"-"`
|
||||
@@ -218,20 +218,20 @@ s := Server {
|
||||
}
|
||||
b, _ := json.Marshal(s)
|
||||
os.Stdout.Write(b)
|
||||
|
||||
```
|
||||
|
||||
會輸出以下內容:
|
||||
```json
|
||||
|
||||
{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}
|
||||
|
||||
```
|
||||
|
||||
Marshal 函式只有在轉換成功的時候才會回傳資料,在轉換的過程中我們需要注意幾點:
|
||||
|
||||
|
||||
- JSON 物件只支援 string 作為 key,所以要編碼一個 map,那麼必須是 map[string]T 這種型別(T 是 Go 語言中任意的型別)
|
||||
- Channel, complex 和 function 是不能被編碼成 JSON 的
|
||||
- 巢狀的資料是不能編碼的,不然會讓 JSON 編碼進入死迴圈
|
||||
- 巢狀的資料是不能編碼的,不然會讓 JSON 編碼進入無窮遞迴
|
||||
- 指標在編碼的時候會輸出指標指向的內容,而空指標會輸出 null
|
||||
|
||||
|
||||
|
||||
@@ -9,18 +9,18 @@ Go 語言透過 `regexp` 標準套件為正則表示式提供了官方支援,
|
||||
|
||||
## 透過正則判斷是否匹配
|
||||
`regexp`套件中含有三個函式用來判斷是否匹配,如果匹配回傳 true,否則回傳 false
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func Match(pattern string, b []byte) (matched bool, error error)
|
||||
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)
|
||||
func MatchString(pattern string, s string) (matched bool, error error)
|
||||
|
||||
```
|
||||
|
||||
上面的三個函式實現了同一個功能,就是判斷 `pattern` 是否和輸入源匹配,匹配的話就回傳 true,如果解析正則出錯則回傳 error。三個函式的輸入源分別是 byte slice、RuneReader 和 string。
|
||||
|
||||
如果要驗證一個輸入是不是 IP 地址,那麼如何來判斷呢?請看如下實現
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func IsIP(ip string) (b bool) {
|
||||
if m, _ := regexp.MatchString("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$", ip); !m {
|
||||
return false
|
||||
@@ -29,8 +29,8 @@ func IsIP(ip string) (b bool) {
|
||||
}
|
||||
```
|
||||
可以看到,`regexp`的 pattern 和我們平常使用的正則一模一樣。再來看一個例子:當用戶輸入一個字串,我們想知道是不是一次合法的輸入:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
if len(os.Args) == 1 {
|
||||
fmt.Println("Usage: regexp [string]")
|
||||
@@ -48,8 +48,8 @@ func main() {
|
||||
Match 模式只能用來對字串的判斷,而無法擷取字串的一部分、過濾字串、或者提取出符合條件的一批字串。如果想要滿足這些需求,那就需要使用正則表示式的複雜模式。
|
||||
|
||||
我們經常需要一些爬蟲程式,下面就以爬蟲為例來說明如何使用正則來過濾或擷取抓取到的資料:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -96,13 +96,13 @@ func main() {
|
||||
|
||||
fmt.Println(strings.TrimSpace(src))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
從這個範例可以看出,使用複雜的正則首先是 Compile,它會解析正則表示式是否合法,如果正確,那麼就會回傳一個 Regexp,然後就可以利用回傳的 Regexp 在任意的字串上面執行需要的操作。
|
||||
|
||||
解析正則表示式的有如下幾個方法:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func Compile(expr string) (*Regexp, error)
|
||||
func CompilePOSIX(expr string) (*Regexp, error)
|
||||
func MustCompile(str string) *Regexp
|
||||
@@ -111,8 +111,8 @@ func MustCompilePOSIX(str string) *Regexp
|
||||
CompilePOSIX 和 Compile 的不同點在於 POSIX 必須使用 POSIX 語法,它使用最左最長方式搜尋,而 Compile 是採用的則只採用最左方式搜尋(例如[a-z]{2,4}這樣一個正則表示式,應用於"aa09aaa88aaaa"這個文字串時,CompilePOSIX 回傳了 aaaa,而 Compile 的回傳的是 aa)。字首有 Must 的函式表示,在解析正則語法的時候,如果匹配模式串不滿足正確的語法則直接 panic,而不加 Must 的則只是回傳錯誤。
|
||||
|
||||
在了解了如何建立一個 Regexp 之後,我們再來看一下這個 struct 提供了哪些方法來輔助我們操作字串,首先我們來看下面這些用來搜尋的函式:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func (re *Regexp) Find(b []byte) []byte
|
||||
func (re *Regexp) FindAll(b []byte, n int) [][]byte
|
||||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
|
||||
@@ -133,8 +133,8 @@ func (re *Regexp) FindSubmatch(b []byte) [][]byte
|
||||
func (re *Regexp) FindSubmatchIndex(b []byte) []int
|
||||
```
|
||||
上面這 18 個函式我們根據輸入源(byte slice、string 和 io.RuneReader)不同還可以繼續簡化成如下幾個,其他的只是輸入源不一樣,其他功能基本是一樣的:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func (re *Regexp) Find(b []byte) []byte
|
||||
func (re *Regexp) FindAll(b []byte, n int) [][]byte
|
||||
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
|
||||
@@ -145,8 +145,8 @@ func (re *Regexp) FindSubmatch(b []byte) [][]byte
|
||||
func (re *Regexp) FindSubmatchIndex(b []byte) []int
|
||||
```
|
||||
對於這些函式的使用我們來看下面這個例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -200,16 +200,18 @@ func main() {
|
||||
fmt.Println(submatchallindex)
|
||||
}
|
||||
```
|
||||
前面介紹過匹配函式,Regexp 也定義了三個函式,它們和同名的外部函式功能一模一樣,其實外部函式就是呼叫了這 Regexp 的三個函式來實現的:
|
||||
```Go
|
||||
|
||||
前面介紹過匹配函式,Regexp 也定義了三個函式,它們和同名的外部函式功能一模一樣,其實外部函式就是呼叫了這 Regexp 的三個函式來實現的:
|
||||
|
||||
```Go
|
||||
func (re *Regexp) Match(b []byte) bool
|
||||
func (re *Regexp) MatchReader(r io.RuneReader) bool
|
||||
func (re *Regexp) MatchString(s string) bool
|
||||
```
|
||||
接下里讓我們來了解替換函式是怎麼操作的?
|
||||
```Go
|
||||
|
||||
接下來讓我們來了解替換函式是怎麼操作的?
|
||||
|
||||
```Go
|
||||
func (re *Regexp) ReplaceAll(src, repl []byte) []byte
|
||||
func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte
|
||||
func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte
|
||||
@@ -220,14 +222,14 @@ func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) str
|
||||
這些替換函式我們在上面的抓網頁的例子有詳細應用範例,
|
||||
|
||||
接下來我們看一下 Expand 的解釋:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func (re *Regexp) Expand(dst []byte, template []byte, src []byte, match []int) []byte
|
||||
func (re *Regexp) ExpandString(dst []byte, template string, src string, match []int) []byte
|
||||
```
|
||||
那麼這個 Expand 到底用來幹嘛的呢?請看下面的例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func main() {
|
||||
src := []byte(`
|
||||
call hello alice
|
||||
|
||||
@@ -12,8 +12,8 @@ Web 應用反饋給客戶端的資訊中的大部分內容是靜態的,不變
|
||||
|
||||
## Go 範本使用
|
||||
在 Go 語言中,我們使用 `template` 套件來進行範本處理,使用類似`Parse`、`ParseFile`、`Execute`等方法從檔案或者字串載入範本,然後執行類似上面圖片展示的範本的 merge 操作。請看下面的例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
t := template.New("some template") //建立一個範本
|
||||
t, _ = t.ParseFiles("tmpl/welcome.html") //解析範本檔案
|
||||
@@ -34,8 +34,8 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
### 欄位操作
|
||||
Go 語言的範本透過 `{{}}` 來包含需要在渲染時被替換的欄位,`{{.}}`表示當前的物件,這和 Java 或者 C++中的 this 類似,如果要訪問當前物件的欄位透過`{{.FieldName}}`,但是需要注意一點:這個欄位必須是匯出的(欄位首字母必須是大寫的),否則在渲染的時候就會報錯,請看下面的這個例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -55,8 +55,8 @@ func main() {
|
||||
}
|
||||
```
|
||||
上面的程式碼我們可以正確的輸出`hello Astaxie`,但是如果我們稍微修改一下程式碼,在範本中含有了未匯出的欄位,那麼就會報錯
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type Person struct {
|
||||
UserName string
|
||||
email string //未匯出的欄位,首字母是小寫的
|
||||
@@ -75,8 +75,8 @@ t, _ = t.Parse("hello {{.UserName}}! {{.email}}")
|
||||
- {{with}}操作是指當前物件的值,類似上下文的概念
|
||||
|
||||
詳細的使用請看下面的例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -116,8 +116,8 @@ func main() {
|
||||
```
|
||||
### 條件處理
|
||||
在 Go 範本裡面如果需要進行條件判斷,那麼我們可以使用和 Go 語言的`if-else`語法類似的方式來處理,如果 pipeline 為空,那麼 if 就認為是 false,下面的例子展示了如何使用`if-else`語法:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -145,48 +145,48 @@ func main() {
|
||||
|
||||
### pipelines
|
||||
Unix 使用者已經很熟悉什麼是 `pipe` 了,`ls | grep "beego"`類似這樣的語法你是不是經常使用,過濾當前目錄下面的檔案,顯示含有"beego"的資料,表達的意思就是前面的輸出可以當做後面的輸入,最後顯示我們想要的資料,而 Go 語言範本最強大的一點就是支援 pipe 資料,在 Go 語言裡面任何 `{{}}` 裡面的都是 pipelines 資料,例如我們上面輸出的 email 裡面如果還有一些可能引起 XSS 注入的,那麼我們如何來進行轉化呢?
|
||||
|
||||
```Go
|
||||
|
||||
{{. | html}}
|
||||
|
||||
```
|
||||
|
||||
在 email 輸出的地方我們可以採用如上方式可以把輸出全部轉化 html 的實體,上面的這種方式和我們平常寫 Unix 的方式是不是一模一樣,操作起來相當的簡便,呼叫其他的函式也是類似的方式。
|
||||
|
||||
### 範本變數
|
||||
有時候,我們在範本使用過程中需要定義一些區域性變數,我們可以在一些操作中宣告區域性變數,例如 `with``range``if` 過程中宣告區域性變數,這個變數的作用域是 `{{end}}` 之前,Go 語言透過宣告的區域性變數格式如下所示:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
$variable := pipeline
|
||||
```
|
||||
詳細的例子看下面的:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
|
||||
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
|
||||
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
|
||||
|
||||
```
|
||||
|
||||
### 範本函式
|
||||
範本在輸出物件的欄位值時,採用了 `fmt` 套件把物件轉化成了字串。但是有時候我們的需求可能不是這樣的,例如有時候我們為了防止垃圾郵件傳送者透過採集網頁的方式來發送給我們的郵箱資訊,我們希望把 `@` 替換成 `at` 例如:`astaxie at beego.me`,如果要實現這樣的功能,我們就需要自訂函式來做這個功能。
|
||||
|
||||
每一個範本函式都有一個唯一值的名字,然後與一個 Go 函式關聯,透過如下的方式來關聯
|
||||
```Go
|
||||
|
||||
```Go
|
||||
type FuncMap map[string]interface{}
|
||||
```
|
||||
例如,如果我們想要的 email 函式的範本函式名是`emailDeal`,它關聯的 Go 函式名稱是`EmailDealWith`,那麼我們可以透過下面的方式來註冊這個函式
|
||||
```Go
|
||||
|
||||
```Go
|
||||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
|
||||
```
|
||||
`EmailDealWith`這個函式的參數和回傳值定義如下:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
func EmailDealWith(args …interface{}) string
|
||||
```
|
||||
我們來看下面的實現例子:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -244,11 +244,11 @@ func main() {
|
||||
Friends: []*Friend{&f1, &f2}}
|
||||
t.Execute(os.Stdout, p)
|
||||
}
|
||||
|
||||
```
|
||||
上面示範了如何自訂函式,其實,在範本套件內部已經有內建的實現函式,下面程式碼擷取自範本套件裡面
|
||||
```Go
|
||||
|
||||
上面示範了如何自訂函式,其實,在範本套件內部已經有內建的實現函式,下面程式碼擷取自範本套件裡面
|
||||
|
||||
```Go
|
||||
var builtins = FuncMap{
|
||||
"and": and,
|
||||
"call": call,
|
||||
@@ -263,12 +263,12 @@ var builtins = FuncMap{
|
||||
"println": fmt.Sprintln,
|
||||
"urlquery": URLQueryEscaper,
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Must 操作
|
||||
範本套件裡面有一個函式`Must`,它的作用是檢測範本是否正確,例如大括號是否匹配,註釋是否正確的關閉,變數是否正確的書寫。接下來我們示範一個例子,用 Must 來判斷範本是否正確:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -288,8 +288,8 @@ func main() {
|
||||
tErr := template.New("check parse error with Must")
|
||||
template.Must(tErr.Parse(" some static text {{ .Name }"))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
將輸出如下內容
|
||||
|
||||
```
|
||||
@@ -301,13 +301,13 @@ panic: template: check parse error with Must:1: unexpected "}" in command
|
||||
```
|
||||
## 巢狀範本
|
||||
我們平常開發 Web 應用的時候,經常會遇到一些範本有些部分是固定不變的,然後可以抽取出來作為一個獨立的部分,例如一個部落格的頭部和尾部是不變的,而唯一改變的是中間的內容部分。所以我們可以定義成`header`、`content`、`footer`三個部分。Go 語言中透過如下的語法來宣告
|
||||
```Go
|
||||
|
||||
```Go
|
||||
{{define "子範本名稱"}}內容{{end}}
|
||||
```
|
||||
透過如下方式來呼叫:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
{{template "子範本名稱"}}
|
||||
```
|
||||
接下來我們示範如何使用巢狀範本,我們定義三個檔案,`header.tmpl`、`content.tmpl`、`footer.tmpl`檔案,裡面的內容如下
|
||||
@@ -340,8 +340,8 @@ panic: template: check parse error with Must:1: unexpected "}" in command
|
||||
{{end}}
|
||||
```
|
||||
示範程式碼如下:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
|
||||
下面是示範程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -67,8 +68,9 @@ func main() {
|
||||
|
||||
開啟名稱為 name 的檔案,flag 是開啟的方式,只讀、讀寫等,perm 是許可權
|
||||
|
||||
### 寫檔案
|
||||
寫檔案函式:
|
||||
### 寫入檔案
|
||||
|
||||
寫入檔案函式:
|
||||
|
||||
- func (file *File) Write(b []byte) (n int, err Error)
|
||||
|
||||
@@ -82,9 +84,9 @@ func main() {
|
||||
|
||||
寫入 string 資訊到檔案
|
||||
|
||||
寫檔案的範例程式碼
|
||||
```Go
|
||||
寫入檔案的範例程式碼
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -107,8 +109,10 @@ func main() {
|
||||
}
|
||||
|
||||
```
|
||||
### 讀檔案
|
||||
讀檔案函式:
|
||||
|
||||
### 讀取檔案
|
||||
|
||||
讀取檔案函式:
|
||||
|
||||
- func (file *File) Read(b []byte) (n int, err Error)
|
||||
|
||||
@@ -118,7 +122,8 @@ func main() {
|
||||
|
||||
從 off 開始讀取資料到 b 中
|
||||
|
||||
讀檔案的範例程式碼:
|
||||
讀取檔案的範例程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -145,8 +150,8 @@ func main() {
|
||||
os.Stdout.Write(buf[:n])
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### 刪除檔案
|
||||
Go 語言裡面刪除檔案和刪除資料夾是同一個函式
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ func main() {
|
||||
```
|
||||
|
||||
- Format 系列函式把其他型別的轉換為字串
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
@@ -32,11 +32,12 @@ IPv6 是下一版本的網際網路協議,也可以說是下一代網際網路
|
||||
|
||||
### Go 支援的 IP 型別
|
||||
在 Go 的`net`套件中定義了很多型別、函式和方法用來網路程式設計,其中 IP 的定義如下:
|
||||
|
||||
```Go
|
||||
|
||||
type IP []byte
|
||||
|
||||
```
|
||||
|
||||
在 `net` 套件中有很多函式來操作 IP,但是其中比較有用的也就幾個,其中`ParseIP(s string) IP`函式會把一個 IPv4 或者 IPv6 的地址轉化成 IP 型別,請看下面的例子:
|
||||
|
||||
```Go
|
||||
@@ -61,8 +62,8 @@ func main() {
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
執行之後你就會發現只要你輸入一個 IP 地址就會給出相應的 IP 格式
|
||||
|
||||
## TCP Socket
|
||||
@@ -116,6 +117,7 @@ func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
|
||||
"HEAD / HTTP/1.0\r\n\r\n"
|
||||
|
||||
從伺服器端接收到的回應資訊格式可能如下:
|
||||
|
||||
```Go
|
||||
|
||||
HTTP/1.0 200 OK
|
||||
@@ -127,6 +129,7 @@ Date: Sat, 28 Aug 2010 00:43:48 GMT
|
||||
Server: lighttpd/1.4.23
|
||||
```
|
||||
我們的客戶端程式碼如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -161,18 +164,20 @@ func checkError(err error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
透過上面的程式碼我們可以看出:首先程式將使用者的輸入作為參數 `service` 傳入`net.ResolveTCPAddr`取得一個 tcpAddr,然後把 tcpAddr 傳入 DialTCP 後建立了一個 TCP 連線`conn`,透過 `conn` 來發送請求資訊,最後透過`ioutil.ReadAll`從 `conn` 中讀取全部的文字,也就是伺服器端回應反饋的資訊。
|
||||
|
||||
### TCP server
|
||||
上面我們編寫了一個 TCP 的客戶端程式,也可以透過 net 套件來建立一個伺服器端程式,在伺服器端我們需要繫結服務到指定的非啟用埠,並監聽此埠,當有客戶端請求到達的時候可以接收到來自客戶端連線的請求。net 套件中有相應功能的函式,函式定義如下:
|
||||
|
||||
```Go
|
||||
|
||||
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
|
||||
func (l *TCPListener) Accept() (Conn, error)
|
||||
```
|
||||
參數說明同 DialTCP 的參數一樣。下面我們實現一個簡單的時間同步服務,監聽 7777 埠
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -206,11 +211,12 @@ func checkError(err error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面的服務跑起來之後,它將會一直在那裡等待,直到有新的客戶端請求到達。當有新的客戶端請求到達並同意接受 `Accept` 該請求的時候他會反饋當前的時間資訊。值得注意的是,在程式碼中 `for` 迴圈裡,當有錯誤發生時,直接 continue 而不是退出,是因為在伺服器端跑程式碼的時候,當有錯誤發生的情況下最好是由伺服器端記錄錯誤,然後當前連線的客戶端直接報錯而退出,從而不會影響到當前伺服器端執行的整個服務。
|
||||
|
||||
上面的程式碼有個缺點,執行的時候是單任務的,不能同時接收多個請求,那麼該如何改造以使它支援多併發呢?Go 裡面有一個 goroutine 機制,請看下面改造後的程式碼
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -249,11 +255,12 @@ func checkError(err error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
透過把業務處理分離到函式`handleClient`,我們就可以進一步地實現多併發執行了。看上去是不是很帥,增加 `go` 關鍵詞就實現了伺服器端的多併發,從這個小例子也可以看出 goroutine 的強大之處。
|
||||
|
||||
有的朋友可能要問:這個伺服器端沒有處理客戶端實際請求的內容。如果我們需要透過從客戶端傳送不同的請求來取得不同的時間格式,而且需要一個長連線,該怎麼做呢?請看:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -314,25 +321,28 @@ func checkError(err error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在上面這個例子中,我們使用`conn.Read()`不斷讀取客戶端發來的請求。由於我們需要保持與客戶端的長連線,所以不能在讀取完一次請求後就關閉連線。由於`conn.SetReadDeadline()`設定了超時,當一定時間內客戶端無請求傳送,`conn`便會自動關閉,下面的 for 迴圈即會因為連線已關閉而跳出。需要注意的是,`request`在建立時需要指定一個最大長度以防止 flood attack;每次讀取到請求處理完畢後,需要清理 request,因為`conn.Read()`會將新讀取到的內容 append 到原內容之後。
|
||||
|
||||
### 控制 TCP 連線
|
||||
TCP 有很多連線控制函式,我們平常用到比較多的有如下幾個函式:
|
||||
|
||||
```Go
|
||||
|
||||
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)
|
||||
|
||||
```
|
||||
|
||||
設定建立連線的超時時間,客戶端和伺服器端都適用,當超過設定時間時,連線自動關閉。
|
||||
|
||||
```Go
|
||||
|
||||
func (c *TCPConn) SetReadDeadline(t time.Time) error
|
||||
func (c *TCPConn) SetWriteDeadline(t time.Time) error
|
||||
|
||||
```
|
||||
|
||||
用來設定寫入/讀取一個連線的超時時間。當超過設定時間時,連線自動關閉。
|
||||
|
||||
```Go
|
||||
|
||||
func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error
|
||||
@@ -342,6 +352,7 @@ func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error
|
||||
更多的內容請檢視 `net` 套件的文件。
|
||||
## UDP Socket
|
||||
Go 語言套件中處理 UDP Socket 和 TCP Socket 不同的地方就是在伺服器端處理多個客戶端請求資料套件的方式不同,UDP 缺少了對客戶端連線請求的 Accept 函式。其他基本幾乎一模一樣,只有 TCP 換成了 UDP 而已。UDP 的幾個主要函式如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
|
||||
@@ -351,6 +362,7 @@ func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error)
|
||||
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)
|
||||
```
|
||||
一個 UDP 的客戶端程式碼如下所示,我們可以看到不同的就是 TCP 換成了 UDP 而已:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -385,9 +397,10 @@ func checkError(err error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我們來看一下 UDP 伺服器端如何來處理:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -424,8 +437,8 @@ func checkError(err error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 總結
|
||||
透過對 TCP 和 UDP Socket 程式設計的描述和實現,可見 Go 已經完備地支援了 Socket 程式設計,而且使用起來相當的方便,Go 提供了很多函式,透過這些函式可以很容易就編寫出高效能的 Socket 應用。
|
||||
|
||||
|
||||
@@ -87,8 +87,8 @@ WebSocket 分為客戶端和伺服器端,接下來我們將實現一個簡單
|
||||
<button onclick="send();">Send Message</button>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
可以看到客戶端 JS,很容易的就透過 WebSocket 函式建立了一個與伺服器的連線 sock,當握手成功後,會觸發 WebScoket 物件的 onopen 事件,告訴客戶端連線已經成功建立。客戶端一共綁定了四個事件。
|
||||
|
||||
- 1)onopen 建立連線後觸發
|
||||
@@ -99,7 +99,6 @@ WebSocket 分為客戶端和伺服器端,接下來我們將實現一個簡單
|
||||
我們伺服器端的實現如下:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -139,8 +138,8 @@ func main() {
|
||||
log.Fatal("ListenAndServe:", err)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
當客戶端將使用者輸入的資訊 Send 之後,伺服器端透過 Receive 接收到了相應資訊,然後透過 Send 傳送了回應資訊。
|
||||
|
||||

|
||||
|
||||
@@ -63,7 +63,6 @@ Go 沒有為 REST 提供直接支援,但是因為 RESTful 是基於 HTTP 協
|
||||
我們現在可以透過 `POST` 裡面增加隱藏欄位 `_method` 這種方式可以來模擬`PUT`、`DELETE`等方式,但是伺服器端需要做轉換。我現在的專案裡面就按照這種方式來做的 REST 介面。當然 Go 語言裡面完全按照 RESTful 來實現是很容易的,我們透過下面的例子來說明如何實現 RESTful 的應用設計。
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -115,8 +114,8 @@ func main() {
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", router))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面的程式碼示範了如何編寫一個 REST 的應用,我們訪問的資源是使用者,我們透過不同的 method 來訪問不同的函式,這裡使用了第三方函式庫`github.com/julienschmidt/httprouter`,在前面章節我們介紹過如何實現自訂的路由器,這個函式庫實現了自訂路由和方便的路由規則對映,透過它,我們可以很方便的實現 REST 的架構。透過上面的程式碼可知,REST 就是根據不同的 method 訪問同一個資源的時候實現不同的邏輯處理。
|
||||
|
||||
## 總結
|
||||
|
||||
@@ -46,7 +46,6 @@ T、T1 和 T2 型別必須能被`encoding/gob`套件編解碼。
|
||||
http 的伺服器端程式碼實現如下:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -91,14 +90,13 @@ func main() {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
透過上面的例子可以看到,我們註冊了一個 Arith 的 RPC 服務,然後透過`rpc.HandleHTTP`函式把該服務註冊到了 HTTP 協議上,然後我們就可以利用 http 的方式來傳遞資料了。
|
||||
|
||||
請看下面的客戶端程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -144,21 +142,21 @@ func main() {
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
我們把上面的伺服器端和客戶端的程式碼分別編譯,然後先把伺服器端開啟,然後開啟客戶端,輸入程式碼,就會輸出如下資訊:
|
||||
```Go
|
||||
|
||||
我們把上面的伺服器端和客戶端的程式碼分別編譯,然後先把伺服器端開啟,然後開啟客戶端,輸入程式碼,就會輸出如下資訊:
|
||||
|
||||
```Go
|
||||
$ ./http_c localhost
|
||||
Arith: 17*8=136
|
||||
Arith: 17/8=2 remainder 1
|
||||
|
||||
```
|
||||
|
||||
透過上面的呼叫可以看到參數和回傳值是我們定義的 struct 型別,在伺服器端我們把它們當做呼叫函式的參數的型別,在客戶端作為`client.Call`的第 2,3 兩個參數的型別。客戶端最重要的就是這個 Call 函式,它有 3 個參數,第 1 個要呼叫的函式的名字,第 2 個是要傳遞的參數,第 3 個要回傳的參數(注意是指標型別),透過上面的程式碼例子我們可以發現,使用 Go 的 RPC 實現相當的簡單,方便。
|
||||
### TCP RPC
|
||||
上面我們實現了基於 HTTP 協議的 RPC,接下來我們要實現基於 TCP 協議的 RPC,伺服器端的實現程式碼如下所示:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -220,15 +218,14 @@ func checkError(err error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面這個程式碼和 http 的伺服器相比,不同在於 : 在此處我們採用了 TCP 協議,然後需要自己控制連線,當有客戶端連線上來後,我們需要把這個連線交給 rpc 來處理。
|
||||
|
||||
如果你留心了,你會發現這它是一個阻塞型的單使用者的程式,如果想要實現多併發,那麼可以使用 goroutine 來實現,我們前面在 socket 小節的時候已經介紹過如何處理 goroutine。
|
||||
下面展現了 TCP 實現的 RPC 客戶端:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -274,15 +271,14 @@ func main() {
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
這個客戶端程式碼和 http 的客戶端程式碼對比,唯一的區別一個是 DialHTTP,一個是 Dial(tcp),其他處理一模一樣。
|
||||
|
||||
### JSON RPC
|
||||
JSON RPC 是資料編碼採用了 JSON,而不是 gob 編碼,其他和上面介紹的 RPC 概念一模一樣,下面我們來示範一下,如何使用 Go 提供的 json-rpc 標準套件,請看伺服器端程式碼的實現:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -345,13 +341,13 @@ func checkError(err error) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
透過範例我們可以看出 json-rpc 是基於 TCP 協議實現的,目前它還不支援 HTTP 方式。
|
||||
|
||||
請看客戶端的實現程式碼:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -397,8 +393,8 @@ func main() {
|
||||
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 總結
|
||||
Go 已經提供了對 RPC 的良好支援,透過上面 HTTP、TCP、JSON RPC 的實現,我們就可以很方便的開發很多分散式的 Web 應用,我想作為讀者的你已經領會到這一點。但遺憾的是目前 Go 尚未提供對 SOAP RPC 的支援,欣慰的是現在已經有第三方的開源實現了。
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
與安全加密相關的,能夠增強我們的 Web 應用程式的強大手段就是加密,CSDN 洩密事件就是因為密碼儲存的是明文,使得攻擊拿手函式庫之後就可以直接實施一些破壞行為了。不過,和其他工具一樣,加密手段也必須運用得當。我們將在 9.5 小節介紹如何儲存密碼,如何讓密碼儲存的安全。
|
||||
|
||||
加密的本質就是擾亂資料,某些不可恢復的資料擾亂我們稱為單向加密或者雜湊演算法。另外還有一種雙向加密方式,也就是可以對加密後的資料進行解密。我們將會在 9.6 小節介紹如何實現這種雙向加密方式。
|
||||
加密的本質就是擾亂資料,某些不可還原的資料擾亂我們稱為單向加密或者雜湊演算法。另外還有一種雙向加密方式,也就是可以對加密後的資料進行解密。我們將會在 9.6 小節介紹如何實現這種雙向加密方式。
|
||||
|
||||
## 目錄
|
||||

|
||||
|
||||
@@ -50,11 +50,10 @@ CSRF 的防禦可以從伺服器端和客戶端兩方面著手,防禦效果是
|
||||
接下來我就以 Go 語言來舉例說明,如何限制對資源的訪問方法:
|
||||
|
||||
```Go
|
||||
|
||||
mux.Get("/user/:uid", getuser)
|
||||
mux.Post("/user/:uid", modifyuser)
|
||||
|
||||
```
|
||||
|
||||
這樣處理後,因為我們限定了修改只能使用 POST,當 GET 方式請求時就拒絕回應,所以上面圖示中 GET 方式的 CSRF 攻擊就可以防止了,但這樣就能全部解決問題了嗎?當然不是,因為 POST 也是可以模擬的。
|
||||
|
||||
因此我們需要實施第二步,在非 GET 方式的請求中增加隨機數,這個大概有三種方式來進行:
|
||||
@@ -66,7 +65,6 @@ mux.Post("/user/:uid", modifyuser)
|
||||
產生隨機數 token
|
||||
|
||||
```Go
|
||||
|
||||
h := md5.New()
|
||||
io.WriteString(h, strconv.FormatInt(crutime, 10))
|
||||
io.WriteString(h, "ganraomaxxxxxxxxx")
|
||||
@@ -74,18 +72,17 @@ token := fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
t, _ := template.ParseFiles("login.gtpl")
|
||||
t.Execute(w, token)
|
||||
|
||||
```
|
||||
|
||||
輸出 token
|
||||
```html
|
||||
|
||||
<input type="hidden" name="token" value="{{.}}">
|
||||
|
||||
```
|
||||
|
||||
驗證 token
|
||||
|
||||
```Go
|
||||
|
||||
r.ParseForm()
|
||||
token := r.Form.Get("token")
|
||||
if token != "" {
|
||||
@@ -93,8 +90,8 @@ if token != "" {
|
||||
} else {
|
||||
//不存在 token 報錯
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
這樣基本就實現了安全的 POST,但是也許你會說如果破解了 token 的演算法呢,按照理論上是,但是實際上破解是基本不可能的,因為有人曾計算過,暴力破解該串大概需要 2 的 11 次方時間。
|
||||
|
||||
## 總結
|
||||
|
||||
@@ -42,33 +42,33 @@
|
||||
</select>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
|
||||
```
|
||||
在處理這個表單的程式設計邏輯中,非常容易犯的錯誤是認為只能提交三個選擇中的一個。其實攻擊者可以模擬 POST 操作,提交 `name=attack` 這樣的資料,所以在此時我們需要做類似白名單的處理
|
||||
```Go
|
||||
|
||||
在處理這個表單的程式設計邏輯中,非常容易犯的錯誤是認為只能提交三個選擇中的一個。其實攻擊者可以模擬 POST 操作,提交 `name=attack` 這樣的資料,所以在此時我們需要做類似白名單的處理
|
||||
|
||||
```Go
|
||||
r.ParseForm()
|
||||
name := r.Form.Get("name")
|
||||
CleanMap := make(map[string]interface{}, 0)
|
||||
if name == "astaxie" || name == "herry" || name == "marry" {
|
||||
CleanMap["name"] = name
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面程式碼中我們初始化了一個 CleanMap 的變數,當判斷取得的 name 是`astaxie`、`herry`、`marry`三個中的一個之後
|
||||
,我們把資料儲存到了 CleanMap 之中,這樣就可以確保 CleanMap["name"]中的資料是合法的,從而在程式碼的其它部分使用它。當然我們還可以在 else 部分增加非法資料的處理,一種可能是再次顯示錶單並提示錯誤。但是不要試圖為了友好而輸出被汙染的資料。
|
||||
|
||||
上面的方法對於過濾一組已知的合法值的資料很有效,但是對於過濾有一組已知合法字元組成的資料時就沒有什麼幫助。例如,你可能需要一個使用者名稱只能由字母及數字組成:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
r.ParseForm()
|
||||
username := r.Form.Get("username")
|
||||
CleanMap := make(map[string]interface{}, 0)
|
||||
if ok, _ := regexp.MatchString("^[a-zA-Z0-9]+$", username); ok {
|
||||
CleanMap["username"] = username
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 總結
|
||||
資料過濾在 Web 安全中起到一個基石的作用,大多數的安全問題都是由於沒有過濾資料和驗證資料引起的,例如前面小節的 CSRF 攻擊,以及接下來將要介紹的 XSS 攻擊、SQL 注入等都是沒有認真地過濾資料引起的,因此我們需要特別重視這部分的內容。
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@ Web 應用未對使用者提交請求的資料做充分的檢查過濾,允許
|
||||
`w.Header().Set("Content-Type","text/javascript")`
|
||||
|
||||
這樣就可以讓瀏覽器解析 javascript 程式碼,而不會是 html 輸出。
|
||||
|
||||
```
|
||||
|
||||
## 總結
|
||||
XSS 漏洞是相當有危害的,在開發 Web 應用的時候,一定要記住過濾資料,特別是在輸出到客戶端之前,這是現在行之有效的防止 XSS 的手段。
|
||||
|
||||
|
||||
@@ -16,23 +16,26 @@ SQL 注入攻擊(SQL Injection),簡稱注入攻擊,是 Web 開發中最
|
||||
<p>Password: <input type="password" name="password" /></p>
|
||||
<p><input type="submit" value="登陸" /></p>
|
||||
</form>
|
||||
|
||||
```
|
||||
|
||||
我們的處理裡面的 SQL 可能是這樣的:
|
||||
|
||||
```Go
|
||||
|
||||
username:=r.Form.Get("username")
|
||||
password:=r.Form.Get("password")
|
||||
sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'"
|
||||
|
||||
```
|
||||
|
||||
如果使用者的輸入的使用者名稱如下,密碼任意
|
||||
|
||||
```Go
|
||||
|
||||
myuser' or 'foo' = 'foo' --
|
||||
|
||||
```
|
||||
|
||||
那麼我們的 SQL 變成了如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx'
|
||||
@@ -40,12 +43,14 @@ SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='x
|
||||
在 SQL 裡面`--`是註釋標記,所以查詢語句會在此中斷。這就讓攻擊者在不知道任何合法使用者名稱和密碼的情況下成功登入了。
|
||||
|
||||
對於 MSSQL 還有更加危險的一種 SQL 注入,就是控制系統,下面這個可怕的例子將示範如何在某些版本的 MSSQL 資料庫上執行系統命令。
|
||||
|
||||
```Go
|
||||
|
||||
sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'"
|
||||
Db.Exec(sql)
|
||||
```
|
||||
如果攻擊提交`a%' exec master..xp_cmdshell 'net user test testpass /ADD' --`作為變數 prod 的值,那麼 sql 將會變成
|
||||
|
||||
```Go
|
||||
|
||||
sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'"
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
那麼我們作為一個 Web 應用開發者,在選擇密碼儲存方案時, 容易掉入哪些陷阱, 以及如何避免這些陷阱?
|
||||
|
||||
## 普通方案
|
||||
目前用的最多的密碼儲存方案是將明文密碼做單向雜湊後儲存,單向雜湊演算法有一個特徵:無法透過雜湊後的摘要(digest)恢復原始資料,這也是“單向”二字的來源。常用的單向雜湊演算法包括 SHA-256, SHA-1, MD5 等。
|
||||
目前用的最多的密碼儲存方案是將明文密碼做單向雜湊後儲存,單向雜湊演算法有一個特徵:無法透過雜湊後的摘要(digest)還原原始資料,這也是“單向”二字的來源。常用的單向雜湊演算法包括 SHA-256, SHA-1, MD5 等。
|
||||
|
||||
Go 語言對這三種加密演算法的實現如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
//import "crypto/sha256"
|
||||
@@ -23,8 +24,8 @@ fmt.Printf("% x", h.Sum(nil))
|
||||
h := md5.New()
|
||||
io.WriteString(h, "需要加密的密碼")
|
||||
fmt.Printf("%x", h.Sum(nil))
|
||||
|
||||
```
|
||||
|
||||
單向雜湊有兩個特性:
|
||||
|
||||
- 1)同一個密碼進行單向雜湊,得到的總是唯一確定的摘要。
|
||||
@@ -65,8 +66,8 @@ io.WriteString(h, salt2)
|
||||
io.WriteString(h, pwmd5)
|
||||
|
||||
last :=fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
```
|
||||
|
||||
在兩個 salt 沒有洩露的情況下,黑客如果拿到的是最後這個加密串,就幾乎不可能推算出原始的密碼是什麼了。
|
||||
|
||||
## 專家方案
|
||||
@@ -79,6 +80,7 @@ last :=fmt.Sprintf("%x", h.Sum(nil))
|
||||
這裡推薦 `scrypt` 方案,scrypt 是由著名的 FreeBSD 黑客 Colin Percival 為他的備份服務 Tarsnap 開發的。
|
||||
|
||||
目前 Go 語言裡面支援的函式庫 https://github.com/golang/crypto/tree/master/scrypt
|
||||
|
||||
```Go
|
||||
|
||||
dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)
|
||||
|
||||
@@ -38,8 +38,8 @@ func main() {
|
||||
|
||||
fmt.Println(string(enbyte))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 高階加解密
|
||||
|
||||
Go 語言的 `crypto` 裡面支援對稱加密的高階加解密套件有:
|
||||
@@ -48,6 +48,7 @@ Go 語言的 `crypto` 裡面支援對稱加密的高階加解密套件有:
|
||||
- `crypto/des`套件:DES(Data Encryption Standard),是一種對稱加密標準,是目前使用最廣泛的金鑰系統,特別是在保護金融資料的安全中。曾是美國聯邦政府的加密標準,但現已被 AES 所替代。
|
||||
|
||||
因為這兩種演算法使用方法類似,所以在此,我們僅用 aes 套件為例來講解它們的使用,請看下面的例子
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -97,8 +98,8 @@ func main() {
|
||||
cfbdec.XORKeyStream(plaintextCopy, ciphertext)
|
||||
fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面透過呼叫函式`aes.NewCipher`(參數 key 必須是 16、24 或者 32 位的[]byte,分別對應 AES-128, AES-192 或 AES-256 演算法),回傳了一個`cipher.Block`介面,這個介面實現了三個功能:
|
||||
|
||||
```Go
|
||||
|
||||
@@ -23,6 +23,7 @@ GO 語言預設採用"UTF-8"編碼集,所以我們實現 i18n 時不考慮第
|
||||
|
||||
|
||||
我們可以透過下面的程式碼來實現域名的對應 locale:
|
||||
|
||||
```Go
|
||||
|
||||
if r.Host == "www.asta.com" {
|
||||
@@ -34,6 +35,7 @@ if r.Host == "www.asta.com" {
|
||||
}
|
||||
```
|
||||
當然除了整域名設定地區之外,我們還可以透過子域名來設定地區,例如"en.asta.com"表示英文站點,"cn.asta.com"表示中文站點。實現程式碼如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
prefix := strings.Split(r.Host,".")
|
||||
@@ -55,6 +57,7 @@ if prefix[0] == "en" {
|
||||
這種設定方式幾乎擁有前面講的透過域名設定 Locale 的所有優點,它採用 RESTful 的方式,以使得我們不需要增加額外的方法來處理。但是這種方式需要在每一個的 link 裡面增加相應的參數 locale,這也許有點複雜而且有時候甚至相當的繁瑣。不過我們可以寫一個通用的函式 url,讓所有的 link 地址都透過這個函式來產生,然後在這個函式裡面增加`locale=params["locale"]`參數來緩解一下。
|
||||
|
||||
也許我們希望 URL 地址看上去更加的 RESTful 一點,例如:www.asta.com/en/books (英文站點)和 www.asta.com/zh/books (中文站點),這種方式的 URL 更加有利於 SEO,而且對於使用者也比較友好,能夠透過 URL 直觀的知道訪問的站點。那麼這樣的 URL 地址可以透過 router 來取得 locale(參考 REST 小節裡面介紹的 router 外掛實現):
|
||||
|
||||
```Go
|
||||
|
||||
mux.Get("/:locale/books", listbook)
|
||||
@@ -65,6 +68,7 @@ mux.Get("/:locale/books", listbook)
|
||||
- Accept-Language
|
||||
|
||||
客戶端請求的時候在 HTTP 頭資訊裡面有`Accept-Language`,一般的客戶端都會設定該資訊,下面是 Go 語言實現的一個簡單的根據`Accept-Language`實現設定地區的程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
AL := r.Header.Get("Accept-Language")
|
||||
|
||||
@@ -35,11 +35,12 @@ func msg(locale, key string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面範例示範了不同 locale 的文字翻譯,實現了中文和英文對於同一個 key 顯示不同語言的實現,上面實現了中文的文字訊息,如果想切換到英文版本,只需要把 lang 設定為 en 即可。
|
||||
|
||||
有些時候僅是 key-value 替換是不能滿足需要的,例如"I am 30 years old",中文表達是"我今年 30 歲了",而此處的 30 是一個變數,該怎麼辦呢?這個時候,我們可以結合`fmt.Printf`函式來實現,請看下面的程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
en["how old"] ="I am %d years old"
|
||||
@@ -66,9 +67,10 @@ loc,_:=time.LoadLocation(msg(lang,"time_zone"))
|
||||
t:=time.Now()
|
||||
t = t.In(loc)
|
||||
fmt.Println(t.Format(time.RFC3339))
|
||||
|
||||
```
|
||||
|
||||
我們可以透過類似處理文字格式的方式來解決時間格式的問題,舉例如下:
|
||||
|
||||
```Go
|
||||
|
||||
en["date_format"]="%Y-%m-%d %H:%M:%S"
|
||||
@@ -87,10 +89,11 @@ func date(fomate string,t time.Time) string{
|
||||
//%d 替換成 24
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 本地化貨幣值
|
||||
各個地區的貨幣表示也不一樣,處理方式也與日期差不多,細節請看下面程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
en["money"] ="USD %d"
|
||||
@@ -101,8 +104,8 @@ fmt.Println(money_format(msg(lang,"date_format"),100))
|
||||
func money_format(fomate string,money int64) string{
|
||||
return fmt.Sprintf(fomate,money)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 本地化檢視和資源
|
||||
我們可能會根據 Locale 的不同來展示檢視,這些檢視包含不同的圖片、css、js 等各種靜態資源。那麼應如何來處理這些資訊呢?首先我們應按 locale 來組織檔案資訊,請看下面的檔案目錄安排:
|
||||
```html
|
||||
@@ -120,9 +123,10 @@ views
|
||||
|--css
|
||||
index.tpl
|
||||
login.tpl
|
||||
|
||||
```
|
||||
|
||||
有了這個目錄結構後我們就可以在渲染的地方這樣來實現程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
s1, _ := template.ParseFiles("views/"+lang+"/index.tpl")
|
||||
|
||||
@@ -22,16 +22,18 @@
|
||||
"create": "Create"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
為了支援國際化,在此我們使用了一個國際化相關的套件——[go-i18n](https://github.com/astaxie/go-i18n),首先我們向 go-i18n 套件註冊 config/locales 這個目錄,以載入所有的 locale 檔案
|
||||
|
||||
```Go
|
||||
|
||||
Tr:=i18n.NewLocale()
|
||||
Tr.LoadPath("config/locales")
|
||||
|
||||
```
|
||||
|
||||
這個套件使用起來很簡單,你可以透過下面的方式進行測試:
|
||||
|
||||
```Go
|
||||
|
||||
fmt.Println(Tr.Translate("submit"))
|
||||
@@ -44,6 +46,7 @@ fmt.Println(Tr.Translate("submit"))
|
||||
## 自動載入本地套件
|
||||
|
||||
上面我們介紹了如何自動載入自訂語言套件,其實 go-i18n 函式庫已經預載入了很多預設的格式資訊,例如時間格式、貨幣格式,使用者可以在自訂配置時改寫這些預設配置,請看下面的處理過程:
|
||||
|
||||
```Go
|
||||
|
||||
//載入預設配置檔案,這些檔案都放在 go-i18n/locales 下面
|
||||
@@ -89,9 +92,10 @@ func (il *IL) loadDefaultTranslations(dirPath string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
透過上面的方法載入配置資訊到預設的檔案,這樣我們就可以在我們沒有自訂時間資訊的時候執行如下的程式碼取得對應的資訊:
|
||||
|
||||
```Go
|
||||
|
||||
//locale=zh 的情況下,執行如下程式碼:
|
||||
@@ -111,6 +115,7 @@ fmt.Println(Tr.Money(11.11))
|
||||
1. 文字資訊
|
||||
|
||||
文字資訊呼叫`Tr.Translate`來實現相應的資訊轉換,mapFunc 的實現如下:
|
||||
|
||||
```Go
|
||||
|
||||
func I18nT(args ...interface{}) string {
|
||||
@@ -124,14 +129,16 @@ func I18nT(args ...interface{}) string {
|
||||
}
|
||||
return Tr.Translate(s)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
註冊函式如下:
|
||||
|
||||
```Go
|
||||
|
||||
t.Funcs(template.FuncMap{"T": I18nT})
|
||||
```
|
||||
範本中使用如下:
|
||||
|
||||
```Go
|
||||
|
||||
{{.V.Submit | T}}
|
||||
@@ -140,6 +147,7 @@ t.Funcs(template.FuncMap{"T": I18nT})
|
||||
2. 時間日期
|
||||
|
||||
時間日期呼叫`Tr.Time`函式來實現相應的時間轉換,mapFunc 的實現如下:
|
||||
|
||||
```Go
|
||||
|
||||
func I18nTimeDate(args ...interface{}) string {
|
||||
@@ -155,11 +163,13 @@ func I18nTimeDate(args ...interface{}) string {
|
||||
}
|
||||
```
|
||||
註冊函式如下:
|
||||
|
||||
```Go
|
||||
|
||||
t.Funcs(template.FuncMap{"TD": I18nTimeDate})
|
||||
```
|
||||
範本中使用如下:
|
||||
|
||||
```Go
|
||||
|
||||
{{.V.Now | TD}}
|
||||
@@ -167,6 +177,7 @@ t.Funcs(template.FuncMap{"TD": I18nTimeDate})
|
||||
3. 貨幣資訊
|
||||
|
||||
貨幣呼叫`Tr.Money`函式來實現相應的時間轉換,mapFunc 的實現如下:
|
||||
|
||||
```Go
|
||||
|
||||
func I18nMoney(args ...interface{}) string {
|
||||
@@ -182,11 +193,13 @@ func I18nMoney(args ...interface{}) string {
|
||||
}
|
||||
```
|
||||
註冊函式如下:
|
||||
|
||||
```Go
|
||||
|
||||
t.Funcs(template.FuncMap{"M": I18nMoney})
|
||||
```
|
||||
範本中使用如下:
|
||||
|
||||
```Go
|
||||
|
||||
{{.V.Money | M}}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# 11.1 錯誤處理
|
||||
Go 語言主要的設計準則是:簡潔、明白,簡潔是指語法和 C 類似,相當的簡單,明白是指任何語句都是很明顯的,不含有任何隱含的東西,在錯誤處理方案的設計中也貫徹了這一思想。我們知道在 C 語言裡面是透過回傳-1 或者 NULL 之類別的資訊來表示錯誤,但是對於使用者來說,不檢視相應的 API 說明文件,根本搞不清楚這個回傳值究竟代表什麼意思,比如 : 回傳 0 是成功,還是失敗,而 Go 定義了一個叫做 error 的型別,來明確的表達錯誤。在使用時,透過把回傳的 error 變數與 nil 的比較,來判定操作是否成功。例如`os.Open`函式在開啟檔案失敗時將回傳一個不為 nil 的 error 變數
|
||||
|
||||
```Go
|
||||
|
||||
func Open(name string) (file *File, err error)
|
||||
```
|
||||
下面這個例子透過呼叫`os.Open`開啟一個檔案,如果出現錯誤,那麼就會呼叫`log.Fatal`來輸出錯誤資訊:
|
||||
|
||||
```Go
|
||||
|
||||
f, err := os.Open("filename.ext")
|
||||
@@ -15,6 +17,7 @@ if err != nil {
|
||||
類似於`os.Open`函式,標準套件中所有可能出錯的 API 都會回傳一個 error 變數,以方便錯誤處理,這個小節將詳細地介紹 error 型別的設計,和討論開發 Web 應用中如何更好地處理 error。
|
||||
## Error 型別
|
||||
error 型別是一個介面型別,這是它的定義:
|
||||
|
||||
```Go
|
||||
|
||||
type error interface {
|
||||
@@ -35,6 +38,7 @@ func (e *errorString) Error() string {
|
||||
}
|
||||
```
|
||||
你可以透過`errors.New`把一個字串轉化為 errorString,以得到一個滿足介面 error 的物件,其內部實現如下:
|
||||
|
||||
```Go
|
||||
|
||||
// New returns an error that formats as the given text.
|
||||
@@ -43,6 +47,7 @@ func New(text string) error {
|
||||
}
|
||||
```
|
||||
下面這個例子示範了如何使用`errors.New`:
|
||||
|
||||
```Go
|
||||
|
||||
func Sqrt(f float64) (float64, error) {
|
||||
@@ -53,6 +58,7 @@ func Sqrt(f float64) (float64, error) {
|
||||
}
|
||||
```
|
||||
在下面的例子中,我們在呼叫 Sqrt 的時候傳遞的一個負數,然後就得到了 non-nil 的 error 物件,將此物件與 nil 比較,結果為 true,所以 fmt.Println(fmt 套件在處理 error 時會呼叫 Error 方法)被呼叫,以輸出錯誤,請看下面呼叫的範例程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
f, err := Sqrt(-1)
|
||||
@@ -63,6 +69,7 @@ f, err := Sqrt(-1)
|
||||
## 自訂 Error
|
||||
|
||||
透過上面的介紹我們知道 error 是一個 interface,所以在實現自己的套件的時候,透過定義實現此介面的結構,我們就可以實現自己的錯誤定義,請看來自 Json 套件的範例:
|
||||
|
||||
```Go
|
||||
|
||||
type SyntaxError struct {
|
||||
@@ -73,6 +80,7 @@ type SyntaxError struct {
|
||||
func (e *SyntaxError) Error() string { return e.msg }
|
||||
```
|
||||
Offset 欄位在呼叫 Error 的時候不會被列印,但是我們可以透過型別斷言取得錯誤型別,然後可以列印相應的錯誤資訊,請看下面的例子:
|
||||
|
||||
```Go
|
||||
|
||||
if err := dec.Decode(&val); err != nil {
|
||||
@@ -84,6 +92,7 @@ if err := dec.Decode(&val); err != nil {
|
||||
}
|
||||
```
|
||||
需要注意的是,函式回傳自訂錯誤時,回傳值推薦設定為 error 型別,而非自訂錯誤型別,特別需要注意的是不應預宣告自訂錯誤型別的變數。例如:
|
||||
|
||||
```Go
|
||||
|
||||
func Decode() *SyntaxError { // 錯誤,將可能導致上層呼叫者 err!=nil 的判斷永遠為 true。
|
||||
@@ -98,6 +107,7 @@ func Decode() *SyntaxError { // 錯誤,將可能導致上層呼叫者 err!=nil
|
||||
原因見 http://golang.org/doc/faq#nil_error
|
||||
|
||||
上面例子簡單的示範了如何自訂 Error 型別。但是如果我們還需要更復雜的錯誤處理呢?此時,我們來參考一下 net 套件採用的方法:
|
||||
|
||||
```Go
|
||||
|
||||
package net
|
||||
@@ -107,9 +117,10 @@ type Error interface {
|
||||
Timeout() bool // Is the error a timeout?
|
||||
Temporary() bool // Is the error temporary?
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在呼叫的地方,透過型別斷言 err 是不是 net.Error,來細化錯誤的處理,例如下面的例子,如果一個網路發生臨時性錯誤,那麼將會 sleep 1 秒之後重試:
|
||||
|
||||
```Go
|
||||
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
|
||||
@@ -124,6 +135,7 @@ if err != nil {
|
||||
Go 在錯誤處理上採用了與 C 類似的檢查回傳值的方式,而不是其他多數主流語言採用的異常方式,這造成了程式碼編寫上的一個很大的缺點 : 錯誤處理程式碼的冗餘,對於這種情況是我們透過複用檢測函式來減少類似的程式碼。
|
||||
|
||||
請看下面這個例子程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
func init() {
|
||||
@@ -144,6 +156,7 @@ func viewRecord(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
上面的例子中取得資料和範本展示呼叫時都有檢測錯誤,當有錯誤發生時,呼叫了統一的處理函式`http.Error`,回傳給客戶端 500 錯誤碼,並顯示相應的錯誤資料。但是當越來越多的 HandleFunc 加入之後,這樣的錯誤處理邏輯程式碼就會越來越多,其實我們可以透過自訂路由器來縮減程式碼(實現的思路可以參考第三章的 HTTP 詳解)。
|
||||
|
||||
```Go
|
||||
|
||||
type appHandler func(http.ResponseWriter, *http.Request) error
|
||||
@@ -155,6 +168,7 @@ func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
上面我們定義了自訂的路由器,然後我們可以透過如下方式來註冊函式:
|
||||
|
||||
```Go
|
||||
|
||||
func init() {
|
||||
@@ -162,6 +176,7 @@ func init() {
|
||||
}
|
||||
```
|
||||
當請求/view 的時候我們的邏輯處理可以變成如下程式碼,和第一種實現方式相比較已經簡單了很多。
|
||||
|
||||
```Go
|
||||
|
||||
func viewRecord(w http.ResponseWriter, r *http.Request) error {
|
||||
@@ -175,6 +190,7 @@ func viewRecord(w http.ResponseWriter, r *http.Request) error {
|
||||
}
|
||||
```
|
||||
上面的例子錯誤處理的時候所有的錯誤回傳給使用者的都是 500 錯誤碼,然後顯示出來相應的錯誤程式碼,其實我們可以把這個錯誤資訊定義的更加友好,除錯的時候也方便定位問題,我們可以自訂回傳的錯誤型別:
|
||||
|
||||
```Go
|
||||
|
||||
type appError struct {
|
||||
@@ -184,6 +200,7 @@ type appError struct {
|
||||
}
|
||||
```
|
||||
這樣我們的自訂路由器可以改成如下方式:
|
||||
|
||||
```Go
|
||||
|
||||
type appHandler func(http.ResponseWriter, *http.Request) *appError
|
||||
@@ -197,6 +214,7 @@ func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
這樣修改完自訂錯誤之後,我們的邏輯處理可以改成如下方式:
|
||||
|
||||
```Go
|
||||
|
||||
func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
|
||||
|
||||
@@ -96,6 +96,7 @@ GDB 的一些常用命令如下所示
|
||||
|
||||
## 除錯過程
|
||||
我們透過下面這個程式碼來示範如何透過 GDB 來除錯 Go 程式,下面是將要示範的程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 12 部署與維護
|
||||
到目前為止,我們前面已經介紹了如何開發程式、除錯程式以及測試程式,正如人們常說的:開發最後的 10%需要花費 90%的時間,所以這一章我們將強調這最後的 10%部分,要真正成為讓人信任並使用的優秀應用,需要考慮到一些細節,以上所說的 10%就是指這些小細節。
|
||||
|
||||
本章我們將透過四個小節來介紹這些小細節的處理,第一小節介紹如何在生產服務上記錄程式產生的日誌,如何記錄日誌,第二小節介紹發生錯誤時我們的程式如何處理,如何保證儘量少的影響到使用者的訪問,第三小節介紹如何來部署 Go 的獨立程式,由於目前 Go 程式還無法像 C 那樣寫成 daemon,那麼我們如何管理這樣的程序程式後臺執行呢?第四小節將介紹應用資料的備份和恢復,儘量保證應用在崩潰的情況能夠保持資料的完整性。
|
||||
本章我們將透過四個小節來介紹這些小細節的處理,第一小節介紹如何在生產服務上記錄程式產生的日誌,如何記錄日誌,第二小節介紹發生錯誤時我們的程式如何處理,如何保證儘量少的影響到使用者的訪問,第三小節介紹如何來部署 Go 的獨立程式,由於目前 Go 程式還無法像 C 那樣寫成 daemon,那麼我們如何管理這樣的程序程式後臺執行呢?第四小節將介紹應用資料的備份和還原,儘量保證應用在崩潰的情況能夠保持資料的完整性。
|
||||
## 目錄
|
||||

|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
### 基於 logrus 的自訂日誌處理
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -100,9 +101,10 @@ seelog 是用 Go 語言實現的一個日誌系統,它提供了一些簡單的
|
||||
```Go
|
||||
|
||||
go get -u github.com/cihub/seelog
|
||||
|
||||
```
|
||||
|
||||
然後我們來看一個簡單的例子:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -113,12 +115,13 @@ func main() {
|
||||
defer log.Flush()
|
||||
log.Info("Hello from Seelog!")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
編譯後執行如果出現了`Hello from seelog`,說明 seelog 日誌系統已經成功安裝並且可以正常運行了。
|
||||
|
||||
### 基於 seelog 的自訂日誌處理
|
||||
seelog 支援自訂日誌處理,下面是我基於它自訂的日誌處理套件的部分內容:
|
||||
|
||||
```Go
|
||||
|
||||
package logs
|
||||
@@ -201,6 +204,7 @@ func UseLogger(newLogger seelog.LoggerInterface) {
|
||||
設定當前的日誌器為相應的日誌處理
|
||||
|
||||
上面我們定義了一個自訂的日誌處理套件,下面就是使用範例:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -230,6 +234,7 @@ func main() {
|
||||
郵件的格式透過 criticalemail 配置,然後透過其他的配置傳送郵件伺服器的配置,透過 recipient 配置接收郵件的使用者,如果有多個使用者可以再新增一行。
|
||||
|
||||
要測試這個程式碼是否正常工作,可以在程式碼中增加類似下面的一個假訊息。不過記住過後要把它刪除,否則上線之後就會收到很多垃圾郵件。
|
||||
|
||||
```Go
|
||||
|
||||
logs.Logger.Critical("test Critical message")
|
||||
@@ -239,6 +244,7 @@ logs.Logger.Critical("test Critical message")
|
||||
對於應用日誌,每個人的應用場景可能會各不相同,有些人利用應用日誌來做資料分析,有些人利用應用日誌來做效能分析,有些人來做使用者行為分析,還有些就是純粹的記錄,以方便應用出現問題的時候輔助查詢問題。
|
||||
|
||||
舉一個例子,我們需要追蹤使用者嘗試登陸系統的操作。這裡會把成功與不成功的嘗試都記錄下來。記錄成功的使用"Info"日誌級別,而不成功的使用"warn"級別。如果想查詢所有不成功的登陸,我們可以利用 linux 的 grep 之類別的命令工具,如下:
|
||||
|
||||
```Go
|
||||
|
||||
# cat /data/logs/roll.log | grep "failed login"
|
||||
|
||||
@@ -78,8 +78,8 @@
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
404 的錯誤處理邏輯,如果是系統的錯誤也是類似的操作,同時我們看到在:
|
||||
|
||||
```Go
|
||||
@@ -106,12 +106,13 @@
|
||||
ErrorInfo := "系統暫時不可用" //取得當前報錯資訊
|
||||
t.Execute(w, ErrorInfo) //執行範本的 merger 操作
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 如何處理異常
|
||||
我們知道在很多其他語言中有 try...catch 關鍵詞,用來捕獲異常情況,但是其實很多錯誤都是可以預期發生的,而不需要異常處理,應該當做錯誤來處理,這也是為什麼 Go 語言採用了函式回傳錯誤的設計,這些函式不會 panic,例如如果一個檔案找不到,os.Open 回傳一個錯誤,它不會 panic;如果你向一箇中斷的網路連線寫資料,net.Conn 系列型別的 Write 函式回傳一個錯誤,它們不會 panic。這些狀態在這樣的程式裡都是可以預期的。你知道這些操作可能會失敗,因為設計者已經用回傳錯誤清楚地表明了這一點。這就是上面所講的可以預期發生的錯誤。
|
||||
|
||||
但是還有一種情況,有一些操作幾乎不可能失敗,而且在一些特定的情況下也沒有辦法回傳錯誤,也無法繼續執行,這樣情況就應該 panic。舉個例子:如果一個程式計算 x[j],但是 j 越界了,這部分程式碼就會導致 panic,像這樣的一個不可預期嚴重錯誤就會引起 panic,在預設情況下它會殺掉程序,它允許一個正在執行這部分程式碼的 goroutine 從發生錯誤的 panic 中恢復執行,發生 panic 之後,這部分程式碼後面的函式和程式碼都不會繼續執行,這是 Go 特意這樣設計的,因為要區別於錯誤和異常,panic 其實就是異常處理。如下程式碼,我們期望透過 uid 來取得 User 中的 username 資訊,但是如果 uid 越界了就會丟擲異常,這個時候如果我們沒有 recover 機制,程序就會被殺死,從而導致程式不可服務。因此為了程式的健壯性,在一些地方需要建立 recover 機制。
|
||||
|
||||
```Go
|
||||
|
||||
func GetUser(uid int) (username string) {
|
||||
|
||||
@@ -37,6 +37,7 @@ if *d {
|
||||
```
|
||||
|
||||
- 另一種是利用 syscall 的方案,但是這個方案並不完善:
|
||||
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -169,8 +170,8 @@ startsecs = 5
|
||||
user = root
|
||||
redirect_stderr = true
|
||||
stdout_logfile = /var/log/supervisord/blogdemon.log
|
||||
|
||||
```
|
||||
|
||||
### Supervisord 管理
|
||||
Supervisord 安裝完成後有兩個可用的命令列 supervisor 和 supervisorctl,命令使用解釋如下:
|
||||
|
||||
@@ -188,4 +189,4 @@ Supervisord 安裝完成後有兩個可用的命令列 supervisor 和 supervisor
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一章: [網站錯誤處理](<12.2.md>)
|
||||
* 下一節: [備份和恢復](<12.4.md>)
|
||||
* 下一節: [備份和還原](<12.4.md>)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 12.4 備份和恢復
|
||||
這小節我們要討論應用程式管理的另一個方面:生產伺服器上資料的備份和恢復。我們經常會遇到生產伺服器的網路斷了、硬碟壞了、作業系統崩潰、或者資料庫不可用了等各種異常情況,所以維護人員需要對生產伺服器上的應用和資料做好異地災備,冷備熱備的準備。在接下來的介紹中,講解了如何備份應用、如何備份/恢復 Mysql 資料庫和 redis 資料庫。
|
||||
# 12.4 備份和還原
|
||||
這小節我們要討論應用程式管理的另一個方面:生產伺服器上資料的備份和還原。我們經常會遇到生產伺服器的網路斷了、硬碟壞了、作業系統崩潰、或者資料庫不可用了等各種異常情況,所以維護人員需要對生產伺服器上的應用和資料做好異地災備,冷備熱備的準備。在接下來的介紹中,講解了如何備份應用、如何備份/還原 Mysql 資料庫和 redis 資料庫。
|
||||
|
||||
## 應用備份
|
||||
在大多數叢集環境下,Web 應用程式基本不需要備份,因為這個其實就是一個程式碼副本,我們在本地開發環境中,或者版本控制系統中已經保持這些程式碼。但是很多時候,一些開發的站點需要使用者來上傳檔案,那麼我們需要對這些使用者上傳的檔案進行備份。目前其實有一種合適的做法就是把和網站相關的需要儲存的檔案儲存到雲儲存,這樣即使系統崩潰,只要我們的檔案還在雲端儲存上,至少資料不會丟失。
|
||||
@@ -59,7 +59,7 @@ rsync 主要有以下三個配置檔案 rsyncd.conf(主配置檔案)、rsyncd.se
|
||||
|
||||
|
||||
## MySQL 備份
|
||||
應用資料庫目前還是 MySQL 為主流,目前 MySQL 的備份有兩種方式:熱備份和冷備份,熱備份目前主要是採用 master/slave 方式(master/slave 方式的同步目前主要用於資料庫讀寫分離,也可以用於熱備份資料),關於如何配置這方面的資料,大家可以找到很多。冷備份的話就是資料有一定的延遲,但是可以保證該時間段之前的資料完整,例如有些時候可能我們的誤操作引起了資料的丟失,那麼 master/slave 模式是無法找回丟失資料的,但是透過冷備份可以部分恢復資料。
|
||||
應用資料庫目前還是 MySQL 為主流,目前 MySQL 的備份有兩種方式:熱備份和冷備份,熱備份目前主要是採用 master/slave 方式(master/slave 方式的同步目前主要用於資料庫讀寫分離,也可以用於熱備份資料),關於如何配置這方面的資料,大家可以找到很多。冷備份的話就是資料有一定的延遲,但是可以保證該時間段之前的資料完整,例如有些時候可能我們的誤操作引起了資料的丟失,那麼 master/slave 模式是無法找回丟失資料的,但是透過冷備份可以部分還原資料。
|
||||
|
||||
冷備份一般使用 shell 指令碼來實現定時備份資料庫,然後透過上面介紹 rsync 同步非本地機房的一臺伺服器。
|
||||
|
||||
@@ -148,25 +148,27 @@ rsync 主要有以下三個配置檔案 rsyncd.conf(主配置檔案)、rsyncd.se
|
||||
|
||||
00 00 * * * /root/mysql_backup.sh
|
||||
|
||||
## MySQL 恢復
|
||||
前面介紹 MySQL 備份分為熱備份和冷備份,熱備份主要的目的是為了能夠即時的恢復,例如應用伺服器出現了硬碟故障,那麼我們可以透過修改配置檔案把資料庫的讀取和寫入改成 slave,這樣就可以儘量少時間的中斷服務。
|
||||
## MySQL 還原
|
||||
前面介紹 MySQL 備份分為熱備份和冷備份,熱備份主要的目的是為了能夠即時的還原,例如應用伺服器出現了硬碟故障,那麼我們可以透過修改配置檔案把資料庫的讀取和寫入改成 slave,這樣就可以儘量少時間的中斷服務。
|
||||
|
||||
但是有時候我們需要透過冷備份的 SQL 來進行資料恢復,既然有了資料庫的備份,就可以透過命令匯入:
|
||||
但是有時候我們需要透過冷備份的 SQL 來進行資料還原,既然有了資料庫的備份,就可以透過命令匯入:
|
||||
|
||||
mysql -u username -p databse < backup.sql
|
||||
|
||||
可以看到,匯出和匯入資料庫資料都是相當簡單,不過如果還需要管理許可權,或者其他的一些字符集的設定的話,可能會稍微複雜一些,但是這些都是可以透過一些命令來完成的。
|
||||
|
||||
## redis 備份
|
||||
|
||||
redis 是目前我們使用最多的 NoSQL,它的備份也分為兩種:熱備份和冷備份,redis 也支援 master/slave 模式,所以我們的熱備份可以透過這種方式實現,相應的配置大家可以參考官方的文件配置,相當的簡單。我們這裡介紹冷備份的方式:redis 其實會定時的把記憶體裡面的快取資料儲存到資料庫檔案裡面,我們備份只要備份相應的檔案就可以,就是利用前面介紹的 rsync 備份到非本地機房就可以實現。
|
||||
|
||||
## redis 恢復
|
||||
redis 的恢復分為熱備份恢復和冷備份恢復,熱備份恢復的目的和方法同 MySQL 的恢復一樣,只要修改應用的相應的資料庫連線即可。
|
||||
## redis 還原
|
||||
|
||||
但是有時候我們需要根據冷備份來恢復資料,redis 的冷備份恢復其實就是隻要把儲存的資料庫檔案 copy 到 redis 的工作目錄,然後啟動 redis 就可以了,redis 在啟動的時候會自動載入資料庫檔案到記憶體中,啟動的速度根據資料庫的檔案大小來決定。
|
||||
redis 的還原分為熱備份還原和冷備份還原,熱備份還原的目的和方法同 MySQL 的還原一樣,只要修改應用的相應的資料庫連線即可。
|
||||
|
||||
但是有時候我們需要根據冷備份來還原資料,redis 的冷備份還原其實就是隻要把儲存的資料庫檔案 copy 到 redis 的工作目錄,然後啟動 redis 就可以了,redis 在啟動的時候會自動載入資料庫檔案到記憶體中,啟動的速度根據資料庫的檔案大小來決定。
|
||||
|
||||
## 小結
|
||||
本小節介紹了我們的應用部分的備份和恢復,即如何做好災備,包括檔案的備份、資料庫的備份。同時也介紹了使用 rsync 同步不同系統的檔案,MySQL 資料庫和 redis 資料庫的備份和恢復,希望透過本小節的介紹,能夠給作為開發的你對於線上產品的災備方案提供一個參考方案。
|
||||
本小節介紹了我們的應用部分的備份和還原,即如何做好災備,包括檔案的備份、資料庫的備份。同時也介紹了使用 rsync 同步不同系統的檔案,MySQL 資料庫和 redis 資料庫的備份和還原,希望透過本小節的介紹,能夠給作為開發的你對於線上產品的災備方案提供一個參考方案。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
- 處理 404 錯誤,告訴使用者請求的頁面找不到
|
||||
- 將應用部署到一個生產環境中(包括如何部署更新)
|
||||
- 如何讓部署的應用程式具有高可用
|
||||
- 備份和恢復檔案以及資料庫
|
||||
- 備份和還原檔案以及資料庫
|
||||
|
||||
讀完本章內容後,對於從頭開始開發一個 Web 應用需要考慮那些問題,你應該已經有了全面的了解。本章內容將有助於你在實際環境中管理前面各章介紹開發的程式碼。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一章: [備份和恢復](<12.4.md>)
|
||||
* 上一章: [備份和還原](<12.4.md>)
|
||||
* 下一節: [如何設計一個 Web 框架](<13.0.md>)
|
||||
@@ -9,6 +9,7 @@ HTTP 路由元件負責將 HTTP 請求交到對應的函式處理(或者是一
|
||||
路由器就是根據使用者請求的事件資訊轉發到相應的處理函式(控制層)。
|
||||
## 預設的路由實現
|
||||
在 3.4 小節有過介紹 Go 的 http 套件的詳解,裡面介紹了 Go 的 http 套件如何設計和實現路由,這裡繼續以一個例子來說明:
|
||||
|
||||
```Go
|
||||
|
||||
func fooHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -22,8 +23,8 @@ http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
|
||||
```
|
||||
|
||||
上面的例子呼叫了 http 預設的 DefaultServeMux 來新增路由,需要提供兩個參數,第一個參數是希望使用者訪問此資源的 URL 路徑(儲存在 r.URL.Path),第二參數是即將要執行的函式,以提供使用者訪問的資源。路由的思路主要集中在兩點:
|
||||
|
||||
- 新增路由資訊
|
||||
@@ -32,6 +33,7 @@ log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
Go 預設的路由新增是透過函式`http.Handle`和`http.HandleFunc`等來新增,底層都是呼叫了`DefaultServeMux.Handle(pattern string, handler Handler)`,這個函式會把路由資訊儲存在一個 map 資訊中`map[string]muxEntry`,這就解決了上面說的第一點。
|
||||
|
||||
Go 監聽埠,然後接收到 tcp 連線會扔給 Handler 來處理,上面的例子預設 nil 即為`http.DefaultServeMux`,透過`DefaultServeMux.ServeHTTP`函式來進行排程,遍歷之前儲存的 map 路由資訊,和使用者訪問的 URL 進行匹配,以查詢對應註冊的處理函式,這樣就實現了上面所說的第二點。
|
||||
|
||||
```Go
|
||||
|
||||
for k, v := range mux.m {
|
||||
@@ -52,12 +54,13 @@ for k, v := range mux.m {
|
||||
- 無法很好的支援 REST 模式,無法限制訪問的方法,例如上面的例子中,使用者訪問/foo,可以用 GET、POST、DELETE、HEAD 等方式訪問
|
||||
- 一般網站的路由規則太多了,編寫繁瑣。我前面自己開發了一個 API 應用,路由規則有三十幾條,這種路由多了之後其實可以進一步簡化,透過 struct 的方法進行一種簡化
|
||||
|
||||
beego 框架的路由器基於上面的幾點限制考慮設計了一種 REST 方式的路由實現,路由設計也是基於上面 Go 預設設計的兩點來考慮:儲存路由和轉發路由
|
||||
beego 框架的路由器基於上面的幾點限制考慮設計了一種 REST 方式的路由實現,路由設計也是基於上面 Go 預設設計的兩點來考慮:儲存路由和轉向路由
|
||||
|
||||
### 儲存路由
|
||||
針對前面所說的限制點,我們首先要解決參數支援就需要用到正則,第二和第三點我們透過一種變通的方法來解決,REST 的方法對應到 struct 的方法中去,然後路由到 struct 而不是函式,這樣在轉發路由的時候就可以根據 method 來執行不同的方法。
|
||||
針對前面所說的限制點,我們首先要解決參數支援就需要用到正則,第二和第三點我們透過一種變通的方法來解決,REST 的方法對應到 struct 的方法中去,然後路由到 struct 而不是函式,這樣在轉向路由的時候就可以根據 method 來執行不同的方法。
|
||||
|
||||
根據上面的思路,我們設計了兩個資料型別 controllerInfo(儲存路徑和對應的 struct,這裡是一個 reflect.Type 型別)和 ControllerRegistor(routers 是一個 slice 用來儲存使用者新增的路由資訊,以及 beego 框架的應用資訊)
|
||||
|
||||
```Go
|
||||
|
||||
type controllerInfo struct {
|
||||
@@ -70,14 +73,16 @@ type ControllerRegistor struct {
|
||||
routers []*controllerInfo
|
||||
Application *App
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
ControllerRegistor 對外的介面函式有
|
||||
|
||||
```Go
|
||||
|
||||
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface)
|
||||
```
|
||||
詳細的實現如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) {
|
||||
@@ -127,6 +132,7 @@ func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) {
|
||||
```
|
||||
### 靜態路由實現
|
||||
上面我們實現的動態路由的實現,Go 的 http 套件預設支援靜態檔案處理 FileServer,由於我們實現了自訂的路由器,那麼靜態檔案也需要自己設定,beego 的靜態資料夾路徑儲存在全域性變數 StaticDir 中,StaticDir 是一個 map 型別,實現如下:
|
||||
|
||||
```Go
|
||||
|
||||
func (app *App) SetStaticPath(url string, path string) *App {
|
||||
@@ -134,16 +140,18 @@ func (app *App) SetStaticPath(url string, path string) *App {
|
||||
return app
|
||||
}
|
||||
```
|
||||
|
||||
應用中設定靜態路徑可以使用如下方式實現:
|
||||
```Go
|
||||
|
||||
```Go
|
||||
beego.SetStaticPath("/img","/static/img")
|
||||
|
||||
```
|
||||
### 轉發路由
|
||||
轉發路由是基於 ControllerRegistor 裡的路由資訊來進行轉發的,詳細的實現如下程式碼所示:
|
||||
```Go
|
||||
|
||||
### 轉向路由
|
||||
|
||||
轉向路由是基於 ControllerRegistor 裡的路由資訊來進行轉發的,詳細的實現如下程式碼所示:
|
||||
|
||||
```Go
|
||||
// AutoRoute
|
||||
func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
@@ -256,16 +264,19 @@ func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
基於這樣的路由設計之後就可以解決前面所說的三個限制點,使用的方式如下所示:
|
||||
|
||||
基本的使用註冊路由:
|
||||
|
||||
```Go
|
||||
|
||||
beego.BeeApp.RegisterController("/", &controllers.MainController{})
|
||||
```
|
||||
參數註冊:
|
||||
|
||||
```Go
|
||||
|
||||
beego.BeeApp.RegisterController("/:param", &controllers.UserController{})
|
||||
```
|
||||
正則匹配:
|
||||
|
||||
```Go
|
||||
|
||||
beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{})
|
||||
|
||||
@@ -36,6 +36,7 @@ type ControllerInterface interface {
|
||||
}
|
||||
```
|
||||
那麼前面介紹的路由 add 函式的時候是定義了 ControllerInterface 型別,因此,只要我們實現這個介面就可以,所以我們的基底類別 Controller 實現如下的方法:
|
||||
|
||||
```Go
|
||||
|
||||
func (c *Controller) Init(ct *Context, cn string) {
|
||||
@@ -118,6 +119,7 @@ func (c *Controller) Redirect(url string, code int) {
|
||||
}
|
||||
```
|
||||
上面的 controller 基底類別已經實現了介面定義的函式,透過路由根據 url 執行相應的 controller 的原則,會依次執行如下:
|
||||
|
||||
```Go
|
||||
|
||||
Init() 初始化
|
||||
@@ -129,6 +131,7 @@ Finish() 執行完之後執行的操作,每個繼承的子類別可以來
|
||||
```
|
||||
## 應用指南
|
||||
上面 beego 框架中完成了 controller 基底類別的設計,那麼我們在我們的應用中可以這樣來設計我們的方法:
|
||||
|
||||
```Go
|
||||
|
||||
package controllers
|
||||
|
||||
@@ -36,6 +36,7 @@ func SetLevel(l int) {
|
||||
}
|
||||
```
|
||||
上面這一段實現了日誌系統的日誌分級,預設的級別是 Trace,使用者透過 SetLevel 可以設定不同的分級。
|
||||
|
||||
```Go
|
||||
|
||||
// logger references the used application logger.
|
||||
@@ -120,6 +121,7 @@ func Critical(v ...interface{}) {
|
||||
配置資訊的解析,beego 實現了一個 key=value 的配置檔案讀取,類似 ini 配置檔案的格式,就是一個檔案解析的過程,然後把解析的資料儲存到 map 中,最後在呼叫的時候通過幾個 string、int 之類別的函式呼叫回傳相應的值,具體的實現請看下面:
|
||||
|
||||
首先定義了一些 ini 配置檔案的一些全域常數:
|
||||
|
||||
```Go
|
||||
var (
|
||||
bComment = []byte{'#'}
|
||||
@@ -129,6 +131,7 @@ var (
|
||||
)
|
||||
```
|
||||
定義了配置檔案的格式:
|
||||
|
||||
```Go
|
||||
|
||||
// A Config represents the configuration.
|
||||
@@ -141,6 +144,7 @@ type Config struct {
|
||||
}
|
||||
```
|
||||
定義了解析檔案的函式,解析檔案的過程是開啟檔案,然後一行一行的讀取,解析註釋、空行和 key=value 資料:
|
||||
|
||||
```Go
|
||||
|
||||
// ParseFile creates a new Config and parses the file configuration from the
|
||||
@@ -203,6 +207,7 @@ func LoadConfig(name string) (*Config, error) {
|
||||
}
|
||||
```
|
||||
下面實現了一些讀取配置檔案的函式,回傳的值確定為 bool、int、float64 或 string:
|
||||
|
||||
```Go
|
||||
|
||||
// Bool returns the boolean value for a given key.
|
||||
@@ -227,6 +232,7 @@ func (c *Config) String(key string) string {
|
||||
```
|
||||
## 應用指南
|
||||
下面這個函式是我一個應用中的例子,用來取得遠端 url 地址的 json 資料,實現如下:
|
||||
|
||||
```Go
|
||||
|
||||
func GetJson() {
|
||||
@@ -244,12 +250,13 @@ func GetJson() {
|
||||
}
|
||||
```
|
||||
函式中呼叫了框架的日誌函式`beego.Critical`函式用來報錯,呼叫了`beego.AppConfig.String("url")`用來取得配置檔案中的資訊,配置檔案的資訊如下(app.conf):
|
||||
|
||||
```Go
|
||||
|
||||
appname = hs
|
||||
url ="http://www.api.com/api.html"
|
||||
|
||||
```
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一章: [controller 設計](<13.3.md>)
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
## 部落格路由
|
||||
部落格主要的路由規則如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
//顯示部落格首頁
|
||||
@@ -36,8 +37,8 @@ beego.Router("/new", &controllers.NewController{})
|
||||
beego.Router("/delete/:id([0-9]+)", &controllers.DeleteController{})
|
||||
//編輯博文
|
||||
beego.Router("/edit/:id([0-9]+)", &controllers.EditController{})
|
||||
|
||||
```
|
||||
|
||||
## 資料庫結構
|
||||
資料庫設計最簡單的部落格資訊
|
||||
```sql
|
||||
@@ -81,6 +82,7 @@ func (this *ViewController) Get() {
|
||||
}
|
||||
```
|
||||
NewController
|
||||
|
||||
```Go
|
||||
|
||||
type NewController struct {
|
||||
@@ -103,6 +105,7 @@ func (this *NewController) Post() {
|
||||
}
|
||||
```
|
||||
EditController
|
||||
|
||||
```Go
|
||||
|
||||
type EditController struct {
|
||||
@@ -128,6 +131,7 @@ func (this *EditController) Post() {
|
||||
}
|
||||
```
|
||||
DeleteController
|
||||
|
||||
```Go
|
||||
|
||||
type DeleteController struct {
|
||||
@@ -143,6 +147,7 @@ func (this *DeleteController) Get() {
|
||||
}
|
||||
```
|
||||
## model 層
|
||||
|
||||
```Go
|
||||
|
||||
package models
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
## beego 靜態檔案實現和設定
|
||||
Go 的 net/http 套件中提供了靜態檔案的服務,`ServeFile`和 `FileServer` 等函式。beego 的靜態檔案處理就是基於這一層處理的,具體的實現如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
//static file server
|
||||
@@ -18,6 +19,7 @@ for prefix, staticDir := range StaticDir {
|
||||
StaticDir 裡面儲存的是相應的 url 對應到靜態檔案所在的目錄,因此在處理 URL 請求的時候只需要判斷對應的請求地址是否包含靜態處理開頭的 url,如果包含的話就採用 http.ServeFile 提供服務。
|
||||
|
||||
舉例如下:
|
||||
|
||||
```Go
|
||||
|
||||
beego.StaticDir["/asset"] = "/static"
|
||||
@@ -49,6 +51,7 @@ Bootstrap 是 Twitter 推出的一個開源的用於前端開發的工具套件
|
||||
圖 14.2 專案中靜態檔案目錄結構
|
||||
|
||||
2. 因為 beego 預設設定了 StaticDir 的值,所以如果你的靜態檔案目錄是 static 的話就無須再增加了:
|
||||
|
||||
```Go
|
||||
|
||||
StaticDir["/static"] = "static"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
## session 整合
|
||||
beego 中主要有以下的全域性變數來控制 session 處理:
|
||||
|
||||
```Go
|
||||
|
||||
//related to session
|
||||
@@ -15,6 +16,7 @@ SessionGCMaxLifetime int64 // cookies 有效期
|
||||
GlobalSessions *session.Manager //全域性 session 控制器
|
||||
```
|
||||
當然上面這些變數需要初始化值,也可以按照下面的程式碼來配合配置檔案以設定這些值:
|
||||
|
||||
```Go
|
||||
|
||||
if ar, err := AppConfig.Bool("sessionon"); err != nil {
|
||||
@@ -40,6 +42,7 @@ if ar, err := AppConfig.Int("sessiongcmaxlifetime"); err != nil && ar != 0 {
|
||||
}
|
||||
```
|
||||
在 beego.Run 函式中增加如下程式碼:
|
||||
|
||||
```Go
|
||||
|
||||
if SessionOn {
|
||||
@@ -50,6 +53,7 @@ if SessionOn {
|
||||
這樣只要 SessionOn 設定為 true,那麼就會預設開啟 session 功能,獨立開一個 goroutine 來處理 session。
|
||||
|
||||
為了方便我們在自訂 Controller 中快速使用 session,作者在`beego.Controller`中提供了如下方法:
|
||||
|
||||
```Go
|
||||
|
||||
func (c *Controller) StartSession() (sess session.Session) {
|
||||
@@ -61,12 +65,14 @@ func (c *Controller) StartSession() (sess session.Session) {
|
||||
透過上面的程式碼我們可以看到,beego 框架簡單地繼承了 session 功能,那麼在專案中如何使用呢?
|
||||
|
||||
首先我們需要在應用的 main 入口處開啟 session:
|
||||
|
||||
```Go
|
||||
|
||||
beego.SessionOn = true
|
||||
```
|
||||
|
||||
然後我們就可以在控制器的相應方法中如下所示的使用 session 了:
|
||||
|
||||
```Go
|
||||
|
||||
func (this *MainController) Get() {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
對於開發者來說,一般開發過程都是相當複雜,而且大多是在重複一樣的工作。假設一個場景專案中忽然需要增加一個表單資料,那麼區域性程式碼的整個流程都需要修改。我們知道 Go 裡面 struct 是常用的一個數據結構,因此 beego 的 form 採用了 struct 來處理表單資訊。
|
||||
|
||||
首先定義一個開發 Web 應用時相對應的 struct,一個欄位對應一個 form 元素,透過 struct 的 tag 來定義相應的元素資訊和驗證資訊,如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
type User struct{
|
||||
@@ -32,6 +33,7 @@ type User struct{
|
||||
}
|
||||
```
|
||||
定義好 struct 之後接下來在 controller 中這樣操作
|
||||
|
||||
```Go
|
||||
|
||||
func (this *AddController) Get() {
|
||||
@@ -49,6 +51,7 @@ func (this *AddController) Get() {
|
||||
</form>
|
||||
```
|
||||
上面我們定義好了整個的第一步,從 struct 到顯示錶單的過程,接下來就是使用者填寫資訊,伺服器端接收資料然後驗證,最後插入資料庫。
|
||||
|
||||
```Go
|
||||
|
||||
func (this *AddController) Post() {
|
||||
|
||||
@@ -9,11 +9,13 @@ beego 目前沒有針對這三種方式進行任何形式的整合,但是可
|
||||
|
||||
## HTTP Basic 和 HTTP Digest 認證
|
||||
這兩個認證是一些應用採用的比較簡單的認證,目前已經有開源的第三方函式庫支援這兩個認證:
|
||||
|
||||
```Go
|
||||
|
||||
github.com/abbot/go-http-auth
|
||||
```
|
||||
下面程式碼示範了如何把這個函式庫引入 beego 中從而實現認證:
|
||||
|
||||
```Go
|
||||
|
||||
package controllers
|
||||
@@ -52,6 +54,7 @@ func (this *MainController) Get() {
|
||||
|
||||
## oauth 和 oauth2 的認證
|
||||
oauth 和 oauth2 是目前比較流行的兩種認證方式,還好第三方有一個函式庫實現了這個認證,但是是國外實現的,並沒有 QQ、微博之類別的國內應用認證整合:
|
||||
|
||||
```Go
|
||||
|
||||
github.com/bradrydzewski/go.auth
|
||||
@@ -59,12 +62,14 @@ github.com/bradrydzewski/go.auth
|
||||
下面程式碼示範了如何把該函式庫引入 beego 中從而實現 oauth 的認證,這裡以 github 為例示範:
|
||||
|
||||
1. 新增兩條路由
|
||||
|
||||
```Go
|
||||
|
||||
beego.RegisterController("/auth/login", &controllers.GithubController{})
|
||||
beego.RegisterController("/mainpage", &controllers.PageController{})
|
||||
```
|
||||
2. 然後我們處理 GithubController 登陸的頁面:
|
||||
|
||||
```Go
|
||||
package controllers
|
||||
|
||||
@@ -92,9 +97,10 @@ func (this *GithubController) Get() {
|
||||
|
||||
githubHandler.ServeHTTP(this.Ctx.ResponseWriter, this.Ctx.Request)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
3. 處理登陸成功之後的頁面
|
||||
|
||||
```Go
|
||||
package controllers
|
||||
|
||||
@@ -130,8 +136,8 @@ func (this *PageController) Get() {
|
||||
this.Data["name"] = user.Name()
|
||||
this.TplNames = "home.tpl"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
整個的流程如下,首先開啟瀏覽器輸入地址:
|
||||
|
||||

|
||||
@@ -152,6 +158,7 @@ func (this *PageController) Get() {
|
||||
|
||||
## 自訂認證
|
||||
自訂的認證一般都是和 session 結合驗證的,如下程式碼來源於一個基於 beego 的開源部落格:
|
||||
|
||||
```Go
|
||||
|
||||
//登陸處理
|
||||
@@ -248,6 +255,7 @@ func checkUsername(username string) (b bool) {
|
||||
}
|
||||
```
|
||||
有了使用者登陸和註冊之後,其他模組的地方可以增加如下這樣的使用者是否登陸的判斷:
|
||||
|
||||
```Go
|
||||
|
||||
func (this *AddBlogController) Prepare() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
## i18n 整合
|
||||
beego 中設定全域性變數如下:
|
||||
|
||||
```Go
|
||||
|
||||
Translation i18n.IL
|
||||
@@ -11,6 +12,7 @@ Lang string //設定語言套件,zh、en
|
||||
LangPath string //設定語言套件所在位置
|
||||
```
|
||||
初始化多語言函式:
|
||||
|
||||
```Go
|
||||
|
||||
func InitLang(){
|
||||
@@ -20,6 +22,7 @@ func InitLang(){
|
||||
}
|
||||
```
|
||||
為了方便在範本中直接呼叫多語言套件,我們設計了三個函式來處理回應的多語言:
|
||||
|
||||
```Go
|
||||
|
||||
beegoTplFuncMap["Trans"] = i18n.I18nT
|
||||
@@ -64,6 +67,7 @@ func I18nMoney(args ...interface{}) string {
|
||||
```
|
||||
## 多語言開發使用
|
||||
1. 設定語言以及語言套件所在位置,然後初始化 i18n 物件:
|
||||
|
||||
```Go
|
||||
|
||||
beego.Lang = "zh"
|
||||
@@ -98,6 +102,7 @@ beego.InitLang()
|
||||
|
||||
|
||||
我們可以在 controller 中呼叫翻譯取得回應的翻譯語言,如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
func (this *MainController) Get() {
|
||||
@@ -106,6 +111,7 @@ func (this *MainController) Get() {
|
||||
}
|
||||
```
|
||||
我們也可以在範本中直接呼叫回應的翻譯函式:
|
||||
|
||||
```Go
|
||||
|
||||
//直接文字翻譯
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# 14.6 pprof 支援
|
||||
Go 語言有一個非常棒的設計就是標準函式庫裡面帶有程式碼的效能監聽工具,在兩個地方有套件:
|
||||
|
||||
```Go
|
||||
|
||||
net/http/pprof
|
||||
@@ -50,11 +51,12 @@ func (this *ProfController) Get() {
|
||||
}
|
||||
this.Ctx.ResponseWriter.WriteHeader(200)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 使用入門
|
||||
|
||||
透過上面的設計,你可以透過如下程式碼開啟 pprof:
|
||||
|
||||
```Go
|
||||
|
||||
beego.PprofOn = true
|
||||
@@ -71,6 +73,7 @@ beego.PprofOn = true
|
||||
圖 14.8 顯示當前 goroutine 的詳細資訊
|
||||
|
||||
我們還可以透過命令列取得更多詳細的資訊
|
||||
|
||||
```Go
|
||||
|
||||
go tool pprof http://localhost:8080/debug/pprof/profile
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
* [應用日誌](12.1.md)
|
||||
* [網站錯誤處理](12.2.md)
|
||||
* [應用部署](12.3.md)
|
||||
* [備份和恢復](12.4.md)
|
||||
* [備份和還原](12.4.md)
|
||||
* [小結](12.5.md)
|
||||
* [如何設計一個 Web 框架](13.0.md)
|
||||
* [專案規劃](13.1.md)
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
- 12.1 [應用日誌](12.1.md)
|
||||
- 12.2 [網站錯誤處理](12.2.md)
|
||||
- 12.3 [應用部署](12.3.md)
|
||||
- 12.4 [備份和恢復](12.4.md)
|
||||
- 12.4 [備份和還原](12.4.md)
|
||||
- 12.5 [小結](12.5.md)
|
||||
* 13.[如何設計一個 Web 框架](13.0.md)
|
||||
- 13.1 [專案規劃](13.1.md)
|
||||
|
||||
Reference in New Issue
Block a user