Fix many CN to TW terms issues
This commit is contained in:
@@ -10,13 +10,13 @@ Go 是一種新的語言,一種併發的、帶垃圾回收的、快速編譯
|
||||
- Go 完全是垃圾回收型的語言,併為併發執行與通訊提供了基本的支援。
|
||||
- 按照其設計,Go 打算為多核機器上系統軟體的構造提供一種方法。
|
||||
|
||||
Go 是一種編譯型語言,它結合瞭解釋型語言的遊刃有餘,動態型別語言的開發效率,以及靜態型別的安全性。它也打算成為現代的,支援網路與多核計算的語言。要滿足這些目標,需要解決一些語言上的問題:一個富有表達能力但輕量級的型別系統,併發與垃圾回收機制,嚴格的依賴規範等等。這些無法透過函式庫或工具解決好,因此 Go 也就應運而生了。
|
||||
Go 是一種編譯型語言,它結合了解釋型語言的遊刃有餘,動態型別語言的開發效率,以及靜態型別的安全性。它也打算成為現代的,支援網路與多核計算的語言。要滿足這些目標,需要解決一些語言上的問題:一個富有表達能力但輕量級的型別系統,併發與垃圾回收機制,嚴格的依賴規範等等。這些無法透過函式庫或工具解決好,因此 Go 也就應運而生了。
|
||||
|
||||
在本章中,我們將講述 Go 的安裝方法,以及如何配置專案資訊。
|
||||
|
||||
## 目錄
|
||||
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -40,7 +40,7 @@ Go 1.5 徹底移除 C 程式碼,Runtime、Compiler、Linker 均由 Go 編寫
|
||||
|
||||
當你設定完畢之後在命令列裡面輸入`go`,看到如下圖片即說明你已經安裝成功
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.1 原始碼安裝之後執行 Go 命令的圖
|
||||
|
||||
@@ -79,7 +79,7 @@ Linux 系統使用者可透過在 Terminal 中執行命令`arch`(即`uname -m`)
|
||||
|
||||
### Mac 安裝
|
||||
|
||||
訪問 [ 下載地址][downlink],32 位系統下載 go1.4.2.darwin-386-osx10.8.pkg(更新的版本已無 32 位下載),64 位系統下載 go1.8.3.darwin-amd64.pkg,雙擊下載檔案,一路預設安裝點選下一步,這個時候 go 已經安裝到你的系統中,預設已經在 PATH 中增加了相應的`~/go/bin`,這個時候開啟終端,輸入`go`
|
||||
訪問 [Golang 下載地址](https://golang.org/dl/),32 位系統下載 go1.4.2.darwin-386-osx10.8.pkg(更新的版本已無 32 位下載),64 位系統下載 go1.8.3.darwin-amd64.pkg,雙擊下載檔案,一路預設安裝點選下一步,這個時候 go 已經安裝到你的系統中,預設已經在 PATH 中增加了相應的`~/go/bin`,這個時候開啟終端,輸入`go`
|
||||
|
||||
看到類似上面原始碼安裝成功的圖片說明已經安裝成功
|
||||
|
||||
@@ -87,7 +87,7 @@ Linux 系統使用者可透過在 Terminal 中執行命令`arch`(即`uname -m`)
|
||||
|
||||
### Linux 安裝
|
||||
|
||||
訪問 [ 下載地址][downlink],32 位系統下載 go1.8.3.linux-386.tar.gz,64 位系統下載 go1.8.3.linux-amd64.tar.gz,
|
||||
訪問 [Golang 下載地址](https://golang.org/dl/),32 位系統下載 go1.8.3.linux-386.tar.gz,64 位系統下載 go1.8.3.linux-amd64.tar.gz,
|
||||
|
||||
假定你想要安裝 Go 的目錄為 `$GO_INSTALL_DIR`,後面替換為相應的目錄路徑。
|
||||
|
||||
@@ -97,7 +97,7 @@ Linux 系統使用者可透過在 Terminal 中執行命令`arch`(即`uname -m`)
|
||||
|
||||
然後執行`go`
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.2 Linux 系統下安裝成功之後執行 go 顯示的資訊
|
||||
|
||||
@@ -105,7 +105,7 @@ Linux 系統使用者可透過在 Terminal 中執行命令`arch`(即`uname -m`)
|
||||
|
||||
### Windows 安裝 ###
|
||||
|
||||
訪問[Golang 下載頁][downlink],32 位請選擇名稱中包含 windows-386 的 msi 安裝套件,64 位請選擇名稱中包含 windows-amd64 的。下載好後執行,不要修改預設安裝目錄 C:\Go\,若安裝到其他位置會導致不能執行自己所編寫的 Go 程式碼。安裝完成後預設會在環境變數 Path 後新增 Go 安裝目錄下的 bin 目錄 `C:\Go\bin\`,並新增環境變數 GOROOT,值為 Go 安裝根目錄 `C:\Go\` 。
|
||||
訪問[Golang 下載頁](https://golang.org/dl/),32 位請選擇名稱中包含 windows-386 的 msi 安裝套件,64 位請選擇名稱中包含 windows-amd64 的。下載好後執行,不要修改預設安裝目錄 C:\Go\,若安裝到其他位置會導致不能執行自己所編寫的 Go 程式碼。安裝完成後預設會在環境變數 Path 後新增 Go 安裝目錄下的 bin 目錄 `C:\Go\bin\`,並新增環境變數 GOROOT,值為 Go 安裝根目錄 `C:\Go\` 。
|
||||
|
||||
**驗證是否安裝成功**
|
||||
|
||||
@@ -185,7 +185,6 @@ homebrew 是 Mac 系統下面目前使用最多的管理軟體的工具,目前
|
||||
2.安裝 go
|
||||
|
||||
```sh
|
||||
|
||||
brew update && brew upgrade
|
||||
brew install go
|
||||
brew install git
|
||||
@@ -196,5 +195,3 @@ brew install mercurial //可選安裝
|
||||
* [目錄](<preface.md>)
|
||||
* 上一節: [Go 環境配置](<01.0.md>)
|
||||
* 下一節: [GOPATH 與工作空間](<01.2.md>)
|
||||
|
||||
[downlink]:http://golang.org/dl/ "Go 安裝套件下載"
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
```sh
|
||||
export GOPATH=/home/apple/mygo
|
||||
```
|
||||
為了方便,應該新建以上資料夾,並且上一行加入到 `.bashrc` 或者 `.zshrc` 或者自己的 `sh` 的配置檔案中。
|
||||
為了方便,應該建立以上資料夾,並且上一行加入到 `.bashrc` 或者 `.zshrc` 或者自己的 `sh` 的配置檔案中。
|
||||
|
||||
Windows 設定如下,新建一個環境變數名稱叫做 GOPATH:
|
||||
Windows 設定如下,建立一個環境變數名稱叫做 GOPATH:
|
||||
```sh
|
||||
GOPATH=c:\mygo
|
||||
```
|
||||
@@ -36,7 +36,7 @@ GOPATH 允許多個目錄,當有多個目錄時,請注意分隔符,多個
|
||||
GOPATH 下的 src 目錄就是接下來開發程式的主要目錄,所有的原始碼都是放在這個目錄下面,那麼一般我們的做法就是一個目錄一個專案,例如: $GOPATH/src/mymath 表示 mymath 這個套件或者可執行應用,這個根據 package 是 main 還是其他來決定,main 的話就是可執行應用,其他的話就是套件,這個會在後續詳細介紹 package。
|
||||
|
||||
|
||||
所以當新建應用或者一個程式碼套件時都是在 src 目錄下新建一個資料夾,資料夾名稱一般是程式碼套件名稱,當然也允許多級目錄,例如在 src 下面新建了目錄$GOPATH/src/github.com/astaxie/beedb 那麼這個套件路徑就是"github.com/astaxie/beedb",套件名稱是最後一個目錄 beedb
|
||||
所以當建立應用或者一個程式碼套件時都是在 src 目錄下建立一個資料夾,資料夾名稱一般是程式碼套件名稱,當然也允許多階層目錄,例如在 src 下面建立了目錄$GOPATH/src/github.com/astaxie/beedb 那麼這個套件路徑就是"github.com/astaxie/beedb",套件名稱是最後一個目錄 beedb
|
||||
|
||||
|
||||
下面我就以 mymath 為例來講述如何編寫套件,執行如下程式碼
|
||||
@@ -45,7 +45,7 @@ cd $GOPATH/src
|
||||
mkdir mymath
|
||||
```
|
||||
|
||||
新建檔案 sqrt.go,內容如下
|
||||
建立檔案 sqrt.go,內容如下
|
||||
```go
|
||||
// $GOPATH/src/mymath/sqrt.go 原始碼如下:
|
||||
package mymath
|
||||
@@ -58,7 +58,7 @@ func Sqrt(x float64) float64 {
|
||||
return z
|
||||
}
|
||||
```
|
||||
這樣我的套件目錄和程式碼已經新建完畢,注意:一般建議 package 的名稱和目錄名保持一致
|
||||
這樣我的套件目錄和程式碼已經建立完畢,注意:一般建議 package 的名稱和目錄名保持一致
|
||||
|
||||
## 編譯應用
|
||||
上面我們已經建立了自己的套件,如何進行編譯安裝呢?有兩種方式可以進行安裝
|
||||
@@ -75,9 +75,9 @@ mymath.a
|
||||
```
|
||||
這個.a 檔案是套件,那麼我們如何進行呼叫呢?
|
||||
|
||||
接下來我們新建一個應用程式來呼叫這個套件
|
||||
接下來我們建立一個應用程式來呼叫這個套件
|
||||
|
||||
新建套件 mathapp
|
||||
建立套件 mathapp
|
||||
|
||||
```sh
|
||||
cd $GOPATH/src
|
||||
@@ -100,7 +100,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
可以看到這個的 package 是`main`,import 裡面呼叫的套件是`mymath`,這個就是相對於`$GOPATH/src`的路徑,如果是多級目錄,就在 import 裡面引入多級目錄,如果你有多個 GOPATH,也是一樣,Go 會自動在多個`$GOPATH/src`中尋找。
|
||||
可以看到這個的 package 是`main`,import 裡面呼叫的套件是`mymath`,這個就是相對於`$GOPATH/src`的路徑,如果是多階層目錄,就在 import 裡面引入多階層目錄,如果你有多個 GOPATH,也是一樣,Go 會自動在多個`$GOPATH/src`中尋找。
|
||||
|
||||
如何編譯程式呢?進入該應用目錄,然後執行`go build`,那麼在該目錄下面會產生一個 mathapp 的可執行檔案
|
||||
```sh
|
||||
@@ -130,7 +130,7 @@ mathapp
|
||||
|
||||
go get github.com/astaxie/beedb
|
||||
|
||||
>go get -u 引數可以自動更新套件,而且當 go get 的時候會自動取得該套件依賴的其他第三方套件
|
||||
>go get -u 參數可以自動更新套件,而且當 go get 的時候會自動取得該套件依賴的其他第三方套件
|
||||
|
||||
|
||||
透過這個命令可以取得相應的原始碼,對應的開源平臺採用不同的原始碼控制工具,例如 github 採用 git、googlecode 採用 hg,所以要想取得這些原始碼,必須先安裝相應的原始碼控制工具
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## Go 命令
|
||||
|
||||
Go 語言自帶有一套完整的命令操作工具,你可以透過在命令列中執行 `go` 來檢視它們:
|
||||
Go 語言自帶有一套完整的命令列工具,你可以透過在命令列中執行 `go` 來檢視它們:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.3 Go 命令顯示詳細的資訊
|
||||
|
||||
這些命令對於我們平時編寫的程式碼非常有用,接下來就讓我們瞭解一些常用的命令。
|
||||
這些命令對於我們平時編寫的程式碼非常有用,接下來就讓我們了解一些常用的命令。
|
||||
|
||||
## go build
|
||||
|
||||
@@ -35,24 +35,24 @@
|
||||
|
||||
`go build`的時候會選擇性地編譯以系統名結尾的檔案(Linux、Darwin、Windows、Freebsd)。例如 Linux 系統下面編譯只會選擇 array_linux.go 檔案,其它系統命名字尾檔案全部忽略。
|
||||
|
||||
引數的介紹
|
||||
參數的介紹
|
||||
|
||||
- `-o` 指定輸出的檔名,可以帶上路徑,例如 `go build -o a/b/c`
|
||||
- `-i` 安裝相應的套件,編譯+`go install`
|
||||
- `-a` 更新全部已經是最新的套件的,但是對標準套件不適用
|
||||
- `-n` 把需要執行的編譯命令打印出來,但是不執行,這樣就可以很容易的知道底層是如何執行的
|
||||
- `-n` 把需要執行的編譯命令顯示出來,但是不執行,這樣就可以很容易的知道底層是如何執行的
|
||||
- `-p n` 指定可以並行可執行的編譯數目,預設是 CPU 數目
|
||||
- `-race` 開啟編譯的時候自動檢測資料競爭的情況,目前只支援 64 位的機器
|
||||
- `-v` 打印出來我們正在編譯的套件名
|
||||
- `-work` 打印出來編譯時候的臨時資料夾名稱,並且如果已經存在的話就不要刪除
|
||||
- `-x` 打印出來執行的命令,其實就是和`-n`的結果類似,只是這個會執行
|
||||
- `-ccflags 'arg list'` 傳遞引數給 5c, 6c, 8c 呼叫
|
||||
- `-v` 顯示出來我們正在編譯的套件名
|
||||
- `-work` 顯示出來編譯時候的臨時資料夾名稱,並且如果已經存在的話就不要刪除
|
||||
- `-x` 顯示出來執行的命令,其實就是和`-n`的結果類似,只是這個會執行
|
||||
- `-ccflags 'arg list'` 傳遞參數給 5c, 6c, 8c 呼叫
|
||||
- `-compiler name` 指定相應的編譯器,gccgo 還是 gc
|
||||
|
||||
- `-gccgoflags 'arg list'` 傳遞引數給 gccgo 編譯連線呼叫
|
||||
- `-gcflags 'arg list'` 傳遞引數給 5g, 6g, 8g 呼叫
|
||||
- `-gccgoflags 'arg list'` 傳遞參數給 gccgo 編譯連線呼叫
|
||||
- `-gcflags 'arg list'` 傳遞參數給 5g, 6g, 8g 呼叫
|
||||
- `-installsuffix suffix` 為了和預設的安裝套件區別開來,採用這個字首來重新安裝那些依賴的套件,`-race`的時候預設已經是`-installsuffix race`,大家可以透過`-n`命令來驗證
|
||||
- `-ldflags 'flag list'` 傳遞引數給 5l, 6l, 8l 呼叫
|
||||
- `-ldflags 'flag list'` 傳遞參數給 5l, 6l, 8l 呼叫
|
||||
- `-tags 'tag list'` 設定在編譯的時候可以適配的那些 tag,詳細的 tag 限制參考裡面的 [Build Constraints](http://golang.org/pkg/go/build/)
|
||||
|
||||
## go clean
|
||||
@@ -71,30 +71,30 @@
|
||||
MAINFILE(.exe) 由 go build MAINFILE.go 產生
|
||||
*.so 由 SWIG 產生
|
||||
|
||||
我一般都是利用這個命令清除編譯檔案,然後 github 遞交原始碼,在本機測試的時候這些編譯檔案都是和系統相關的,但是對於原始碼管理來說沒必要。
|
||||
我一般都是利用這個命令清除編譯檔案,然後 github 提交原始碼,在本機測試的時候這些編譯檔案都是和系統相關的,但是對於原始碼管理來說沒必要。
|
||||
|
||||
$ go clean -i -n
|
||||
cd /Users/astaxie/develop/gopath/src/mathapp
|
||||
rm -f mathapp mathapp.exe mathapp.test mathapp.test.exe app app.exe
|
||||
rm -f /Users/astaxie/develop/gopath/bin/mathapp
|
||||
|
||||
引數介紹
|
||||
參數介紹
|
||||
|
||||
- `-i` 清除關聯的安裝的套件和可執行檔案,也就是透過 go install 安裝的檔案
|
||||
- `-n` 把需要執行的清除命令打印出來,但是不執行,這樣就可以很容易的知道底層是如何執行的
|
||||
- `-n` 把需要執行的清除命令顯示出來,但是不執行,這樣就可以很容易的知道底層是如何執行的
|
||||
- `-r` 迴圈的清除在 import 中引入的套件
|
||||
|
||||
- `-x` 打印出來執行的詳細命令,其實就是`-n`列印的執行版本
|
||||
- `-x` 顯示出來執行的詳細命令,其實就是`-n`列印的執行版本
|
||||
|
||||
## go fmt
|
||||
|
||||
有過 C/C++經驗的讀者會知道,一些人經常為程式碼採取 K&R 風格還是 ANSI 風格而爭論不休。在 go 中,程式碼則有標準的風格。由於之前已經有的一些習慣或其它的原因我們常將程式碼寫成 ANSI 風格或者其它更合適自己的格式,這將為人們在閱讀別人的程式碼時新增不必要的負擔,所以 go 強制了程式碼格式(比如左大括號必須放在行尾),不按照此格式的程式碼將不能編譯透過,為了減少浪費在排版上的時間,go 工具集中提供了一個`go fmt`命令 它可以幫你格式化你寫好的程式碼檔案,使你寫程式碼的時候不需要關心格式,你只需要在寫完之後執行`go fmt <檔名>.go`,你的程式碼就被修改成了標準格式,但是我平常很少用到這個命令,因為開發工具裡面一般都帶了儲存時候自動格式化功能,這個功能其實在底層就是呼叫了`go fmt`。接下來的一節我將講述兩個工具,這兩個工具都自帶了儲存檔案時自動化`go fmt`功能。
|
||||
|
||||
使用 go fmt 命令,其實是呼叫了 gofmt,而且需要引數-w,否則格式化結果不會寫入檔案。gofmt -w -l src,可以格式化整個專案。
|
||||
使用 go fmt 命令,其實是呼叫了 gofmt,而且需要參數-w,否則格式化結果不會寫入檔案。gofmt -w -l src,可以格式化整個專案。
|
||||
|
||||
所以 go fmt 是 gofmt 的上層一個套件裝的命令,我們想要更多的個性化的格式化可以參考 [gofmt](http://golang.org/cmd/gofmt/)
|
||||
所以 go fmt 是 gofmt 的上層一個封裝命令,如果想要更多的自訂格式化,可以參考 [gofmt](http://golang.org/cmd/gofmt/)
|
||||
|
||||
gofmt 的引數介紹
|
||||
gofmt 的參數介紹
|
||||
|
||||
- `-l` 顯示那些需要格式化的檔案
|
||||
- `-w` 把改寫後的內容直接寫入到檔案中,而不是作為結果列印到標準輸出。
|
||||
@@ -116,10 +116,10 @@ gofmt 的引數介紹
|
||||
|
||||
所以為了`go get` 能正常工作,你必須確保安裝了合適的原始碼管理工具,並同時把這些命令加入你的 PATH 中。其實`go get`支援自訂域名的功能,具體參見`go help remote`。
|
||||
|
||||
引數介紹:
|
||||
參數介紹:
|
||||
|
||||
- `-d` 只下載不安裝
|
||||
- `-f` 只有在你包含了`-u`引數的時候才有效,不讓`-u`去驗證 import 中的每一個都已經取得了,這對於本地 fork 的套件特別有用
|
||||
- `-f` 只有在你包含了`-u`參數的時候才有效,不讓`-u`去驗證 import 中的每一個都已經取得了,這對於本地 fork 的套件特別有用
|
||||
- `-fix` 在取得原始碼之後先執行 fix,然後再去做其他的事情
|
||||
- `-t` 同時也下載需要為執行測試所需要的套件
|
||||
|
||||
@@ -130,7 +130,7 @@ gofmt 的引數介紹
|
||||
|
||||
這個命令在內部實際上分成了兩步操作:第一步是產生結果檔案(可執行檔案或者.a 套件),第二步會把編譯好的結果移到`$GOPATH/pkg`或者`$GOPATH/bin`。
|
||||
|
||||
引數支援`go build`的編譯引數。大家只要記住一個引數`-v`就好了,這個隨時隨地的可以檢視底層的執行資訊。
|
||||
參數支援`go build`的編譯參數。大家只要記住一個參數`-v`就好了,這個隨時隨地的可以檢視底層的執行資訊。
|
||||
|
||||
## go test
|
||||
|
||||
@@ -141,9 +141,9 @@ gofmt 的引數介紹
|
||||
ok compress/gzip 0.033s
|
||||
...
|
||||
|
||||
預設的情況下,不需要任何的引數,它會自動把你原始碼套件下面所有 test 檔案測試完畢,當然你也可以帶上引數,詳情請參考`go help testflag`
|
||||
預設的情況下,不需要任何的參數,它會自動把你原始碼套件下面所有 test 檔案測試完畢,當然你也可以帶上參數,詳情請參考`go help testflag`
|
||||
|
||||
這裡我介紹幾個我們常用的引數:
|
||||
這裡我介紹幾個我們常用的參數:
|
||||
|
||||
- `-bench regexp` 執行相應的 benchmarks,例如 `-bench=.`
|
||||
- `-cover` 開啟測試覆蓋率
|
||||
@@ -155,10 +155,11 @@ gofmt 的引數介紹
|
||||
|
||||
|
||||
- `go tool fix .` 用來修復以前老版本的程式碼到新版本,例如 go1 之前老版本的程式碼轉化到 go1,例如 API 的變化
|
||||
- `go tool vet directory|files` 用來分析當前目錄的程式碼是否都是正確的程式碼,例如是不是呼叫 fmt.Printf 裡面的引數不正確,例如函式裡面提前 return 瞭然後出現了無用程式碼之類別的。
|
||||
- `go tool vet directory|files` 用來分析當前目錄的程式碼是否都是正確的程式碼,例如是不是呼叫 fmt.Printf 裡面的參數不正確,例如函式裡面提前 return 了然後出現了無用程式碼之類別的。
|
||||
|
||||
## go generate
|
||||
這個命令是從 Go1.4 開始才設計的,用於在編譯前自動化產生某類別程式碼。`go generate`和`go build`是完全不一樣的命令,透過分析原始碼中特殊的註釋,然後執行相應的命令。這些命令都是很明確的,沒有任何的依賴在裡面。而且大家在用這個之前心裡面一定要有一個理念,這個`go generate`是給你用的,不是給使用你這個套件的人用的,是方便你來產生一些程式碼的。
|
||||
|
||||
這個命令是從 Go1.4 開始才設計的,用於在編譯前自動化產生某類別程式碼。`go generate`和`go build`是完全不一樣的命令,透過分析原始碼中特殊的註釋,然後執行相應的命令。這些命令都是很明確的,沒有任何的依賴在裡面。而且大家在用這個之前心裡面一定要有一個觀念,這個 `go generate` 是給你用的,不是給使用你這個套件的人用的,主要是方便你自動產生一些程式碼。
|
||||
|
||||
這裡我們來舉一個簡單的例子,例如我們經常會使用 `yacc` 來產生程式碼,那麼我們常用這樣的命令:
|
||||
|
||||
@@ -201,7 +202,7 @@ gofmt 的引數介紹
|
||||
|
||||
go run 編譯並執行 Go 程式
|
||||
|
||||
以上這些工具還有很多引數沒有一一介紹,使用者可以使用`go help 命令 ` 取得更詳細的幫助資訊。
|
||||
以上這些工具還有很多參數沒有一一介紹,使用者可以使用`go help 命令 ` 取得更詳細的幫助資訊。
|
||||
|
||||
|
||||
## links
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
LiteIDE 是一款專門為 Go 語言開發的跨平臺輕量級整合開發環境(IDE),由 visualfc 編寫。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.4 LiteIDE 主介面
|
||||
|
||||
@@ -97,14 +97,14 @@
|
||||
|
||||
- 自動化提示程式碼,如下圖所示
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.5 sublime 自動化提示介面
|
||||
|
||||
- 儲存的時候自動格式化程式碼,讓您編寫的程式碼更加美觀,符合 Go 的標準。
|
||||
- 支援專案管理
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.6 sublime 專案管理介面
|
||||
|
||||
@@ -131,7 +131,7 @@ import urllib2,os;pf='Package Control.sublime-package';ipp=sublime.installed_pa
|
||||
|
||||
這個時候重啟一下 Sublime,可以發現在在選單欄多了一個如下的節目,說明 Package Control 已經安裝成功了。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.7 sublime 套件管理
|
||||
|
||||
@@ -140,7 +140,7 @@ import urllib2,os;pf='Package Control.sublime-package';ipp=sublime.installed_pa
|
||||
|
||||
這個時候看左下角顯示正在讀取套件資料,完成之後出現如下介面
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.8 sublime 安裝外掛介面
|
||||
|
||||
@@ -181,7 +181,7 @@ go get -u -v github.com/cweill/gotests/...
|
||||
|
||||
## Visual Studio Code
|
||||
|
||||
vscode 是微軟基於 Electron 和 web 技術建構的開源編輯器, 是一款很強大的編輯器。開源地址:https://github.com/Microsoft/vscode
|
||||
VSCode 是微軟基於 Electron 和 web 技術建構的開源編輯器, 是一款很強大的編輯器。開源地址:https://github.com/Microsoft/VSCode
|
||||
|
||||
1、安裝 Visual Studio Code 最新版
|
||||
|
||||
@@ -196,7 +196,7 @@ vscode 是微軟基於 Electron 和 web 技術建構的開源編輯器, 是一
|
||||
|
||||
建議把自動儲存功能開啟。開啟方法為:選擇選單 File,點選 Auto save。
|
||||
|
||||
vscode 程式碼設定可用於 Go 擴充套件。這些都可以在使用者的喜好來設定或工作區設定(.vscode/settings.json)。
|
||||
VSCode 程式碼設定可用於 Go 擴充套件。這些都可以在使用者的喜好來設定或工作區設定(.VSCode/settings.json)。
|
||||
|
||||
開啟首選項-使用者設定 settings.json:
|
||||
|
||||
@@ -236,7 +236,7 @@ go get -u -v golang.org/x/tools/cmd/guru
|
||||
go get -u -v github.com/cweill/gotests/...
|
||||
```
|
||||
|
||||
vscode 還有一項很強大的功能就是斷點除錯,結合 [delve](https://github.com/derekparker/delve) 可以很好的進行 Go 程式碼除錯
|
||||
VSCode 還有一項很強大的功能就是斷點除錯,結合 [delve](https://github.com/derekparker/delve) 可以很好的進行 Go 程式碼除錯
|
||||
|
||||
```Go
|
||||
|
||||
@@ -364,7 +364,7 @@ Plugin 'Valloric/YouCompleteMe'
|
||||
在 Vim 內執行: PluginInstall
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.9 VIM 編輯器自動化提示 Go 介面
|
||||
|
||||
@@ -397,9 +397,9 @@ Plugin 'Valloric/YouCompleteMe'
|
||||
propose-builtins true
|
||||
lib-path "/home/border/gocode/pkg/linux_amd64"
|
||||
|
||||
>gocode set 裡面的兩個引數的含意說明:
|
||||
>gocode set 裡面的兩個參數的含意說明:
|
||||
>
|
||||
>propose-builtins:是否自動提示 Go 的內建函式、型別和常量,預設為 false,不提示。
|
||||
>propose-builtins:是否自動提示 Go 的內建函式、型別和常數,預設為 false,不提示。
|
||||
>
|
||||
>lib-path:預設情況下,gocode 只會搜尋**$GOPATH/pkg/$GOOS_$GOARCH** 和 **$GOROOT/pkg/$GOOS_$GOARCH**目錄下的套件,當然這個設定就是可以設定我們額外的 lib 能訪問的路徑
|
||||
|
||||
@@ -411,7 +411,7 @@ Plugin 'Valloric/YouCompleteMe'
|
||||
## Emacs
|
||||
Emacs 傳說中的神器,她不僅僅是一個編輯器,它是一個整合環境,或可稱它為整合開發環境,這些功能如讓使用者置身於全功能的作業系統中。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.10 Emacs 編輯 Go 主介面
|
||||
|
||||
@@ -548,7 +548,7 @@ Emacs 傳說中的神器,她不僅僅是一個編輯器,它是一個整合
|
||||
## Eclipse
|
||||
Eclipse 也是非常常用的開發利器,以下介紹如何使用 Eclipse 來編寫 Go 程式。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.11 Eclipse 編輯 Go 的主介面
|
||||
|
||||
@@ -580,34 +580,34 @@ Eclipse 也是非常常用的開發利器,以下介紹如何使用 Eclipse 來
|
||||
|
||||
(1).配置 Go 的編譯器
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.12 設定 Go 的一些基礎資訊
|
||||
|
||||
|
||||
(2).配置 Gocode(可選,程式碼自動完成),設定 Gocode 路徑為之前產生的 gocode.exe 檔案
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.13 設定 gocode 資訊
|
||||
|
||||
(3).配置 GDB(可選,做除錯用),設定 GDB 路徑為 MingW 安裝目錄下的 gdb.exe 檔案
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.14 設定 GDB 資訊
|
||||
|
||||
6. 測試是否成功
|
||||
|
||||
新建一個 go 工程,再建立一個 hello.go。如下圖:
|
||||
建立一個 go 工程,再建立一個 hello.go。如下圖:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.15 新建專案編輯檔案
|
||||
圖 1.15 建立專案編輯檔案
|
||||
|
||||
除錯如下(要在 console 中用輸入命令來除錯):
|
||||
|
||||

|
||||

|
||||
|
||||
圖 1.16 除錯 Go 程式
|
||||
|
||||
@@ -616,21 +616,21 @@ Eclipse 也是非常常用的開發利器,以下介紹如何使用 Eclipse 來
|
||||
|
||||
1. 先下載 idea,idea 支援多平臺:win,mac,linux,如果有錢就買個正式版,如果不行就使用社群免費版,對於只是開發 Go 語言來說免費版足夠用了
|
||||
|
||||

|
||||

|
||||
|
||||
2. 安裝 Go 外掛,點選選單 File 中的 Setting,找到 Plugins,點選,Broswer repo 按鈕。國內的使用者可能會報錯,自己解決哈。
|
||||
|
||||

|
||||

|
||||
|
||||
3. 這時候會看見很多外掛,搜尋找到 Golang,雙擊,download and install。等到 golang 那一行後面出現 Downloaded 標誌後,點 OK。
|
||||
|
||||

|
||||

|
||||
|
||||
然後點 Apply .這時候 IDE 會要求你重啟。
|
||||
|
||||
4. 重啟完畢後,建立新專案會發現已經可以建立 golang 專案了:
|
||||
|
||||

|
||||

|
||||
|
||||
下一步,會要求你輸入 go sdk 的位置,一般都安裝在 C:\Go,linux 和 mac 根據自己的安裝目錄設定,選中目錄確定,就可以了。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 1.5 總結
|
||||
|
||||
這一章中我們主要介紹瞭如何安裝 Go,Go 可以透過三種方式安裝:原始碼安裝、標準套件安裝、第三方工具安裝,安裝之後我們需要配置我們的開發環境,然後介紹瞭如何配置本地的`$GOPATH`,透過設定`$GOPATH`之後讀者就可以建立專案,接著介紹瞭如何來進行專案編譯、應用安裝等問題,這些需要用到很多 Go 命令,所以接著就介紹了一些 Go 的常用命令工具,包括編譯、安裝、格式化、測試等命令,最後介紹了 Go 的開發工具,目前有很多 Go 的開發工具:LiteIDE、Sublime、VSCode、Atom、Goland、VIM、Emacs、Eclipse、Idea 等工具,讀者可以根據自己熟悉的工具進行配置,希望能夠透過方便的工具快速的開發 Go 應用。
|
||||
這一章中我們主要介紹了如何安裝 Go,Go 可以透過三種方式安裝:原始碼安裝、標準套件安裝、第三方工具安裝,安裝之後我們需要配置我們的開發環境,然後介紹了如何配置本地的`$GOPATH`,透過設定`$GOPATH`之後讀者就可以建立專案,接著介紹了如何來進行專案編譯、應用安裝等問題,這些需要用到很多 Go 命令,所以接著就介紹了一些 Go 的常用命令工具,包括編譯、安裝、格式化、測試等命令,最後介紹了 Go 的開發工具,目前有很多 Go 的開發工具:LiteIDE、Sublime、VSCode、Atom、Goland、VIM、Emacs、Eclipse、Idea 等工具,讀者可以根據自己熟悉的工具進行配置,希望能夠透過方便的工具快速的開發 Go 應用。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -11,7 +11,7 @@ Go 是一門類似 C 的編譯型語言,但是它的編譯速度非常快。
|
||||
在接下來的這一章中,我將帶領你去學習這門語言的基礎。透過每一小節的介紹,你將發現,Go 的世界是那麼地簡潔,設計是如此地美妙,編寫 Go 將會是一件愉快的事情。等回過頭來,你就會發現這二十五個關鍵字是多麼地親切。
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -26,7 +26,7 @@ func main() {
|
||||
|
||||
`package <pkgName>`(在我們的例子中是`package main`)這一行告訴我們當前檔案屬於哪個套件,而套件名 `main` 則告訴我們它是一個可獨立執行的套件,它在編譯後會產生可執行檔案。除了 `main` 套件之外,其它的套件最後都會產生`*.a`檔案(也就是套件檔案)並放置在`$GOPATH/pkg/$GOOS_$GOARCH`中(以 Mac 為例就是`$GOPATH/pkg/darwin_amd64`)。
|
||||
|
||||
>每一個可獨立執行的 Go 程式,必定包含一個`package main`,在這個 `main` 套件中必定包含一個入口函式`main`,而這個函式既沒有引數,也沒有返回值。
|
||||
>每一個可獨立執行的 Go 程式,必定包含一個`package main`,在這個 `main` 套件中必定包含一個入口函式`main`,而這個函式既沒有參數,也沒有回傳值。
|
||||
|
||||
為了列印`Hello, world...`,我們呼叫了一個函式`Printf`,這個函式來自於 `fmt` 套件,所以我們在第三行中匯入了系統級別的 `fmt` 套件:`import "fmt"`。
|
||||
|
||||
@@ -34,7 +34,7 @@ func main() {
|
||||
|
||||
在第五行中,我們透過關鍵字 `func` 定義了一個 `main` 函式,函式體被放在`{}`(大括號)中,就像我們平時寫 C、C++或 Java 時一樣。
|
||||
|
||||
大家可以看到 `main` 函式是沒有任何的引數的,我們接下來就學習如何編寫帶引數的、返回 0 個或多個值的函式。
|
||||
大家可以看到 `main` 函式是沒有任何的參數的,我們接下來就學習如何編寫帶參數的、回傳 0 個或多個值的函式。
|
||||
|
||||
第六行,我們呼叫了 `fmt` 套件裡面定義的函式`Printf`。大家可以看到,這個函式是透過`<pkgName>.<funcName>`的方式呼叫的,這一點和 Python 十分相似。
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 2.2 Go 基礎
|
||||
|
||||
這小節我們將要介紹如何定義變數、常量、Go 內建型別以及 Go 程式設計中的一些技巧。
|
||||
這小節我們將要介紹如何定義變數、常數、Go 內建型別以及 Go 程式設計中的一些技巧。
|
||||
|
||||
## 定義變數
|
||||
|
||||
@@ -59,7 +59,7 @@ vname1, vname2, vname3 := v1, v2, v3
|
||||
|
||||
_, b := 34, 35
|
||||
|
||||
Go 對於已宣告但未使用的變數會在編譯階段報錯,比如下面的程式碼就會產生一個錯誤:聲明瞭 `i` 但未使用。
|
||||
Go 對於已宣告但未使用的變數會在編譯階段報錯,比如下面的程式碼就會產生一個錯誤:宣告了 `i` 但未使用。
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -68,18 +68,18 @@ func main() {
|
||||
var i int
|
||||
}
|
||||
```
|
||||
## 常量
|
||||
## 常數
|
||||
|
||||
所謂常量,也就是在程式編譯階段就確定下來的值,而程式在執行時無法改變該值。在 Go 程式中,常量可定義為數值、布林值或字串等型別。
|
||||
所謂常數,也就是在程式編譯階段就確定下來的值,而程式在執行時無法改變該值。在 Go 程式中,常數可定義為數值、布林值或字串等型別。
|
||||
|
||||
它的語法如下:
|
||||
```Go
|
||||
|
||||
const constantName = value
|
||||
//如果需要,也可以明確指定常量的型別:
|
||||
//如果需要,也可以明確指定常數的型別:
|
||||
const Pi float32 = 3.1415926
|
||||
```
|
||||
下面是一些常量宣告的例子:
|
||||
下面是一些常數宣告的例子:
|
||||
```Go
|
||||
|
||||
const Pi = 3.1415926
|
||||
@@ -87,7 +87,7 @@ const i = 10000
|
||||
const MaxThread = 10
|
||||
const prefix = "astaxie_"
|
||||
```
|
||||
Go 常量和一般程式語言不同的是,可以指定相當多的小數位數(例如 200 位),
|
||||
Go 常數和一般程式語言不同的是,可以指定相當多的小數位數(例如 200 位),
|
||||
若指定給 float32 自動縮短為 32bit,指定給 float64 自動縮短為 64bit,詳情參考 [ 連結](http://golang.org/ref/spec#Constants)
|
||||
|
||||
## 內建基礎型別
|
||||
@@ -140,7 +140,7 @@ fmt.Printf("Value is: %v", c)
|
||||
|
||||
//範例程式碼
|
||||
var frenchHello string // 宣告變數為字串的一般方法
|
||||
var emptyString string = "" // 聲明瞭一個字串變數,初始化為空字串
|
||||
var emptyString string = "" // 宣告了一個字串變數,初始化為空字串
|
||||
func test() {
|
||||
no, yes, maybe := "no", "yes", "maybe" // 簡短宣告,同時宣告多個變數
|
||||
japaneseHello := "Konichiwa" // 同上
|
||||
@@ -176,7 +176,7 @@ fmt.Printf("%s\n", a)
|
||||
```Go
|
||||
|
||||
s := "hello"
|
||||
s = "c" + s[1:] // 字串雖不能更改,但可進行切片操作
|
||||
s = "c" + s[1:] // 字串雖不能更改,但可進行切片(slice)操作
|
||||
fmt.Printf("%s\n", s)
|
||||
```
|
||||
如果要宣告一個多行的字串怎麼辦?可以透過`` ` ``來宣告:
|
||||
@@ -202,7 +202,7 @@ if err != nil {
|
||||
|
||||
下面這張圖來源於[Russ Cox Blog](http://research.swtch.com/)中一篇介紹 [Go 資料結構](http://research.swtch.com/godata)的文章,大家可以看到這些基礎型別底層都是分配了一塊記憶體,然後儲存了相應的值。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 2.1 Go 資料格式的儲存
|
||||
|
||||
@@ -210,7 +210,7 @@ if err != nil {
|
||||
|
||||
### 分組宣告
|
||||
|
||||
在 Go 語言中,同時宣告多個常量、變數,或者匯入多個套件時,可採用分組的方式進行宣告。
|
||||
在 Go 語言中,同時宣告多個常數、變數,或者匯入多個套件時,可採用分組的方式進行宣告。
|
||||
|
||||
例如下面的程式碼:
|
||||
```Go
|
||||
@@ -248,7 +248,7 @@ var(
|
||||
```
|
||||
### iota 列舉
|
||||
|
||||
Go 裡面有一個關鍵字`iota`,這個關鍵字用來宣告 `enum` 的時候採用,它預設開始值是 0,const 中每增加一行加 1:
|
||||
Go 裡面有一個關鍵字 `iota`,這個關鍵字用來宣告 `enum` 的時候採用,它預設開始值是 0,const 中每增加一行加 1:
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -261,7 +261,7 @@ const (
|
||||
x = iota // x == 0
|
||||
y = iota // y == 1
|
||||
z = iota // z == 2
|
||||
w // 常量宣告省略值時,預設和之前一個值的字面相同。這裡隱式地說 w = iota,因此 w == 3。其實上面 y 和 z 可同樣不用"= iota"
|
||||
w // 常數宣告省略值時,預設和之前一個值的字面相同。這裡隱含的說 w = iota,因此 w == 3。其實上面 y 和 z 可同樣不用"= iota"
|
||||
)
|
||||
|
||||
const v = iota // 每遇到一個 const 關鍵字,iota 就會重置,此時 v == 0
|
||||
@@ -282,7 +282,7 @@ func main() {
|
||||
fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v)
|
||||
}
|
||||
```
|
||||
>除非被顯式設定為其它值或`iota`,每個 `const` 分組的第一個常量被預設設定為它的 0 值,第二及後續的常量被預設設定為它前面那個常量的值,如果前面那個常量的值是`iota`,則它也被設定為`iota`。
|
||||
>除非被明確的設定為其它值或`iota`,每個 `const` 分組的第一個常數被預設設定為它的 0 值,第二及後續的常數被預設設定為它前面那個常數的值,如果前面那個常數的值是`iota`,則它也被設定為`iota`。
|
||||
|
||||
### Go 程式設計的一些規則
|
||||
Go 之所以會那麼簡潔,是因為它有一些預設的行為:
|
||||
@@ -300,27 +300,27 @@ var arr [n]type
|
||||
在 `[n]type` 中,`n`表示陣列的長度,`type`表示儲存元素的型別。對陣列的操作和其它語言類似,都是透過 `[]` 來進行讀取或賦值:
|
||||
```Go
|
||||
|
||||
var arr [10]int // 聲明瞭一個 int 型別的陣列
|
||||
var arr [10]int // 宣告了一個 int 型別的陣列
|
||||
arr[0] = 42 // 陣列下標是從 0 開始的
|
||||
arr[1] = 13 // 賦值操作
|
||||
fmt.Printf("The first element is %d\n", arr[0]) // 取得資料,返回 42
|
||||
fmt.Printf("The last element is %d\n", arr[9]) //返回未賦值的最後一個元素,預設返回 0
|
||||
fmt.Printf("The first element is %d\n", arr[0]) // 取得資料,回傳 42
|
||||
fmt.Printf("The last element is %d\n", arr[9]) //回傳未賦值的最後一個元素,預設回傳 0
|
||||
```
|
||||
由於長度也是陣列型別的一部分,因此 `[3]int` 與`[4]int`是不同的型別,陣列也就不能改變長度。陣列之間的賦值是值的賦值,即當把一個數組作為引數傳入函式的時候,傳入的其實是該陣列的副本,而不是它的指標。如果要使用指標,那麼就需要用到後面介紹的 `slice` 型別了。
|
||||
由於長度也是陣列型別的一部分,因此 `[3]int` 與`[4]int`是不同的型別,陣列也就不能改變長度。陣列之間的賦值是值的賦值,即當把一個陣列作為參數傳入函式的時候,傳入的其實是該陣列的副本,而不是它的指標。如果要使用指標,那麼就需要用到後面介紹的 `slice` 型別了。
|
||||
|
||||
陣列可以使用另一種 `:=` 來宣告
|
||||
```Go
|
||||
|
||||
a := [3]int{1, 2, 3} // 聲明瞭一個長度為 3 的 int 陣列
|
||||
a := [3]int{1, 2, 3} // 宣告了一個長度為 3 的 int 陣列
|
||||
|
||||
b := [10]int{1, 2, 3} // 聲明瞭一個長度為 10 的 int 陣列,其中前三個元素初始化為 1、2、3,其它預設為 0
|
||||
b := [10]int{1, 2, 3} // 宣告了一個長度為 10 的 int 陣列,其中前三個元素初始化為 1、2、3,其它預設為 0
|
||||
|
||||
c := [...]int{4, 5, 6} // 可以省略長度而採用`...`的方式,Go 會自動根據元素個數來計算長度
|
||||
```
|
||||
也許你會說,我想數組裡面的值還是陣列,能實現嗎?當然咯,Go 支援巢狀陣列,即多維陣列。比如下面的程式碼就聲明瞭一個二維陣列:
|
||||
也許你會說,我想陣列裡面的值還是陣列,能實現嗎?當然囉,Go 支援巢狀陣列,即多維陣列。比如下面的程式碼就宣告了一個二維陣列:
|
||||
```Go
|
||||
|
||||
// 聲明瞭一個二維陣列,該陣列以兩個陣列作為元素,其中每個陣列中又有 4 個 int 型別的元素
|
||||
// 宣告了一個二維陣列,該陣列以兩個陣列作為元素,其中每個陣列中又有 4 個 int 型別的元素
|
||||
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
|
||||
|
||||
// 上面的宣告可以簡化,直接忽略內部的型別
|
||||
@@ -328,7 +328,7 @@ easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
|
||||
```
|
||||
陣列的分配如下所示:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 2.2 多維陣列的對映關係
|
||||
|
||||
@@ -348,7 +348,7 @@ var fslice []int
|
||||
|
||||
slice := []byte {'a', 'b', 'c', 'd'}
|
||||
```
|
||||
`slice`可以從一個數組或一個已經存在的 `slice` 中再次宣告。`slice`透過 `array[i:j]` 來取得,其中 `i` 是陣列的開始位置,`j`是結束位置,但不包含`array[j]`,它的長度是`j-i`。
|
||||
`slice`可以從一個陣列或一個已經存在的 `slice` 中再次宣告。`slice`透過 `array[i:j]` 來取得,其中 `i` 是陣列的開始位置,`j`是結束位置,但不包含`array[j]`,它的長度是`j-i`。
|
||||
```Go
|
||||
|
||||
// 宣告一個含有 10 個元素元素型別為 byte 的陣列
|
||||
@@ -365,11 +365,11 @@ a = ar[2:5]
|
||||
b = ar[3:5]
|
||||
// b 的元素是:ar[3]和 ar[4]
|
||||
```
|
||||
>注意 `slice` 和陣列在宣告時的區別:宣告陣列時,方括號內寫明瞭陣列的長度或使用`...`自動計算長度,而宣告 `slice` 時,方括號內沒有任何字元。
|
||||
>注意 `slice` 和陣列在宣告時的區別:宣告陣列時,方括號內寫明了陣列的長度或使用`...`自動計算長度,而宣告 `slice` 時,方括號內沒有任何字元。
|
||||
|
||||
它們的資料結構如下所示
|
||||
|
||||

|
||||

|
||||
|
||||
圖 2.3 slice 和 array 的對應關係圖
|
||||
|
||||
@@ -377,12 +377,12 @@ slice 有一些簡便的操作
|
||||
|
||||
- `slice`的預設開始位置是 0,`ar[:n]`等價於`ar[0:n]`
|
||||
- `slice`的第二個序列預設是陣列的長度,`ar[n:]`等價於`ar[n:len(ar)]`
|
||||
- 如果從一個數組裡面直接取得`slice`,可以這樣`ar[:]`,因為預設第一個序列是 0,第二個是陣列的長度,即等價於`ar[0:len(ar)]`
|
||||
- 如果從一個陣列裡面直接取得`slice`,可以這樣`ar[:]`,因為預設第一個序列是 0,第二個是陣列的長度,即等價於`ar[0:len(ar)]`
|
||||
|
||||
下面這個例子展示了更多關於 `slice` 的操作:
|
||||
```Go
|
||||
|
||||
// 宣告一個數組
|
||||
// 宣告一個陣列
|
||||
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
|
||||
// 宣告兩個 slice
|
||||
var aSlice, bSlice []byte
|
||||
@@ -412,7 +412,7 @@ bSlice = aSlice[:] // bSlice 包含所有 aSlice 的元素: d,e,f,g
|
||||
```
|
||||
上面程式碼的真正儲存結構如下圖所示
|
||||
|
||||

|
||||

|
||||
|
||||
圖 2.4 slice 對應陣列的資訊
|
||||
|
||||
@@ -420,13 +420,13 @@ bSlice = aSlice[:] // bSlice 包含所有 aSlice 的元素: d,e,f,g
|
||||
|
||||
- `len` 取得 `slice` 的長度
|
||||
- `cap` 取得 `slice` 的最大容量
|
||||
- `append` 向 `slice` 裡面追加一個或者多個元素,然後返回一個和 `slice` 一樣型別的`slice`
|
||||
- `copy` 函式 `copy` 從源 `slice` 的`src`中複製元素到目標`dst`,並且返回複製的元素的個數
|
||||
- `append` 向 `slice` 裡面追加一個或者多個元素,然後回傳一個和 `slice` 一樣型別的`slice`
|
||||
- `copy` 函式 `copy` 從源 `slice` 的`src`中複製元素到目標`dst`,並且回傳複製的元素的個數
|
||||
|
||||
注:`append`函式會改變 `slice` 所參考的陣列的內容,從而影響到參考同一陣列的其它`slice`。
|
||||
但當 `slice` 中沒有剩餘空間(即`(cap-len) == 0`)時,此時將動態分配新的陣列空間。返回的 `slice` 陣列指標將指向這個空間,而原陣列的內容將保持不變;其它參考此陣列的 `slice` 則不受影響。
|
||||
但當 `slice` 中沒有剩餘空間(即`(cap-len) == 0`)時,此時將動態分配新的陣列空間。回傳的 `slice` 陣列指標將指向這個空間,而原陣列的內容將保持不變;其它參考此陣列的 `slice` 則不受影響。
|
||||
|
||||
從 Go1.2 開始 slice 支援了三個引數的 slice,之前我們一直採用這種方式在 slice 或者 array 基礎上來取得一個 slice
|
||||
從 Go1.2 開始 slice 支援了三個參數的 slice,之前我們一直採用這種方式在 slice 或者 array 基礎上來取得一個 slice
|
||||
```Go
|
||||
|
||||
var array [10]int
|
||||
@@ -438,7 +438,7 @@ slice := array[2:4]
|
||||
|
||||
上面這個的容量就是`7-2`,即 5。這樣這個產生的新的 slice 就沒辦法訪問最後的三個元素。
|
||||
|
||||
如果 slice 是這樣的形式`array[:i:j]`,即第一個引數為空,預設值就是 0。
|
||||
如果 slice 是這樣的形式`array[:i:j]`,即第一個參數為空,預設值就是 0。
|
||||
|
||||
### map
|
||||
|
||||
@@ -456,15 +456,15 @@ numbers["ten"] = 10 //賦值
|
||||
numbers["three"] = 3
|
||||
|
||||
fmt.Println("第三個數字是: ", numbers["three"]) // 讀取資料
|
||||
// 打印出來如 : 第三個數字是: 3
|
||||
// 顯示出來如 : 第三個數字是: 3
|
||||
```
|
||||
|
||||
這個 `map` 就像我們平常看到的表格一樣,左邊列是`key`,右邊列是值
|
||||
|
||||
使用 map 過程中需要注意的幾點:
|
||||
- `map`是無序的,每次打印出來的 `map` 都會不一樣,它不能透過 `index` 取得,而必須透過 `key` 取得
|
||||
- `map`是無序的,每次顯示出來的 `map` 都會不一樣,它不能透過 `index` 取得,而必須透過 `key` 取得
|
||||
- `map`的長度是不固定的,也就是和 `slice` 一樣,也是一種參考型別
|
||||
- 內建的 `len` 函式同樣適用於`map`,返回 `map` 擁有的 `key` 的數量
|
||||
- 內建的 `len` 函式同樣適用於`map`,回傳 `map` 擁有的 `key` 的數量
|
||||
- `map`的值可以很方便的修改,透過`numbers["one"]=11`可以很容易的把 key 為`one`的字典值改為`11`
|
||||
- `map`和其他基本型別不同,它不是 thread-safe,在多個 go-routine 存取時,必須使用 mutex lock 機制
|
||||
|
||||
@@ -476,7 +476,7 @@ fmt.Println("第三個數字是: ", numbers["three"]) // 讀取資料
|
||||
|
||||
// 初始化一個字典
|
||||
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
|
||||
// map 有兩個返回值,第二個返回值,如果不存在 key,那麼 ok 為 false,如果存在 ok 為 true
|
||||
// map 有兩個回傳值,第二個回傳值,如果不存在 key,那麼 ok 為 false,如果存在 ok 為 true
|
||||
csharpRating, ok := rating["C#"]
|
||||
if ok {
|
||||
fmt.Println("C# is in the map and its rating is ", csharpRating)
|
||||
@@ -500,17 +500,17 @@ m1["Hello"] = "Salut" // 現在 m["hello"]的值已經是 Salut 了
|
||||
|
||||
`make`用於內建型別(`map`、`slice` 和`channel`)的記憶體分配。`new`用於各種型別的記憶體分配。
|
||||
|
||||
內建函式 `new` 本質上說跟其它語言中的同名函式功能一樣:`new(T)`分配了零值填充的 `T` 型別的記憶體空間,並且返回其地址,即一個`*T`型別的值。用 Go 的術語說,它返回了一個指標,指向新分配的型別 `T` 的零值。有一點非常重要:
|
||||
內建函式 `new` 本質上說跟其它語言中的同名函式功能一樣:`new(T)`分配了零值填充的 `T` 型別的記憶體空間,並且回傳其地址,即一個`*T`型別的值。用 Go 的術語說,它回傳了一個指標,指向新分配的型別 `T` 的零值。有一點非常重要:
|
||||
|
||||
>`new`返回指標。
|
||||
>`new`回傳指標。
|
||||
|
||||
內建函式`make(T, args)`與`new(T)`有著不同的功能,make 只能建立`slice`、`map`和`channel`,並且返回一個有初始值(非零)的 `T` 型別,而不是`*T`。本質來講,導致這三個型別有所不同的原因是指向資料結構的參考在使用前必須被初始化。例如,一個`slice`,是一個包含指向資料(內部`array`)的指標、長度和容量的三項描述符;在這些專案被初始化之前,`slice`為`nil`。對於`slice`、`map`和 `channel` 來說,`make`初始化了內部的資料結構,填充適當的值。
|
||||
內建函式`make(T, args)`與`new(T)`有著不同的功能,make 只能建立`slice`、`map`和`channel`,並且回傳一個有初始值(非零)的 `T` 型別,而不是`*T`。本質來講,導致這三個型別有所不同的原因是指向資料結構的參考在使用前必須被初始化。例如,一個`slice`,是一個包含指向資料(內部`array`)的指標、長度和容量的三項描述符;在這些專案被初始化之前,`slice`為`nil`。對於`slice`、`map`和 `channel` 來說,`make`初始化了內部的資料結構,填充適當的值。
|
||||
|
||||
>`make`返回初始化後的(非零)值。
|
||||
>`make`回傳初始化後的(非零)值。
|
||||
|
||||
下面這個圖詳細的解釋了 `new` 和`make`之間的區別。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 2.5 make 和 new 對應底層的記憶體分配
|
||||
|
||||
|
||||
103
zh-tw/02.3.md
103
zh-tw/02.3.md
@@ -14,10 +14,10 @@ if x > 10 {
|
||||
fmt.Println("x is less than 10")
|
||||
}
|
||||
```
|
||||
Go 的 `if` 還有一個強大的地方就是條件判斷語句裡面允許宣告一個變數,這個變數的作用域只能在該條件邏輯塊內,其他地方就不起作用了,如下所示
|
||||
Go 的 `if` 還有一個強大的地方就是條件判斷語句裡面允許宣告一個變數,這個變數的作用域只能在該條件邏輯區塊內,其他地方就無法使用,如下所示
|
||||
```Go
|
||||
|
||||
// 計算取得值 x,然後根據 x 返回的大小,判斷是否大於 10。
|
||||
// 計算取得值 x,然後根據 x 回傳的大小,判斷是否大於 10。
|
||||
if x := computedValue(); x > 10 {
|
||||
fmt.Println("x is greater than 10")
|
||||
} else {
|
||||
@@ -51,7 +51,7 @@ Here: //這行的第一個詞,以冒號結束作為標籤
|
||||
goto Here //跳轉到 Here 去
|
||||
}
|
||||
```
|
||||
>標籤名是大小寫敏感的。
|
||||
>標籤名稱(label)是區分大小寫的的。
|
||||
|
||||
### for
|
||||
Go 裡面最強大的一個控制邏輯就是`for`,它既可以用來迴圈讀取資料,又可以當作 `while` 來控制邏輯,還能迭代操作。它的語法如下:
|
||||
@@ -61,7 +61,7 @@ for expression1; expression2; expression3 {
|
||||
//...
|
||||
}
|
||||
```
|
||||
`expression1`、`expression2`和 `expression3` 都是表示式,其中 `expression1` 和`expression3`是變數宣告或者函式呼叫返回值之類別的,`expression2`是用來條件判斷,`expression1`在迴圈開始之前呼叫,`expression3`在每輪迴圈結束之時呼叫。
|
||||
`expression1`、`expression2`和 `expression3` 都是表示式,其中 `expression1` 和`expression3`是變數宣告或者函式呼叫回傳值之類別的,`expression2`是用來條件判斷,`expression1`在迴圈開始之前呼叫,`expression3`在每輪迴圈結束之時呼叫。
|
||||
|
||||
一個例子比上面講那麼多更有用,那麼我們看看下面的例子吧:
|
||||
```Go
|
||||
@@ -107,8 +107,8 @@ for index := 10; index>0; index-- {
|
||||
}
|
||||
fmt.Println(index)
|
||||
}
|
||||
// break 打印出來 10、9、8、7、6
|
||||
// continue 打印出來 10、9、8、7、6、4、3、2、1
|
||||
// break 顯示出來 10、9、8、7、6
|
||||
// continue 顯示出來 10、9、8、7、6、4、3、2、1
|
||||
```
|
||||
`break`和 `continue` 還可以跟著標號,用來跳到多重迴圈中的外層迴圈
|
||||
|
||||
@@ -120,7 +120,7 @@ for k,v:=range map {
|
||||
fmt.Println("map's val:",v)
|
||||
}
|
||||
```
|
||||
由於 Go 支援 “多值返回”, 而對於“宣告而未被呼叫”的變數, 編譯器會報錯, 在這種情況下, 可以使用 `_` 來丟棄不需要的返回值
|
||||
由於 Go 支援 “多值回傳”, 而對於“宣告而未被呼叫”的變數, 編譯器會報錯, 在這種情況下, 可以使用 `_` 來丟棄不需要的回傳值
|
||||
例如
|
||||
```Go
|
||||
|
||||
@@ -144,7 +144,7 @@ default:
|
||||
other code
|
||||
}
|
||||
```
|
||||
`sExpr`和`expr1`、`expr2`、`expr3`的型別必須一致。Go 的 `switch` 非常靈活,表示式不必是常量或整數,執行的過程從上至下,直到找到匹配項;而如果 `switch` 沒有表示式,它會匹配`true`。
|
||||
`sExpr`和`expr1`、`expr2`、`expr3`的型別必須一致。Go 的 `switch` 非常靈活,表示式不必是常數或整數,執行的過程從上至下,直到找到匹配項;而如果 `switch` 沒有表示式,它會匹配`true`。
|
||||
```Go
|
||||
|
||||
i := 10
|
||||
@@ -198,19 +198,19 @@ default case
|
||||
|
||||
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
|
||||
//這裡是處理邏輯程式碼
|
||||
//返回多個值
|
||||
//回傳多個值
|
||||
return value1, value2
|
||||
}
|
||||
```
|
||||
上面的程式碼我們看出
|
||||
|
||||
- 關鍵字 `func` 用來宣告一個函式`funcName`
|
||||
- 函式可以有一個或者多個引數,每個引數後面帶有型別,透過`,`分隔
|
||||
- 函式可以返回多個值
|
||||
- 上面返回值聲明瞭兩個變數 `output1` 和`output2`,如果你不想宣告也可以,直接就兩個型別
|
||||
- 如果只有一個返回值且不宣告返回值變數,那麼你可以省略 包括返回值 的括號
|
||||
- 如果沒有返回值,那麼就直接省略最後的返回資訊
|
||||
- 如果有返回值, 那麼必須在函式的外層新增 return 語句
|
||||
- 函式可以有一個或者多個參數,每個參數後面帶有型別,透過`,`分隔
|
||||
- 函式可以回傳多個值
|
||||
- 上面回傳值宣告了兩個變數 `output1` 和`output2`,如果你不想宣告也可以,直接就兩個型別
|
||||
- 如果只有一個回傳值且不宣告回傳值變數,那麼你可以省略 包括回傳值 的括號
|
||||
- 如果沒有回傳值,那麼就直接省略最後的回傳資訊
|
||||
- 如果有回傳值, 那麼必須在函式的外層新增 return 語句
|
||||
|
||||
下面我們來看一個實際應用函式的例子(用來計算 Max 值)
|
||||
```Go
|
||||
@@ -219,7 +219,7 @@ package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
// 返回 a、b 中最大值.
|
||||
// 回傳 a、b 中最大值.
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
@@ -240,10 +240,10 @@ func main() {
|
||||
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在這直接呼叫它
|
||||
}
|
||||
```
|
||||
上面這個裡面我們可以看到 `max` 函式有兩個引數,它們的型別都是`int`,那麼第一個變數的型別可以省略(即 a,b int,而非 a int, b int),預設為離它最近的型別,同理多於 2 個同類型的變數或者返回值。同時我們注意到它的返回值就是一個型別,這個就是省略寫法。
|
||||
上面這個裡面我們可以看到 `max` 函式有兩個參數,它們的型別都是`int`,那麼第一個變數的型別可以省略(即 a,b int,而非 a int, b int),預設為離它最近的型別,同理多於 2 個同類型的變數或者回傳值。同時我們注意到它的回傳值就是一個型別,這個就是省略寫法。
|
||||
|
||||
### 多個返回值
|
||||
Go 語言比 C 更先進的特性,其中一點就是函式能夠返回多個值。
|
||||
### 多個回傳值
|
||||
Go 語言比 C 更先進的特性,其中一點就是函式能夠回傳多個值。
|
||||
|
||||
我們直接上程式碼看例子
|
||||
```Go
|
||||
@@ -252,7 +252,7 @@ package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
//返回 A+B 和 A*B
|
||||
//回傳 A+B 和 A*B
|
||||
func SumAndProduct(A, B int) (int, int) {
|
||||
return A+B, A*B
|
||||
}
|
||||
@@ -267,7 +267,7 @@ func main() {
|
||||
fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
|
||||
}
|
||||
```
|
||||
上面的例子我們可以看到直接返回了兩個引數,當然我們也可以命名返回引數的變數,這個例子裡面只是用了兩個型別,我們也可以改成如下這樣的定義,然後返回的時候不用帶上變數名,因為直接在函式裡面初始化了。但如果你的函式是匯出的(首字母大寫),官方建議:最好命名返回值,因為不命名返回值,雖然使得程式碼更加簡潔了,但是會造成產生的文件可讀性差。
|
||||
上面的例子我們可以看到直接回傳了兩個參數,當然我們也可以命名回傳參數的變數,這個例子裡面只是用了兩個型別,我們也可以改成如下這樣的定義,然後回傳的時候不用帶上變數名,因為直接在函式裡面初始化了。但如果你的函式是匯出的(首字母大寫),官方建議:最好命名回傳值,因為不命名回傳值,雖然使得程式碼更加簡潔了,但是會造成產生的文件可讀性差。
|
||||
```Go
|
||||
|
||||
func SumAndProduct(A, B int) (add int, Multiplied int) {
|
||||
@@ -276,33 +276,38 @@ func SumAndProduct(A, B int) (add int, Multiplied int) {
|
||||
return
|
||||
}
|
||||
```
|
||||
### 變參
|
||||
Go 函式支援變參。接受變參的函式是有著不定數量的引數的。為了做到這點,首先需要定義函式使其接受變參:
|
||||
```Go
|
||||
|
||||
### 可變參數函式 (Variadic functions)
|
||||
|
||||
Go 函式支援可變參數函式。接受可變參數的函式是有著不定數量的參數的。為了做到這點,首先需要定義函式使其接受可變參數:
|
||||
|
||||
```Go
|
||||
func myfunc(arg ...int) {}
|
||||
```
|
||||
`arg ...int`告訴 Go 這個函式接受不定數量的引數。注意,這些引數的型別全部是`int`。在函式體中,變數 `arg` 是一個 `int` 的`slice`:
|
||||
```Go
|
||||
|
||||
`arg ...int` 告訴 Go 這個函式接受不定數量的參數。注意,這些參數的型別全部是 `int`。在函式體中,變數 `arg` 是一個 `int` 的 `slice`:
|
||||
|
||||
```Go
|
||||
for _, n := range arg {
|
||||
fmt.Printf("And the number is: %d\n", n)
|
||||
}
|
||||
```
|
||||
|
||||
### 傳值與傳指標
|
||||
當我們傳一個引數值到被呼叫函式裡面時,實際上是傳了這個值的一份 copy,當在被呼叫函式中修改引數值的時候,呼叫函式中相應實參不會發生任何變化,因為數值變化只作用在 copy 上。
|
||||
|
||||
當我們傳一個參數值到被呼叫函式裡面時,實際上是傳了這個值的一份 copy,當在被呼叫函式中修改參數值的時候,呼叫函式中相應參數不會發生任何變化,因為數值變化只作用在 copy 上。
|
||||
|
||||
為了驗證我們上面的說法,我們來看一個例子
|
||||
```Go
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
//簡單的一個函式,實現了引數+1 的操作
|
||||
//簡單的一個函式,實現了參數+1 的操作
|
||||
func add1(a int) int {
|
||||
a = a+1 // 我們改變了 a 的值
|
||||
return a //返回一個新值
|
||||
return a //回傳一個新值
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -318,21 +323,21 @@ func main() {
|
||||
```
|
||||
看到了嗎?雖然我們呼叫了 `add1` 函式,並且在 `add1` 中執行`a = a+1`操作,但是上面例子中 `x` 變數的值沒有發生變化
|
||||
|
||||
理由很簡單:因為當我們呼叫 `add1` 的時候,`add1`接收的引數其實是 `x` 的 copy,而不是 `x` 本身。
|
||||
理由很簡單:因為當我們呼叫 `add1` 的時候,`add1`接收的參數其實是 `x` 的 copy,而不是 `x` 本身。
|
||||
|
||||
那你也許會問了,如果真的需要傳這個 `x` 本身,該怎麼辦呢?
|
||||
|
||||
這就牽扯到了所謂的指標。我們知道,變數在記憶體中是存放於一定地址上的,修改變數實際是修改變數地址處的記憶體。只有 `add1` 函式知道 `x` 變數所在的地址,才能修改 `x` 變數的值。所以我們需要將 `x` 所在地址`&x`傳入函式,並將函式的引數的型別由 `int` 改為`*int`,即改為指標型別,才能在函式中修改 `x` 變數的值。此時引數仍然是按 copy 傳遞的,只是 copy 的是一個指標。請看下面的例子
|
||||
這就牽扯到了所謂的指標。我們知道,變數在記憶體中是存放於一定地址上的,修改變數實際是修改變數地址處的記憶體。只有 `add1` 函式知道 `x` 變數所在的地址,才能修改 `x` 變數的值。所以我們需要將 `x` 所在地址`&x`傳入函式,並將函式的參數的型別由 `int` 改為`*int`,即改為指標型別,才能在函式中修改 `x` 變數的值。此時參數仍然是按 copy 傳遞的,只是 copy 的是一個指標。請看下面的例子
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
//簡單的一個函式,實現了引數+1 的操作
|
||||
//簡單的一個函式,實現了參數+1 的操作
|
||||
func add1(a *int) int { // 請注意,
|
||||
*a = *a+1 // 修改了 a 的值
|
||||
return *a // 返回新值
|
||||
return *a // 回傳新值
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -349,11 +354,11 @@ func main() {
|
||||
這樣,我們就達到了修改 `x` 的目的。那麼到底傳指標有什麼好處呢?
|
||||
|
||||
- 傳指標使得多個函式能操作同一個物件。
|
||||
- 傳指標比較輕量級 (8bytes),只是傳記憶體地址,我們可以用指標傳遞體積大的結構體。如果用引數值傳遞的話, 在每次 copy 上面就會花費相對較多的系統開銷(記憶體和時間)。所以當你要傳遞大的結構體的時候,用指標是一個明智的選擇。
|
||||
- 傳指標比較輕量級 (8bytes),只是傳記憶體地址,我們可以用指標傳遞體積大的結構體。如果用參數值傳遞的話, 在每次 copy 上面就會花費相對較多的系統開銷(記憶體和時間)。所以當你要傳遞大的結構體的時候,用指標是一個明智的選擇。
|
||||
- Go 語言中`channel`,`slice`,`map`這三種類型的實現機制類似指標,所以可以直接傳遞,而不用取地址後傳遞指標。(注:若函式需改變 `slice` 的長度,則仍需要取地址傳遞指標)
|
||||
|
||||
### defer
|
||||
Go 語言中有種不錯的設計,即延遲(defer)語句,你可以在函式中新增多個 defer 語句。當函式執行到最後時,這些 defer 語句會按照逆序執行,最後該函式返回。特別是當你在進行一些開啟資源的操作時,遇到錯誤需要提前返回,在返回前你需要關閉相應的資源,不然很容易造成資源洩露等問題。如下程式碼所示,我們一般寫開啟一個資源是這樣操作的:
|
||||
Go 語言中有種不錯的設計,即延遲(defer)語句,你可以在函式中新增多個 defer 語句。當函式執行到最後時,這些 defer 語句會按照逆序執行,最後該函式回傳。特別是當你在進行一些開啟資源的操作時,遇到錯誤需要提前回傳,在回傳前你需要關閉相應的資源,不然很容易造成資源洩露等問題。如下程式碼所示,我們一般寫開啟一個資源是這樣操作的:
|
||||
```Go
|
||||
|
||||
func ReadWrite() bool {
|
||||
@@ -397,7 +402,7 @@ for i := 0; i < 5; i++ {
|
||||
```
|
||||
### 函式作為值、型別
|
||||
|
||||
在 Go 中函式也是一種變數,我們可以透過 `type` 來定義它,它的型別就是所有擁有相同的引數,相同的返回值的一種型別
|
||||
在 Go 中函式也是一種變數,我們可以透過 `type` 來定義它,它的型別就是所有擁有相同的參數,相同的回傳值的一種型別
|
||||
|
||||
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
|
||||
|
||||
@@ -408,7 +413,7 @@ package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
type testInt func(int) bool // 聲明瞭一個函式型別
|
||||
type testInt func(int) bool // 宣告了一個函式型別
|
||||
|
||||
func isOdd(integer int) bool {
|
||||
if integer%2 == 0 {
|
||||
@@ -424,7 +429,7 @@ func isEven(integer int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 宣告的函式型別在這個地方當做了一個引數
|
||||
// 宣告的函式型別在這個地方當做了一個參數
|
||||
|
||||
func filter(slice []int, f testInt) []int {
|
||||
var result []int
|
||||
@@ -445,19 +450,19 @@ func main(){
|
||||
fmt.Println("Even elements of slice are: ", even)
|
||||
}
|
||||
```
|
||||
函式當做值和型別在我們寫一些通用介面的時候非常有用,透過上面例子我們看到 `testInt` 這個型別是一個函式型別,然後兩個 `filter` 函式的引數和返回值與 `testInt` 型別是一樣的,但是我們可以實現很多種的邏輯,這樣使得我們的程式變得非常的靈活。
|
||||
函式當做值和型別在我們寫一些通用介面的時候非常有用,透過上面例子我們看到 `testInt` 這個型別是一個函式型別,然後兩個 `filter` 函式的參數和回傳值與 `testInt` 型別是一樣的,但是我們可以實現很多種的邏輯,這樣使得我們的程式變得非常的靈活。
|
||||
|
||||
### Panic 和 Recover
|
||||
|
||||
Go 沒有像 Java 那樣的異常機制,它不能丟擲異常,而是使用了 `panic` 和`recover`機制。一定要記住,你應當把它作為最後的手段來使用,也就是說,你的程式碼中應當沒有,或者很少有 `panic` 的東西。這是個強大的工具,請明智地使用它。那麼,我們應該如何使用它呢?
|
||||
|
||||
Panic
|
||||
>是一個內建函式,可以中斷原有的控制流程,進入一個 `panic` 狀態中。當函式 `F` 呼叫`panic`,函式 F 的執行被中斷,但是 `F` 中的延遲函式會正常執行,然後 F 返回到呼叫它的地方。在呼叫的地方,`F`的行為就像呼叫了`panic`。這一過程繼續向上,直到發生 `panic` 的`goroutine`中所有呼叫的函式返回,此時程式退出。`panic`可以直接呼叫 `panic` 產生。也可以由執行時錯誤產生,例如訪問越界的陣列。
|
||||
>是一個內建函式,可以中斷原有的控制流程,進入一個 `panic` 狀態中。當函式 `F` 呼叫`panic`,函式 F 的執行被中斷,但是 `F` 中的延遲函式會正常執行,然後 F 回傳到呼叫它的地方。在呼叫的地方,`F`的行為就像呼叫了`panic`。這一過程繼續向上,直到發生 `panic` 的`goroutine`中所有呼叫的函式回傳,此時程式退出。`panic`可以直接呼叫 `panic` 產生。也可以由執行時錯誤產生,例如訪問越界的陣列。
|
||||
|
||||
Recover
|
||||
>是一個內建的函式,可以讓進入 `panic` 狀態的 `goroutine` 恢復過來。`recover`僅在延遲函式中有效。在正常的執行過程中,呼叫 `recover` 會返回`nil`,並且沒有其它任何效果。如果當前的 `goroutine` 陷入 `panic` 狀態,呼叫 `recover` 可以捕獲到 `panic` 的輸入值,並且恢復正常的執行。
|
||||
>是一個內建的函式,可以讓進入 `panic` 狀態的 `goroutine` 恢復過來。`recover`僅在延遲函式中有效。在正常的執行過程中,呼叫 `recover` 會回傳`nil`,並且沒有其它任何效果。如果當前的 `goroutine` 陷入 `panic` 狀態,呼叫 `recover` 可以捕獲到 `panic` 的輸入值,並且恢復正常的執行。
|
||||
|
||||
下面這個函式示範瞭如何在過程中使用`panic`
|
||||
下面這個函式示範了如何在過程中使用`panic`
|
||||
```Go
|
||||
|
||||
var user = os.Getenv("USER")
|
||||
@@ -468,7 +473,7 @@ func init() {
|
||||
}
|
||||
}
|
||||
```
|
||||
下面這個函式檢查作為其引數的函式在執行時是否會產生`panic`:
|
||||
下面這個函式檢查作為其參數的函式在執行時是否會產生`panic`:
|
||||
```Go
|
||||
|
||||
func throwsPanic(f func()) (b bool) {
|
||||
@@ -483,13 +488,13 @@ func throwsPanic(f func()) (b bool) {
|
||||
```
|
||||
### `main`函式和 `init` 函式
|
||||
|
||||
Go 裡面有兩個保留的函式:`init`函式(能夠應用於所有的`package`)和 `main` 函式(只能應用於`package main`)。這兩個函式在定義時不能有任何的引數和返回值。雖然一個 `package` 裡面可以寫任意多個 `init` 函式,但這無論是對於可讀性還是以後的可維護性來說,我們都強烈建議使用者在一個 `package` 中每個檔案只寫一個 `init` 函式。
|
||||
Go 裡面有兩個保留的函式:`init`函式(能夠應用於所有的`package`)和 `main` 函式(只能應用於`package main`)。這兩個函式在定義時不能有任何的參數和回傳值。雖然一個 `package` 裡面可以寫任意多個 `init` 函式,但這無論是對於可讀性還是以後的可維護性來說,我們都強烈建議使用者在一個 `package` 中每個檔案只寫一個 `init` 函式。
|
||||
|
||||
Go 程式會自動呼叫`init()`和`main()`,所以你不需要在任何地方呼叫這兩個函式。每個 `package` 中的 `init` 函式都是可選的,但`package main`就必須包含一個 `main` 函式。
|
||||
|
||||
程式的初始化和執行都起始於 `main` 套件。如果 `main` 套件還匯入了其它的套件,那麼就會在編譯時將它們依次匯入。有時一個套件會被多個套件同時匯入,那麼它只會被匯入一次(例如很多套件可能都會用到 `fmt` 套件,但它只會被匯入一次,因為沒有必要匯入多次)。當一個套件被匯入時,如果該套件還匯入了其它的套件,那麼會先將其它套件匯入進來,然後再對這些套件中的套件級常量和變數進行初始化,接著執行 `init` 函式(如果有的話),依次類別推。等所有被匯入的套件都載入完畢了,就會開始對 `main` 套件中的套件級常量和變數進行初始化,然後執行 `main` 套件中的 `init` 函式(如果存在的話),最後執行 `main` 函式。下圖詳細地解釋了整個執行過程:
|
||||
程式的初始化和執行都起始於 `main` 套件。如果 `main` 套件還匯入了其它的套件,那麼就會在編譯時將它們依次匯入。有時一個套件會被多個套件同時匯入,那麼它只會被匯入一次(例如很多套件可能都會用到 `fmt` 套件,但它只會被匯入一次,因為沒有必要匯入多次)。當一個套件被匯入時,如果該套件還匯入了其它的套件,那麼會先將其它套件匯入進來,然後再對這些套件中的 "套件級" (package-level) 常數和變數進行初始化,接著執行 `init` 函式(如果有的話),依次類別推。等所有被匯入的套件都載入完畢了,就會開始對 `main` 套件中的 "套件級" 常數和變數進行初始化,然後執行 `main` 套件中的 `init` 函式(如果存在的話),最後執行 `main` 函式。下圖詳細地解釋了整個執行過程:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 2.6 main 函式引入套件初始化流程圖
|
||||
|
||||
@@ -517,7 +522,7 @@ fmt.Println("hello world")
|
||||
import “shorturl/model” //載入 gopath/src/shorturl/model 模組
|
||||
|
||||
|
||||
上面展示了一些 import 常用的幾種方式,但是還有一些特殊的 import,讓很多新手很費解,下面我們來一一講解一下到底是怎麼一回事
|
||||
上面展示了一些 import 常用的幾種方式,但是還有一些特殊的 import,讓很多新手很難以理解,下面我們來一一講解一下到底是怎麼一回事
|
||||
|
||||
|
||||
1. 點操作
|
||||
@@ -542,7 +547,7 @@ fmt.Println("hello world")
|
||||
|
||||
3. _操作
|
||||
|
||||
這個操作經常是讓很多人費解的一個運算子,請看下面這個 import
|
||||
這個操作經常是讓很多人難以理解的一個運算子,請看下面這個 import
|
||||
```Go
|
||||
|
||||
import (
|
||||
|
||||
@@ -53,7 +53,7 @@ type person struct {
|
||||
age int
|
||||
}
|
||||
|
||||
// 比較兩個人的年齡,返回年齡大的那個人,並且返回年齡差
|
||||
// 比較兩個人的年齡,回傳年齡大的那個人,並且回傳年齡差
|
||||
// struct 也是傳值的
|
||||
func Older(p1, p2 person) (person, int) {
|
||||
if p1.age>p2.age { // 比較 p1 和 p2 這兩個人的年齡
|
||||
@@ -89,9 +89,9 @@ func main() {
|
||||
}
|
||||
```
|
||||
### struct 的匿名欄位
|
||||
我們上面介紹瞭如何定義一個 struct,定義的時候是欄位名與其型別一一對應,實際上 Go 支援只提供型別,而不寫欄位名的方式,也就是匿名欄位,也稱為嵌入欄位。
|
||||
我們上面介紹了如何定義一個 struct,定義的時候是欄位名與其型別一一對應,實際上 Go 支援只提供型別,而不寫欄位名的方式,也就是匿名欄位,也稱為嵌入欄位。
|
||||
|
||||
當匿名欄位是一個 struct 的時候,那麼這個 struct 所擁有的全部欄位都被隱式地引入了當前定義的這個 struct。
|
||||
當匿名欄位是一個 struct 的時候,那麼這個 struct 所擁有的全部欄位都被隱含的引入了當前定義的這個 struct。
|
||||
|
||||
讓我們來看一個例子,讓上面說的這些更具體化
|
||||
```Go
|
||||
@@ -136,7 +136,7 @@ func main() {
|
||||
```
|
||||
圖例如下:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 2.7 struct 組合,Student 組合了 Human struct 和 string 基本型別
|
||||
|
||||
|
||||
@@ -24,19 +24,19 @@ func main() {
|
||||
fmt.Println("Area of r2 is: ", area(r2))
|
||||
}
|
||||
```
|
||||
這段程式碼可以計算出來長方形的面積,但是 area()不是作為 Rectangle 的方法實現的(類似物件導向裡面的方法),而是將 Rectangle 的物件(如 r1,r2)作為引數傳入函式計算面積的。
|
||||
這段程式碼可以計算出來長方形的面積,但是 area()不是作為 Rectangle 的方法實現的(類似物件導向裡面的方法),而是將 Rectangle 的物件(如 r1,r2)作為參數傳入函式計算面積的。
|
||||
|
||||
這樣實現當然沒有問題咯,但是當需要增加圓形、正方形、五邊形甚至其它多邊形的時候,你想計算他們的面積的時候怎麼辦啊?那就只能增加新的函式咯,但是函式名你就必須要跟著換了,變成`area_rectangle, area_circle, area_triangle...`
|
||||
這樣實現當然沒有問題囉,但是當需要增加圓形、正方形、五邊形甚至其它多邊形的時候,你想計算他們的面積的時候怎麼辦啊?那就只能增加新的函式囉,但是函式名你就必須要跟著換了,變成`area_rectangle, area_circle, area_triangle...`
|
||||
|
||||
像下圖所表示的那樣, 橢圓代表函式, 而這些函式並不從屬於 struct(或者以物件導向的術語來說,並不屬於 class),他們是單獨存在於 struct 外圍,而非在概念上屬於某個 struct 的。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 2.8 方法和 struct 的關係圖
|
||||
|
||||
很顯然,這樣的實現並不優雅,並且從概念上來說"面積"是"形狀"的一個屬性,它是屬於這個特定的形狀的,就像長方形的長和寬一樣。
|
||||
|
||||
基於上面的原因所以就有了 `method` 的概念,`method`是附屬在一個給定的型別上的,他的語法和函式的宣告語法幾乎一樣,只是在 `func` 後面增加了一個 receiver(也就是 method 所依從的主體)。
|
||||
基於上面的原因所以就有了 `method` 的概念,`method`是附屬在一個給定的型別上的,他的語法和函式的宣告語法幾乎一樣,只是在 `func` 後面增加了一個 receiver (也就是 method 所依從的主體)。
|
||||
|
||||
用上面提到的形狀的例子來說,method `area()` 是依賴於某個形狀(比如說 Rectangle)來發生作用的。Rectangle.area()的發出者是 Rectangle, area()是屬於 Rectangle 的方法,而非一個外圍函式。
|
||||
|
||||
@@ -99,20 +99,20 @@ func main() {
|
||||
|
||||
圖示如下:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 2.9 不同 struct 的 method 不同
|
||||
|
||||
在上例,method area() 分別屬於 Rectangle 和 Circle, 於是他們的 Receiver 就變成了 Rectangle 和 Circle, 或者說,這個 area()方法 是由 Rectangle/Circle 發出的。
|
||||
|
||||
>值得說明的一點是,圖示中 method 用虛線標出,意思是此處方法的 Receiver 是以值傳遞,而非參考傳遞,是的,Receiver 還可以是指標, 兩者的差別在於, 指標作為 Receiver 會對例項物件的內容發生操作,而普通型別作為 Receiver 僅僅是以副本作為操作物件,並不對原例項物件發生操作。後文對此會有詳細論述。
|
||||
>值得說明的一點是,圖示中 method 用虛線標出,意思是此處方法的 Receiver 是以值傳遞,而非參考傳遞,是的,Receiver 還可以是指標, 兩者的差別在於, 指標作為 Receiver 會對實體物件的內容發生操作,而普通型別作為 Receiver 僅僅是以副本作為操作物件,並不對原實體物件發生操作。後文對此會有詳細論述。
|
||||
|
||||
那是不是 method 只能作用在 struct 上面呢?當然不是咯,他可以定義在任何你自訂的型別、內建型別、struct 等各種型別上面。這裡你是不是有點迷糊了,什麼叫自訂型別,自訂型別不就是 struct 嘛,不是這樣的哦,struct 只是自訂型別裡面一種比較特殊的型別而已,還有其他自訂型別申明,可以透過如下這樣的申明來實現。
|
||||
那是不是 method 只能作用在 struct 上面呢?當然不是囉,他可以定義在任何你自訂的型別、內建型別、struct 等各種型別上面。這裡你是不是有點迷糊了,什麼叫自訂型別,自訂型別不就是 struct 嘛,不是這樣的哦,struct 只是自訂型別裡面一種比較特殊的型別而已,還有其他自訂型別宣告,可以透過如下這樣的宣告來實現。
|
||||
```Go
|
||||
|
||||
type typeName typeLiteral
|
||||
```
|
||||
請看下面這個申明自訂型別的程式碼
|
||||
請看下面這個宣告自訂型別的程式碼
|
||||
```Go
|
||||
|
||||
type ages int
|
||||
@@ -209,7 +209,7 @@ func main() {
|
||||
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
|
||||
}
|
||||
```
|
||||
上面的程式碼透過 const 定義了一些常量,然後定義了一些自訂型別
|
||||
上面的程式碼透過 const 定義了一些常數,然後定義了一些自訂型別
|
||||
|
||||
- Color 作為 byte 的別名
|
||||
- 定義了一個 struct:Box,含有三個長寬高欄位和一個顏色屬性
|
||||
@@ -217,11 +217,11 @@ func main() {
|
||||
|
||||
然後以上面的自訂型別為接收者定義了一些 method
|
||||
|
||||
- Volume()定義了接收者為 Box,返回 Box 的容量
|
||||
- Volume()定義了接收者為 Box,回傳 Box 的容量
|
||||
- SetColor(c Color),把 Box 的顏色改為 c
|
||||
- BiggestColor()定在在 BoxList 上面,返回 list 裡面容量最大的顏色
|
||||
- BiggestColor()定在在 BoxList 上面,回傳 list 裡面容量最大的顏色
|
||||
- PaintItBlack()把 BoxList 裡面所有 Box 的顏色全部變成黑色
|
||||
- String()定義在 Color 上面,返回 Color 的具體顏色(字串格式)
|
||||
- String()定義在 Color 上面,回傳 Color 的具體顏色(字串格式)
|
||||
|
||||
上面的程式碼透過文字描述出來之後是不是很簡單?我們一般解決問題都是透過問題的描述,去寫相應的程式碼實現。
|
||||
|
||||
@@ -230,7 +230,7 @@ func main() {
|
||||
|
||||
我們定義 SetColor 的真正目的是想改變這個 Box 的顏色,如果不傳 Box 的指標,那麼 SetColor 接受的其實是 Box 的一個 copy,也就是說 method 內對於顏色值的修改,其實只作用於 Box 的 copy,而不是真正的 Box。所以我們需要傳入指標。
|
||||
|
||||
這裡可以把 receiver 當作 method 的第一個引數來看,然後結合前面函式講解的傳值和傳參考就不難理解
|
||||
這裡可以把 receiver 當作 method 的第一個參數來看,然後結合前面函式講解的傳值和傳參考就不難理解
|
||||
|
||||
這裡你也許會問了那 SetColor 函式裡面應該這樣定義`*b.Color=c`,而不是`b.Color=c`,因為我們需要讀取到指標相應的值。
|
||||
|
||||
@@ -241,7 +241,7 @@ func main() {
|
||||
你又說對了,這兩種方式都可以,因為 Go 知道 receiver 是指標,他自動幫你轉了。
|
||||
|
||||
也就是說:
|
||||
>如果一個 method 的 receiver 是*T,你可以在一個 T 型別的例項變數 V 上面呼叫這個 method,而不需要&V 去呼叫這個 method
|
||||
>如果一個 method 的 receiver 是 *T,你可以在一個 T 型別的變數 V 上面呼叫這個 method,而不需要 &V 去呼叫這個 method
|
||||
|
||||
類似的
|
||||
>如果一個 method 的 receiver 是 T,你可以在一個*T 型別的變數 P 上面呼叫這個 method,而不需要 *P 去呼叫這個 method
|
||||
@@ -249,6 +249,7 @@ func main() {
|
||||
所以,你不用擔心你是呼叫的指標的 method 還是不是指標的 method,Go 知道你要做的一切,這對於有多年 C/C++程式設計經驗的同學來說,真是解決了一個很大的痛苦。
|
||||
|
||||
### method 繼承
|
||||
|
||||
前面一章我們學習了欄位的繼承,那麼你也會發現 Go 的一個神奇之處,method 也是可以繼承的。如果匿名欄位實現了一個 method,那麼包含這個匿名欄位的 struct 也能呼叫該 method。讓我們來看下面這個例子
|
||||
```Go
|
||||
|
||||
|
||||
@@ -195,9 +195,9 @@ s := "Hello world"
|
||||
a = i
|
||||
a = s
|
||||
```
|
||||
一個函式把 interface{} 作為引數,那麼他可以接受任意型別的值作為引數,如果一個函式返回 interface{},那麼也就可以返回任意型別的值。是不是很有用啊!
|
||||
### interface 函式引數
|
||||
interface 的變數可以持有任意實現該 interface 型別的物件,這給我們編寫函式(包括 method)提供了一些額外的思考,我們是不是可以透過定義 interface 引數,讓函式接受各種型別的引數。
|
||||
一個函式把 interface{} 作為參數,那麼他可以接受任意型別的值作為參數,如果一個函式回傳 interface{},那麼也就可以回傳任意型別的值。是不是很有用啊!
|
||||
### interface 函式參數
|
||||
interface 的變數可以持有任意實現該 interface 型別的物件,這給我們編寫函式(包括 method)提供了一些額外的思考,我們是不是可以透過定義 interface 參數,讓函式接受各種型別的參數。
|
||||
|
||||
舉個例子:fmt.Println 是我們常用的一個函式,但是你是否注意到它可以接受任意型別的資料。開啟 fmt 的原始碼檔案,你會看到這樣一個定義:
|
||||
```Go
|
||||
@@ -206,7 +206,7 @@ type Stringer interface {
|
||||
String() string
|
||||
}
|
||||
```
|
||||
也就是說,任何實現了 String 方法的型別都能作為引數被 fmt.Println 呼叫,讓我們來試一試
|
||||
也就是說,任何實現了 String 方法的型別都能作為參數被 fmt.Println 呼叫,讓我們來試一試
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -247,7 +247,7 @@ fmt.Println("The biggest one is", boxes.BiggestsColor())
|
||||
|
||||
Go 語言裡面有一個語法,可以直接判斷是否是該型別的變數: value, ok = element.(T),這裡 value 就是變數的值,ok 是一個 bool 型別,element 是 interface 變數,T 是斷言的型別。
|
||||
|
||||
如果 element 裡面確實儲存了 T 型別的數值,那麼 ok 返回 true,否則返回 false。
|
||||
如果 element 裡面確實儲存了 T 型別的數值,那麼 ok 回傳 true,否則回傳 false。
|
||||
|
||||
讓我們透過一個例子來更加深入的理解。
|
||||
```Go
|
||||
@@ -382,7 +382,7 @@ Go 語言實現了反射,所謂反射就是能檢查程式在執行時的狀
|
||||
使用 reflect 一般分成三步,下面簡要的講解一下:要去反射是一個型別的值(這些值都實現了空 interface),首先需要把它轉化成 reflect 物件(reflect.Type 或者 reflect.Value,根據不同的情況呼叫不同的函式)。這兩種取得方式如下:
|
||||
```Go
|
||||
|
||||
t := reflect.TypeOf(i) //得到型別的元資料,透過 t 我們能取得型別定義裡面的所有元素
|
||||
t := reflect.TypeOf(i) //得到型別的 Meta 資料,透過 t 我們能取得型別定義裡面的所有元素
|
||||
v := reflect.ValueOf(i) //得到實際的值,透過 v 我們取得儲存在裡面的值,還可以去改變值
|
||||
```
|
||||
轉化為 reflect 物件之後我們就可以進行一些操作了,也就是將 reflect 物件轉化成相應的值,例如
|
||||
@@ -391,7 +391,7 @@ v := reflect.ValueOf(i) //得到實際的值,透過 v 我們取得儲存在
|
||||
tag := t.Elem().Field(0).Tag //取得定義在 struct 裡面的標籤
|
||||
name := v.Elem().Field(0).String() //取得儲存在第一個欄位裡面的值
|
||||
```
|
||||
取得反射值能返回相應的型別和數值
|
||||
取得反射值能回傳相應的型別和數值
|
||||
```Go
|
||||
|
||||
var x float64 = 3.4
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## goroutine
|
||||
|
||||
goroutine 是 Go 並行設計的核心。goroutine 說到底其實就是協程,但是它比執行緒更小,十幾個 goroutine 可能體現在底層就是五六個執行緒,Go 語言內部幫你實現了這些 goroutine 之間的記憶體共享。執行 goroutine 只需極少的棧記憶體(大概是 4~5KB),當然會根據相應的資料伸縮。也正因為如此,可同時執行成千上萬個併發任務。goroutine 比 thread 更易用、更高效、更輕便。
|
||||
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
|
||||
@@ -51,7 +51,7 @@ func main() {
|
||||
|
||||
>預設情況下,在 Go 1.5 將標識併發系統執行緒個數的 runtime.GOMAXPROCS 的初始值由 1 改為了執行環境的 CPU 核數。
|
||||
|
||||
但在 Go 1.5 以前排程器僅使用單執行緒,也就是說只實現了併發。想要發揮多核處理器的並行,需要在我們的程式中顯式呼叫 runtime.GOMAXPROCS(n) 告訴排程器同時使用多個執行緒。GOMAXPROCS 設定了同時執行邏輯程式碼的系統執行緒的最大數量,並返回之前的設定。如果 n < 1,不會改變當前設定。
|
||||
但在 Go 1.5 以前排程器僅使用單執行緒,也就是說只實現了併發。想要發揮多核處理器的並行,需要在我們的程式中明確的呼叫 runtime.GOMAXPROCS(n) 告訴排程器同時使用多個執行緒。GOMAXPROCS 設定了同時執行邏輯程式碼的系統執行緒的最大數量,並回傳之前的設定。如果 n < 1,不會改變當前設定。
|
||||
|
||||
## channels
|
||||
goroutine 執行在相同的地址空間,因此訪問共享記憶體必須做好同步。那麼 goroutine 之間如何進行資料的通訊呢,Go 提供了一個很好的通訊機制 channel。channel 可以與 Unix shell 中的雙向管道做類別比:可以透過它傳送或者接收值。這些值只能是特定的型別:channel 型別。定義一個 channel 時,也需要定義傳送到 channel 的值的型別。注意,必須使用 make 建立 channel:
|
||||
@@ -94,7 +94,7 @@ func main() {
|
||||
fmt.Println(x, y, x + y)
|
||||
}
|
||||
```
|
||||
預設情況下,channel 接收和傳送資料都是阻塞的,除非另一端已經準備好,這樣就使得 Goroutines 同步變的更加的簡單,而不需要顯式的 lock。所謂阻塞,也就是如果讀取(value := <-ch)它將會被阻塞,直到有資料接收。其次,任何傳送(ch<-5)將會被阻塞,直到資料被讀出。無緩衝 channel 是在多個 goroutine 之間同步很棒的工具。
|
||||
預設情況下,channel 接收和傳送資料都是阻塞的,除非另一端已經準備好,這樣就使得 Goroutines 同步變的更加的簡單,而不需要明確的 lock。所謂阻塞,也就是如果讀取(value := <-ch)它將會被阻塞,直到有資料接收。其次,任何傳送(ch<-5)將會被阻塞,直到資料被讀出。無緩衝 channel 是在多個 goroutine 之間同步很棒的工具。
|
||||
|
||||
## Buffered Channels
|
||||
上面我們介紹了預設的非快取型別的 channel,不過 Go 也允許指定 channel 的緩衝大小,很簡單,就是 channel 可以儲存多少元素。ch:= make(chan bool, 4),建立了可以儲存 4 個元素的 bool 型 channel。在這個 channel 中,前 4 個元素可以無阻塞的寫入。當寫入第 5 個元素時,程式碼將會阻塞,直到其他 goroutine 從 channel 中讀取一些元素,騰出空間。
|
||||
@@ -149,17 +149,17 @@ func main() {
|
||||
}
|
||||
}
|
||||
```
|
||||
`for i := range c`能夠不斷的讀取 channel 裡面的資料,直到該 channel 被顯式的關閉。上面程式碼我們看到可以顯式的關閉 channel,生產者透過內建函式 `close` 關閉 channel。關閉 channel 之後就無法再發送任何資料了,在消費方可以透過語法`v, ok := <-ch`測試 channel 是否被關閉。如果 ok 返回 false,那麼說明 channel 已經沒有任何資料並且已經被關閉。
|
||||
`for i := range c`能夠不斷的讀取 channel 裡面的資料,直到該 channel 被明確的關閉。上面程式碼我們看到可以明確的關閉 channel,生產者透過內建函式 `close` 關閉 channel。關閉 channel 之後就無法再發送任何資料了,在消費方可以透過語法`v, ok := <-ch`測試 channel 是否被關閉。如果 ok 回傳 false,那麼說明 channel 已經沒有任何資料並且已經被關閉。
|
||||
|
||||
>記住應該在生產者的地方關閉 channel,而不是消費的地方去關閉它,這樣容易引起 panic
|
||||
|
||||
|
||||
>另外記住一點的就是 channel 不像檔案之類別的,不需要經常去關閉,只有當你確實沒有任何傳送資料了,或者你想顯式的結束 range 迴圈之類別的
|
||||
>另外記住一點的就是 channel 不像檔案之類別的,不需要經常去關閉,只有當你確實沒有任何傳送資料了,或者你想明確的結束 range 迴圈之類別的
|
||||
|
||||
## Select
|
||||
我們上面介紹的都是隻有一個 channel 的情況,那麼如果存在多個 channel 的時候,我們該如何操作呢,Go 裡面提供了一個關鍵字`select`,透過 `select` 可以監聽 channel 上的資料流動。
|
||||
|
||||
`select`預設是阻塞的,只有當監聽的 channel 中有傳送或接收可以進行時才會執行,當多個 channel 都準備好的時候,select 是隨機的選擇一個執行的。
|
||||
`select` 預設是阻塞的,只有當監聽的 channel 中有傳送或接收可以進行時才會執行,當多個 channel 都準備好的時候,select 會隨機選擇其中一個執行。
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -237,15 +237,15 @@ runtime 套件中有幾個處理 goroutine 的函式:
|
||||
|
||||
- NumCPU
|
||||
|
||||
返回 CPU 核數量
|
||||
回傳 CPU 核數量
|
||||
|
||||
- NumGoroutine
|
||||
|
||||
返回正在執行和排隊的任務總數
|
||||
回傳正在執行和排隊的任務總數
|
||||
|
||||
- GOMAXPROCS
|
||||
|
||||
用來設定可以平行計算的 CPU 核數的最大值,並返回之前的值。
|
||||
用來設定可以平行計算的 CPU 核數的最大值,並回傳之前的值。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ chan else goto package switch
|
||||
const fallthrough if range type
|
||||
continue for import return var
|
||||
```
|
||||
- var 和 const 參考 2.2Go 語言基礎裡面的變數和常量申明
|
||||
- var 和 const 參考 2.2Go 語言基礎裡面的變數和常數宣告
|
||||
- package 和 import 已經有過短暫的接觸
|
||||
- func 用於定義函式和方法
|
||||
- return 用於從函式返回
|
||||
- return 用於從函式回傳
|
||||
- defer 用於類似解構函式
|
||||
- go 用於併發
|
||||
- select 用於選擇不同型別的通訊
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
學習基於 Web 的程式設計可能正是你讀本書的原因。事實上,如何透過 Go 來編寫 Web 應用也是我編寫這本書的初衷。前面已經介紹過,Go 目前已經擁有了成熟的 HTTP 處理套件,這使得編寫能做任何事情的動態 Web 程式易如反掌。在接下來的各章中將要介紹的內容,都是屬於 Web 程式設計的範疇。本章則集中討論一些與 Web 相關的概念和 Go 如何執行 Web 程式的話題。
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
我們平時瀏覽網頁的時候,會開啟瀏覽器,輸入網址後按下回車鍵,然後就會顯示出你想要瀏覽的內容。在這個看似簡單的使用者行為背後,到底隱藏了些什麼呢?
|
||||
|
||||
對於普通的上網過程,系統其實是這樣做的:瀏覽器本身是一個客戶端,當你輸入 URL 的時候,首先瀏覽器會去請求 DNS 伺服器,透過 DNS 取得相應的域名對應的 IP,然後透過 IP 地址找到 IP 對應的伺服器後,要求建立 TCP 連線,等瀏覽器傳送完 HTTP Request(請求)套件後,伺服器接收到請求套件之後才開始處理請求套件,伺服器呼叫自身服務,返回 HTTP Response(響應)套件;客戶端收到來自伺服器的響應後開始渲染這個 Response 套件裡的主體(body),等收到全部的內容隨後斷開與該伺服器之間的 TCP 連線。
|
||||
對於普通的上網過程,系統其實是這樣做的:瀏覽器本身是一個客戶端,當你輸入 URL 的時候,首先瀏覽器會去請求 DNS 伺服器,透過 DNS 取得相應的域名對應的 IP,然後透過 IP 地址找到 IP 對應的伺服器後,要求建立 TCP 連線,等瀏覽器傳送完 HTTP Request(請求)封包後,伺服器接收到請求封包之後才開始處理請求封包,伺服器呼叫自身服務,回傳 HTTP Response 內容;客戶端收到來自伺服器的回應後開始渲染這個 Response 套件裡的主體(body),等收到全部的內容隨後斷開與該伺服器之間的 TCP 連線。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.1 使用者訪問一個 Web 站點的過程
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
Web 伺服器的工作原理可以簡單地歸納為:
|
||||
|
||||
- 客戶機透過 TCP/IP 協議建立到伺服器的 TCP 連線
|
||||
- 客戶端向伺服器傳送 HTTP 協議請求套件,請求伺服器裡的資源文件
|
||||
- 伺服器向客戶機發送 HTTP 協議應答套件,如果請求的資源包含有動態語言的內容,那麼伺服器會呼叫動態語言的解釋引擎負責處理“動態內容”,並將處理得到的資料返回給客戶端
|
||||
- 客戶機與伺服器斷開。由客戶端解釋 HTML 文件,在客戶端螢幕上渲染圖形結果
|
||||
- 客戶端透過 TCP/IP 協議建立到伺服器的 TCP 連線
|
||||
- 客戶端向伺服器傳送 HTTP 協議請求封包,請求伺服器裡的資源文件
|
||||
- 伺服器向客戶端發送 HTTP 協議回應封包,如果請求的資源包含有動態語言的內容,那麼伺服器會呼叫動態語言的解釋引擎負責處理“動態內容”,並將處理得到的資料回傳給客戶端
|
||||
- 客戶端與伺服器斷開。由客戶端解釋 HTML 文件,在客戶端螢幕上渲染圖形結果
|
||||
|
||||
一個簡單的 HTTP 事務就是這樣實現的,看起來很複雜,原理其實是挺簡單的。需要注意的是客戶機與伺服器之間的通訊是非持久連線的,也就是當伺服器傳送了應答後就與客戶機斷開連線,等待下一次請求。
|
||||
一個簡單的 HTTP 事務就是這樣實現的,看起來很複雜,原理其實是挺簡單的。需要注意的是客戶端與伺服器之間的通訊是非持久連線的,也就是當伺服器傳送了回應後就與客戶端斷開連線,等待下一次請求。
|
||||
|
||||
## URL 和 DNS 解析
|
||||
我們瀏覽網頁都是透過 URL 訪問的,那麼 URL 到底是怎麼樣的呢?
|
||||
@@ -34,7 +34,7 @@ URL(Uniform Resource Locator)是“統一資源定位符”的英文縮寫,用
|
||||
|
||||
DNS(Domain Name System)是“域名系統”的英文縮寫,是一種組織成域層次結構的計算機和網路服務命名系統,它用於 TCP/IP 網路,它從事將主機名或域名轉換為實際 IP 地址的工作。DNS 就是這樣的一位“翻譯官”,它的基本工作原理可用下圖來表示。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.2 DNS 工作原理
|
||||
|
||||
@@ -42,21 +42,21 @@ URL(Uniform Resource Locator)是“統一資源定位符”的英文縮寫,用
|
||||
|
||||
1. 在瀏覽器中輸入 www.qq.com 域名,作業系統會先檢查自己本地的 hosts 檔案是否有這個網址對映關係,如果有,就先呼叫這個 IP 地址對映,完成域名解析。
|
||||
|
||||
2. 如果 hosts 裡沒有這個域名的對映,則查詢本地 DNS 解析器快取,是否有這個網址對映關係,如果有,直接返回,完成域名解析。
|
||||
2. 如果 hosts 裡沒有這個域名的對映,則查詢本地 DNS 解析器快取,是否有這個網址對映關係,如果有,直接回傳,完成域名解析。
|
||||
|
||||
3. 如果 hosts 與本地 DNS 解析器快取都沒有相應的網址對映關係,首先會找 TCP/IP 引數中設定的首選 DNS 伺服器,在此我們叫它本地 DNS 伺服器,此伺服器收到查詢時,如果要查詢的域名,包含在本地配置區域資源中,則返回解析結果給客戶機,完成域名解析,此解析具有權威性。
|
||||
3. 如果 hosts 與本地 DNS 解析器快取都沒有相應的網址對映關係,首先會找 TCP/IP 參數中設定的首選 DNS 伺服器,在此我們叫它本地 DNS 伺服器,此伺服器收到查詢時,如果要查詢的域名,包含在本地配置區域資源中,則回傳解析結果給客戶端,完成域名解析,此解析具有權威性。
|
||||
|
||||
4. 如果要查詢的域名,不由本地 DNS 伺服器區域解析,但該伺服器已快取了此網址對映關係,則呼叫這個 IP 地址對映,完成域名解析,此解析不具有權威性。
|
||||
|
||||
5. 如果本地 DNS 伺服器本地區域檔案與快取解析都失效,則根據本地 DNS 伺服器的設定(是否設定轉發器)進行查詢,如果未用轉發模式,本地 DNS 就把請求發至 “根 DNS 伺服器”,“根 DNS 伺服器”收到請求後會判斷這個域名(.com)是誰來授權管理,並會返回一個負責該頂級域名伺服器的一個 IP。本地 DNS 伺服器收到 IP 資訊後,將會聯絡負責.com 域的這臺伺服器。這臺負責.com 域的伺服器收到請求後,如果自己無法解析,它就會找一個管理.com 域的下一級 DNS 伺服器地址(qq.com)給本地 DNS 伺服器。當本地 DNS 伺服器收到這個地址後,就會找 qq.com 域伺服器,重複上面的動作,進行查詢,直至找到 www.qq.com 主機。
|
||||
5. 如果本地 DNS 伺服器本地區域檔案與快取解析都失效,則根據本地 DNS 伺服器的設定(是否設定轉發器)進行查詢,如果未用轉發模式,本地 DNS 就把請求發至 “根 DNS 伺服器”,“根 DNS 伺服器”收到請求後會判斷這個域名(.com)是誰來授權管理,並會回傳一個負責該頂級域名伺服器的一個 IP。本地 DNS 伺服器收到 IP 資訊後,將會聯絡負責.com 域的這臺伺服器。這臺負責.com 域的伺服器收到請求後,如果自己無法解析,它就會找一個管理.com 域的下一級 DNS 伺服器地址(qq.com)給本地 DNS 伺服器。當本地 DNS 伺服器收到這個地址後,就會找 qq.com 域伺服器,重複上面的動作,進行查詢,直至找到 www.qq.com 主機。
|
||||
|
||||
6. 如果用的是轉發模式,此 DNS 伺服器就會把請求轉發至上一級 DNS 伺服器,由上一級伺服器進行解析,上一級伺服器如果不能解析,或找根 DNS 或把轉請求轉至上上級,以此迴圈。不管本地 DNS 伺服器用的是轉發,還是根提示,最後都是把結果返回給本地 DNS 伺服器,由此 DNS 伺服器再返回給客戶機。
|
||||
6. 如果用的是轉發模式,此 DNS 伺服器就會把請求轉發至上一級 DNS 伺服器,由上一級伺服器進行解析,上一級伺服器如果不能解析,或找根 DNS 或把轉請求轉至上上級,以此迴圈。不管本地 DNS 伺服器用的是轉發,還是根提示,最後都是把結果回傳給本地 DNS 伺服器,由此 DNS 伺服器再回傳給客戶端。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.3 DNS 解析的整個流程
|
||||
|
||||
> 所謂 `遞迴查詢過程` 就是 “查詢的遞交者” 更替, 而 `迭代查詢過程` 則是 “查詢的遞交者”不變。
|
||||
> 所謂 `遞迴查詢過程` 就是 “查詢的提交者” 更替, 而 `迭代查詢過程` 則是 “查詢的提交者”不變。
|
||||
>
|
||||
> 舉個例子來說,你想知道某個一起上法律課的女孩的電話,並且你偷偷拍了她的照片,回到寢室告訴一個很仗義的哥們兒,這個哥們兒二話沒說,拍著胸脯告訴你,甭急,我替你查(此處完成了一次遞迴查詢,即,問詢者的角色更替)。然後他拿著照片問了學院大四學長,學長告訴他,這姑娘是 xx 系的;然後這哥們兒馬不停蹄又問了 xx 系的辦公室主任助理同學,助理同學說是 xx 系 yy 班的,然後很仗義的哥們兒去 xx 系 yy 班的班長那裡取到了該女孩兒電話。(此處完成若干次迭代查詢,即,問詢者角色不變,但反覆更替問詢物件)最後,他把號碼交到了你手裡。完成整個查詢過程。
|
||||
|
||||
@@ -64,17 +64,17 @@ URL(Uniform Resource Locator)是“統一資源定位符”的英文縮寫,用
|
||||
|
||||
## HTTP 協議詳解
|
||||
|
||||
HTTP 協議是 Web 工作的核心,所以要了解清楚 Web 的工作方式就需要詳細的瞭解清楚 HTTP 是怎麼樣工作的。
|
||||
HTTP 協議是 Web 工作的核心,所以要了解清楚 Web 的工作方式就需要詳細的了解清楚 HTTP 是怎麼樣工作的。
|
||||
|
||||
HTTP 是一種讓 Web 伺服器與瀏覽器(客戶端)透過 Internet 傳送與接收資料的協議,它建立在 TCP 協議之上,一般採用 TCP 的 80 埠。它是一個請求、響應協議--客戶端發出一個請求,伺服器響應這個請求。在 HTTP 中,客戶端總是透過建立一個連線與傳送一個 HTTP 請求來發起一個事務。伺服器不能主動去與客戶端聯絡,也不能給客戶端發出一個回呼(Callback)連線。客戶端與伺服器端都可以提前中斷一個連線。例如,當瀏覽器下載一個檔案時,你可以透過點選“停止”鍵來中斷檔案的下載,關閉與伺服器的 HTTP 連線。
|
||||
HTTP 是一種讓 Web 伺服器與瀏覽器(客戶端)透過 Internet 傳送與接收資料的協議,它建立在 TCP 協議之上,一般採用 TCP 的 80 埠。它是一個請求、回應協議--客戶端發出一個請求,伺服器回應這個請求。在 HTTP 中,客戶端總是透過建立一個連線與傳送一個 HTTP 請求來發起一個事務。伺服器不能主動去與客戶端聯絡,也不能給客戶端發出一個回呼(Callback)連線。客戶端與伺服器端都可以提前中斷一個連線。例如,當瀏覽器下載一個檔案時,你可以透過點選“停止”鍵來中斷檔案的下載,關閉與伺服器的 HTTP 連線。
|
||||
|
||||
HTTP 協議是無狀態的,同一個客戶端的這次請求和上次請求是沒有對應關係的,對 HTTP 伺服器來說,它並不知道這兩個請求是否來自同一個客戶端。為了解決這個問題, Web 程式引入了 Cookie 機制來維護連線的可持續狀態。
|
||||
|
||||
>HTTP 協議是建立在 TCP 協議之上的,因此 TCP 攻擊一樣會影響 HTTP 的通訊,例如比較常見的一些攻擊:SYN Flood 是當前最流行的 DoS(拒絕服務攻擊)與 DdoS(分散式拒絕服務攻擊)的方式之一,這是一種利用 TCP 協議缺陷,傳送大量偽造的 TCP 連線請求,從而使得被攻擊方資源耗盡(CPU 滿負荷或記憶體不足)的攻擊方式。
|
||||
|
||||
### HTTP 請求套件(瀏覽器資訊)
|
||||
### HTTP 請求封包(瀏覽器資訊)
|
||||
|
||||
我們先來看看 Request 套件的結構, Request 套件分為 3 部分,第一部分叫 Request line(請求行), 第二部分叫 Request header(請求頭),第三部分是 body(主體)。header 和 body 之間有個空行,請求套件的例子所示:
|
||||
我們先來看看 Request 套件的結構, Request 套件分為 3 部分,第一部分叫 Request line(請求行), 第二部分叫 Request header(請求頭),第三部分是 body(主體)。header 和 body 之間有個空行,請求封包的例子所示:
|
||||
|
||||
GET /domains/example/ HTTP/1.1 //請求行: 請求方法 請求 URI HTTP 協議/協議版本
|
||||
Host:www.iana.org //伺服器端的主機名
|
||||
@@ -84,28 +84,29 @@ HTTP 協議是無狀態的,同一個客戶端的這次請求和上次請求是
|
||||
Accept-Encoding:gzip,deflate,sdch //是否支援流壓縮
|
||||
Accept-Charset:UTF-8,*;q=0.5 //客戶端字元編碼集
|
||||
//空行,用於分割請求頭和訊息體
|
||||
//訊息體,請求資源引數,例如 POST 傳遞的引數
|
||||
//訊息體,請求資源參數,例如 POST 傳遞的參數
|
||||
|
||||
HTTP 協議定義了很多與伺服器互動的請求方法,最基本的有 4 種,分別是 GET,POST,PUT,DELETE。一個 URL 地址用於描述一個網路上的資源,而 HTTP 中的 GET, POST, PUT, DELETE 就對應著對這個資源的查,增,改,刪 4 個操作。我們最常見的就是 GET 和 POST 了。GET 一般用於取得/查詢資源資訊,而 POST 一般用於更新資源資訊。
|
||||
|
||||
透過 fiddler 抓套件可以看到如下請求資訊:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.4 fiddler 抓取的 GET 資訊
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.5 fiddler 抓取的 POST 資訊
|
||||
|
||||
我們看看 GET 和 POST 的區別:
|
||||
|
||||
1. 我們可以看到 GET 請求訊息體為空,POST 請求帶有訊息體。
|
||||
2. GET 提交的資料會放在 URL 之後,以 `?` 分割 URL 和傳輸資料,引數之間以`&`相連,如`EditPosts.aspx?name=test1&id=123456`。POST 方法是把提交的資料放在 HTTP 套件的 body 中。
|
||||
2. GET 提交的資料會放在 URL 之後,以 `?` 分割 URL 和傳輸資料,參數之間以`&`相連,如`EditPosts.aspx?name=test1&id=123456`。POST 方法是把提交的資料放在 HTTP 套件的 body 中。
|
||||
3. GET 提交的資料大小有限制(因為瀏覽器對 URL 的長度有限制),而 POST 方法提交的資料沒有限制。
|
||||
4. GET 方式提交資料,會帶來安全問題,比如一個登入頁面,透過 GET 方式提交資料時,使用者名稱和密碼將出現在 URL 上,如果頁面可以被快取或者其他人可以訪問這臺機器,就可以從歷史記錄獲得該使用者的賬號和密碼。
|
||||
|
||||
### HTTP 響應套件(伺服器資訊)
|
||||
### HTTP 回應內容(伺服器資訊)
|
||||
|
||||
我們再來看看 HTTP 的 response 套件,他的結構如下:
|
||||
|
||||
HTTP/1.1 200 OK //狀態行
|
||||
@@ -120,7 +121,7 @@ HTTP 協議定義了很多與伺服器互動的請求方法,最基本的有 4
|
||||
|
||||
Response 套件中的第一行叫做狀態行,由 HTTP 協議版本號, 狀態碼, 狀態訊息 三部分組成。
|
||||
|
||||
狀態碼用來告訴 HTTP 客戶端,HTTP 伺服器是否產生了預期的 Response。HTTP/1.1 協議中定義了 5 類別狀態碼, 狀態碼由三位數字組成,第一個數字定義了響應的類別
|
||||
狀態碼用來告訴 HTTP 客戶端,HTTP 伺服器是否產生了預期的 Response。HTTP/1.1 協議中定義了 5 類別狀態碼, 狀態碼由三位數字組成,第一個數字定義了回應的類別
|
||||
|
||||
- 1XX 提示資訊 - 表示請求已被成功接收,繼續處理
|
||||
- 2XX 成功 - 表示請求已被成功接收,理解,接受
|
||||
@@ -128,9 +129,9 @@ Response 套件中的第一行叫做狀態行,由 HTTP 協議版本號, 狀
|
||||
- 4XX 客戶端錯誤 - 請求有語法錯誤或請求無法實現
|
||||
- 5XX 伺服器端錯誤 - 伺服器未能實現合法的請求
|
||||
|
||||
我們看下面這個圖展示了詳細的返回資訊,左邊可以看到有很多的資源返回碼,200 是常用的,表示正常資訊,302 表示跳轉。response header 裡面展示了詳細的資訊。
|
||||
我們看下面這個圖展示了詳細的回傳資訊,左邊可以看到有很多的資源回傳碼,200 是常用的,表示正常資訊,302 表示跳轉。response header 裡面展示了詳細的資訊。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.6 訪問一次網站的全部請求資訊
|
||||
|
||||
@@ -143,20 +144,20 @@ HTTP 是一個無狀態的連線導向的協議,無狀態不代表 HTTP 不能
|
||||
|
||||
Keep-Alive 不會永久保持連線,它有一個保持時間,可以在不同伺服器軟體(如 Apache)中設定這個時間。
|
||||
|
||||
## 請求例項
|
||||
## 請求範例
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.7 一次請求的 request 和 response
|
||||
|
||||
上面這張圖我們可以了解到整個的通訊過程,同時細心的讀者是否注意到了一點,一個 URL 請求但是左邊欄裡面為什麼會有那麼多的資源請求(這些都是靜態檔案,go 對於靜態檔案有專門的處理方式)。
|
||||
|
||||
上面這張圖我們可以瞭解到整個的通訊過程,同時細心的讀者是否注意到了一點,一個 URL 請求但是左邊欄裡面為什麼會有那麼多的資源請求(這些都是靜態檔案,go 對於靜態檔案有專門的處理方式)。
|
||||
|
||||
這個就是瀏覽器的一個功能,第一次請求 url,伺服器端返回的是 html 頁面,然後瀏覽器開始渲染 HTML:當解析到 HTML DOM 裡面的圖片連線,css 指令碼和 js 指令碼的連結,瀏覽器就會自動發起一個請求靜態資源的 HTTP 請求,取得相對應的靜態資源,然後瀏覽器就會渲染出來,最終將所有資源整合、渲染,完整展現在我們面前的螢幕上。
|
||||
這個就是瀏覽器的一個功能,第一次請求 url,伺服器端回傳的是 html 頁面,然後瀏覽器開始渲染 HTML:當解析到 HTML DOM 裡面的圖片連線,css 指令碼和 js 指令碼的連結,瀏覽器就會自動發起一個請求靜態資源的 HTTP 請求,取得相對應的靜態資源,然後瀏覽器就會渲染出來,最終將所有資源整合、渲染,完整展現在我們面前的螢幕上。
|
||||
|
||||
>網頁優化方面有一項措施是減少 HTTP 請求次數,就是把儘量多的 css 和 js 資源合併在一起,目的是儘量減少網頁請求靜態資源的次數,提高網頁載入速度,同時減緩伺服器的壓力。
|
||||
|
||||
## links
|
||||
|
||||
* [目錄](<preface.md>)
|
||||
* 上一節: [Web 基礎](<03.0.md>)
|
||||
* 下一節: [Go 建立一個 Web 伺服器](<03.2.md>)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func sayhelloName(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm() //解析引數,預設是不會解析的
|
||||
r.ParseForm() //解析參數,預設是不會解析的
|
||||
fmt.Println(r.Form) //這些資訊是輸出到伺服器端的列印資訊
|
||||
fmt.Println("path", r.URL.Path)
|
||||
fmt.Println("scheme", r.URL.Scheme)
|
||||
@@ -48,7 +48,7 @@ func main() {
|
||||
|
||||
在伺服器端輸出的資訊如下:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.8 使用者訪問 Web 之後伺服器端列印的資訊
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 3.3 Go 如何使得 Web 工作
|
||||
前面小節介紹瞭如何透過 Go 建立一個 Web 服務,我們可以看到簡單應用一個 net/http 套件就方便的建立起來了。那麼 Go 在底層到底是怎麼做的呢?萬變不離其宗,Go 的 Web 服務工作也離不開我們第一小節介紹的 Web 工作方式。
|
||||
前面小節介紹了如何透過 Go 建立一個 Web 服務,我們可以看到簡單應用一個 net/http 套件就方便的建立起來了。那麼 Go 在底層到底是怎麼做的呢?萬變不離其宗,Go 的 Web 服務工作也離不開我們第一小節介紹的 Web 工作方式。
|
||||
|
||||
## web 工作方式的幾個概念
|
||||
|
||||
@@ -11,13 +11,13 @@ Response:伺服器需要反饋給客戶端的資訊
|
||||
|
||||
Conn:使用者的每次請求連結
|
||||
|
||||
Handler:處理請求和產生返回資訊的處理邏輯
|
||||
Handler:處理請求和產生回傳資訊的處理邏輯
|
||||
|
||||
## 分析 http 套件執行機制
|
||||
|
||||
下圖是 Go 實現 Web 服務的工作模式的流程圖
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.9 http 套件執行流程
|
||||
|
||||
@@ -27,13 +27,13 @@ Handler:處理請求和產生返回資訊的處理邏輯
|
||||
|
||||
3. 處理客戶端的請求, 首先從 Client Socket 讀取 HTTP 請求的協議頭, 如果是 POST 方法, 還可能要讀取客戶端提交的資料, 然後交給相應的 handler 處理請求, handler 處理完畢準備好客戶端需要的資料, 透過 Client Socket 寫給客戶端。
|
||||
|
||||
這整個的過程裡面我們只要瞭解清楚下面三個問題,也就知道 Go 是如何讓 Web 執行起來了
|
||||
這整個的過程裡面我們只要了解清楚下面三個問題,也就知道 Go 是如何讓 Web 執行起來了
|
||||
|
||||
- 如何監聽埠?
|
||||
- 如何接收客戶端請求?
|
||||
- 如何分配 handler?
|
||||
|
||||
前面小節的程式碼裡面我們可以看到,Go 是透過一個函式 `ListenAndServe` 來處理這些事情的,這個底層其實這樣處理的:初始化一個 server 物件,然後呼叫了`net.Listen("tcp", addr)`,也就是底層用 TCP 協議建立了一個服務,然後監控我們設定的埠。
|
||||
前面小節的程式碼裡面我們可以看到,Go 是透過一個函式 `ListenAndServe` 來處理這些事情的,這個底層其實這樣處理的:初始化一個 server 物件,然後呼叫了`net.Listen("tcp", addr)`,也就是底層用 TCP 協議建立了一個服務,然後監聽我們設定的埠。
|
||||
|
||||
下面程式碼來自 Go 的 http 套件的原始碼,透過下面的程式碼我們可以看到整個的 http 處理過程:
|
||||
```Go
|
||||
@@ -69,18 +69,18 @@ func (srv *Server) Serve(l net.Listener) error {
|
||||
}
|
||||
|
||||
```
|
||||
監控之後如何接收客戶端的請求呢?上面程式碼執行監控埠之後,呼叫了`srv.Serve(net.Listener)`函式,這個函式就是處理接收客戶端的請求資訊。這個函式裡面起了一個`for{}`,首先透過 Listener 接收請求,其次建立一個 Conn,最後單獨開了一個 goroutine,把這個請求的資料當做引數扔給這個 conn 去服務:`go c.serve()`。這個就是高併發體現了,使用者的每一次請求都是在一個新的 goroutine 去服務,相互不影響。
|
||||
監聽之後如何接收客戶端的請求呢?上面程式碼執行監聽埠之後,呼叫了`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 的資訊反饋到客戶端。
|
||||
那麼如何具體分配到相應的函式來處理請求呢?conn 首先會解析 request:`c.readRequest()`,然後取得相應的 handler:`handler := c.server.Handler`,也就是我們剛才在呼叫函式 `ListenAndServe` 時候的第二個參數,我們前面例子傳遞的是 nil,也就是為空,那麼預設取得`handler = DefaultServeMux`,那麼這個變數用來做什麼的呢?對,這個變數就是一個路由器,它用來匹配 url 跳轉到其相應的 handle 函式,那麼這個我們有設定過嗎 ? 有,我們呼叫的程式碼裡面第一句不是呼叫了`http.HandleFunc("/", sayhelloName)`嘛。這個作用就是註冊了請求`/`的路由規則,當請求 uri 為"/",路由就會轉到函式 sayhelloName,DefaultServeMux 會呼叫 ServeHTTP 方法,這個方法內部其實就是呼叫 sayhelloName 本身,最後透過寫入 response 的資訊反饋到客戶端。
|
||||
|
||||
|
||||
詳細的整個流程如下圖所示:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 3.10 一個 http 連線處理流程
|
||||
|
||||
至此我們的三個問題已經全部得到了解答,你現在對於 Go 如何讓 Web 跑起來的是否已經基本瞭解了呢?
|
||||
至此我們的三個問題已經全部得到了解答,你現在對於 Go 如何讓 Web 跑起來的是否已經基本了解了呢?
|
||||
|
||||
|
||||
## links
|
||||
|
||||
@@ -5,7 +5,7 @@ Go 的 http 有兩個核心功能:Conn、ServeMux
|
||||
|
||||
## Conn 的 goroutine
|
||||
|
||||
與我們一般編寫的 http 伺服器不同, Go 為了實現高併發和高效能, 使用了 goroutines 來處理 Conn 的讀寫事件, 這樣每個請求都能保持獨立,相互不會阻塞,可以高效的響應網路事件。這是 Go 高效的保證。
|
||||
與我們一般編寫的 http 伺服器不同, Go 為了實現高併發和高效能, 使用了 goroutines 來處理 Conn 的讀寫事件, 這樣每個請求都能保持獨立,相互不會阻塞,可以高效的回應網路事件。這是 Go 高效的保證。
|
||||
|
||||
Go 在等待客戶端請求裡面是這樣寫的:
|
||||
```Go
|
||||
@@ -75,7 +75,7 @@ func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
```
|
||||
如上所示路由器接收到請求之後,如果是`*`那麼關閉連結,不然呼叫`mux.Handler(r)`返回對應設定路由的處理 Handler,然後執行`h.ServeHTTP(w, r)`
|
||||
如上所示路由器接收到請求之後,如果是`*`那麼關閉連結,不然呼叫`mux.Handler(r)`回傳對應設定路由的處理 Handler,然後執行`h.ServeHTTP(w, r)`
|
||||
|
||||
也就是呼叫對應路由的 handler 的 ServerHTTP 介面,那麼 mux.Handler(r)怎麼處理的呢?
|
||||
```Go
|
||||
@@ -107,9 +107,9 @@ func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
|
||||
return
|
||||
}
|
||||
```
|
||||
原來他是根據使用者請求的 URL 和路由器裡面儲存的 map 去匹配的,當匹配到之後返回儲存的 handler,呼叫這個 handler 的 ServeHTTP 介面就可以執行到相應的函數了。
|
||||
原來他是根據使用者請求的 URL 和路由器裡面儲存的 map 去匹配的,當匹配到之後回傳儲存的 handler,呼叫這個 handler 的 ServeHTTP 介面就可以執行到相應的函數了。
|
||||
|
||||
透過上面這個介紹,我們瞭解了整個路由過程,Go 其實支援外部實現的路由器 `ListenAndServe`的第二個引數就是用以配置外部路由器的,它是一個 Handler 介面,即外部路由器只要實現了 Handler 介面就可以,我們可以在自己實現的路由器的 ServeHTTP 裡面實現自訂路由功能。
|
||||
透過上面這個介紹,我們了解了整個路由過程,Go 其實支援外部實現的路由器 `ListenAndServe`的第二個參數就是用以配置外部路由器的,它是一個 Handler 介面,即外部路由器只要實現了 Handler 介面就可以,我們可以在自己實現的路由器的 ServeHTTP 裡面實現自訂路由功能。
|
||||
|
||||
如下程式碼所示,我們自己實現了一個簡易的路由器
|
||||
```Go
|
||||
@@ -162,8 +162,7 @@ func main() {
|
||||
|
||||
按順序做了幾件事情:
|
||||
|
||||
1 例項化 Server
|
||||
|
||||
1 實體化 Server
|
||||
|
||||
2 呼叫 Server 的 ListenAndServe()
|
||||
|
||||
@@ -171,7 +170,7 @@ func main() {
|
||||
|
||||
4 啟動一個 for 迴圈,在迴圈體中 Accept 請求
|
||||
|
||||
5 對每個請求例項化一個 Conn,並且開啟一個 goroutine 為這個請求進行服務 go c.serve()
|
||||
5 對每個請求實體化一個 Conn,並且開啟一個 goroutine 為這個請求進行服務 go c.serve()
|
||||
|
||||
6 讀取每個請求的內容 w, err := c.readRequest()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 3.5 小結
|
||||
這一章我們介紹了 HTTP 協議, DNS 解析的過程, 如何用 go 實現一個簡陋的 web server。並深入到 net/http 套件的原始碼中為大家揭開實現此 server 的祕密。
|
||||
|
||||
希望透過這一章的學習,你能夠對 Go 開發 Web 有了初步的瞭解,我們也看到相應的程式碼了,Go 開發 Web 應用是很方便的,同時又是相當的靈活。
|
||||
希望透過這一章的學習,你能夠對 Go 開發 Web 有了初步的了解,我們也看到相應的程式碼了,Go 開發 Web 應用是很方便的,同時又是相當的靈活。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -12,12 +12,12 @@
|
||||
|
||||
Go 裡面對於 form 處理已經有很方便的方法了,在 Request 裡面有專門的 form 處理,可以很方便的整合到 Web 開發裡面來,4.1 小節裡面將講解 Go 如何處理表單的輸入。由於不能信任任何使用者的輸入,所以我們需要對這些輸入進行有效性驗證,4.2 小節將就如何進行一些普通的驗證進行詳細的示範。
|
||||
|
||||
HTTP 協議是一種無狀態的協議,那麼如何才能辨別是否是同一個使用者呢?同時又如何保證一個表單不出現多次遞交的情況呢?4.3 和 4.4 小節裡面將對 cookie(cookie 是儲存在客戶端的資訊,能夠每次透過 header 和伺服器進行互動的資料)等進行詳細講解。
|
||||
HTTP 協議是一種無狀態的協議,那麼如何才能辨別是否是同一個使用者呢?同時又如何保證一個表單不出現多次提交的情況呢?4.3 和 4.4 小節裡面將對 cookie(cookie 是儲存在客戶端的資訊,能夠每次透過 header 和伺服器進行互動的資料)等進行詳細講解。
|
||||
|
||||
表單還有一個很大的功能就是能夠上傳檔案,那麼 Go 是如何處理檔案上傳的呢?針對大檔案上傳我們如何有效的處理呢?4.5 小節我們將一起學習 Go 處理檔案上傳的知識。
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 4.1 處理表單的輸入
|
||||
|
||||
先來看一個表單遞交的例子,我們有如下的表單內容,命名成檔案 login.gtpl(放入當前新建專案的目錄裡面)
|
||||
先來看一個表單提交的例子,我們有如下的表單內容,命名成檔案 login.gtpl(放入當前建立專案的目錄裡面)
|
||||
```html
|
||||
|
||||
<html>
|
||||
@@ -16,7 +16,7 @@
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
上面遞交表單到伺服器的`/login`,當用戶輸入資訊點選登入之後,會跳轉到伺服器的路由 `login` 裡面,我們首先要判斷這個是什麼方式傳遞過來,POST 還是 GET 呢?
|
||||
上面提交表單到伺服器的`/login`,當用戶輸入資訊點選登入之後,會跳轉到伺服器的路由 `login` 裡面,我們首先要判斷這個是什麼方式傳遞過來,POST 還是 GET 呢?
|
||||
|
||||
http 套件裡面有一個很簡單的方式就可以取得,我們在前面 web 的例子的基礎上來看看怎麼處理 login 頁面的 form 資料
|
||||
```Go
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
)
|
||||
|
||||
func sayhelloName(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm() //解析 url 傳遞的引數,對於 POST 則解析響應套件的主體(request body)
|
||||
r.ParseForm() //解析 url 傳遞的參數,對於 POST 則解析 HTTP 回應內容的主體(request body)
|
||||
//注意 : 如果沒有呼叫 ParseForm 方法,下面無法取得表單的資料
|
||||
fmt.Println(r.Form) //這些資訊是輸出到伺服器端的列印資訊
|
||||
fmt.Println("path", r.URL.Path)
|
||||
@@ -67,25 +67,25 @@ func main() {
|
||||
}
|
||||
|
||||
```
|
||||
透過上面的程式碼我們可以看出取得請求方法是透過`r.Method`來完成的,這是個字串型別的變數,返回 GET, POST, PUT 等 method 資訊。
|
||||
透過上面的程式碼我們可以看出取得請求方法是透過`r.Method`來完成的,這是個字串型別的變數,回傳 GET, POST, PUT 等 method 資訊。
|
||||
|
||||
login 函式中我們根據`r.Method`來判斷是顯示登入介面還是處理登入邏輯。當 GET 方式請求時顯示登入介面,其他方式請求時則處理登入邏輯,如查詢資料庫、驗證登入資訊等。
|
||||
|
||||
當我們在瀏覽器裡面開啟`http://127.0.0.1:9090/login`的時候,出現如下介面
|
||||
|
||||

|
||||

|
||||
|
||||
如果你看到一個空頁面,可能是你寫的 login.gtpl 檔案中有錯誤,請根據控制檯中的日誌進行修復。
|
||||
|
||||
圖 4.1 使用者登入介面
|
||||
|
||||
我們輸入使用者名稱和密碼之後發現在伺服器端是不會打印出來任何輸出的,為什麼呢?預設情況下,Handler 裡面是不會自動解析 form 的,必須顯式的呼叫`r.ParseForm()`後,你才能對這個表單資料進行操作。我們修改一下程式碼,在`fmt.Println("username:", r.Form["username"])`之前加一行`r.ParseForm()`,重新編譯,再次測試輸入遞交,現在是不是在伺服器端有輸出你的輸入的使用者名稱和密碼了。
|
||||
我們輸入使用者名稱和密碼之後發現在伺服器端是不會顯示出來任何輸出的,為什麼呢?預設情況下,Handler 裡面是不會自動解析 form 的,必須明確的呼叫`r.ParseForm()`後,你才能對這個表單資料進行操作。我們修改一下程式碼,在`fmt.Println("username:", r.Form["username"])`之前加一行`r.ParseForm()`,重新編譯,再次測試輸入提交,現在是不是在伺服器端有輸出你的輸入的使用者名稱和密碼了。
|
||||
|
||||
`r.Form`裡面包含了所有請求的引數,比如 URL 中 query-string、POST 的資料、PUT 的資料,所以當你在 URL 中的 query-string 欄位和 POST 衝突時,會儲存成一個 slice,裡面儲存了多個值,Go 官方文件中說在接下來的版本里面將會把 POST、GET 這些資料分離開來。
|
||||
`r.Form`裡面包含了所有請求的參數,比如 URL 中 query-string、POST 的資料、PUT 的資料,所以當你在 URL 中的 query-string 欄位和 POST 衝突時,會儲存成一個 slice,裡面儲存了多個值,Go 官方文件中說在接下來的版本里面將會把 POST、GET 這些資料分離開來。
|
||||
|
||||
現在我們修改一下 login.gtpl 裡面 form 的 action 值`http://127.0.0.1:9090/login`修改為`http://127.0.0.1:9090/login?username=astaxie`,再次測試,伺服器的輸出 username 是不是一個 slice。伺服器端的輸出如下:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 4.2 伺服器端列印接收到的資訊
|
||||
|
||||
@@ -104,7 +104,7 @@ fmt.Println(v["friend"])
|
||||
|
||||
```
|
||||
>**Tips**:
|
||||
>Request 本身也提供了 FormValue()函式來取得使用者提交的引數。如 r.Form["username"]也可寫成 r.FormValue("username")。呼叫 r.FormValue 時會自動呼叫 r.ParseForm,所以不必提前呼叫。r.FormValue 只會返回同名引數中的第一個,若引數不存在則返回空字串。
|
||||
>Request 本身也提供了 FormValue()函式來取得使用者提交的參數。如 r.Form["username"]也可寫成 r.FormValue("username")。呼叫 r.FormValue 時會自動呼叫 r.ParseForm,所以不必提前呼叫。r.FormValue 只會回傳同名參數中的第一個,若參數不存在則回傳空字串。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -172,7 +172,7 @@ if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m
|
||||
}
|
||||
|
||||
```
|
||||
上面列出了我們一些常用的伺服器端的表單元素驗證,希望透過這個引匯入門,能夠讓你對 Go 的資料驗證有所瞭解,特別是 Go 裡面的正則處理。
|
||||
上面列出了我們一些常用的伺服器端的表單元素驗證,希望透過這個引匯入門,能夠讓你對 Go 的資料驗證有所了解,特別是 Go 裡面的正則處理。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
那麼 Go 裡面是怎麼做這個有效防護的呢?Go 的 html/template 裡面帶有下面幾個函式可以幫你轉義
|
||||
|
||||
- func HTMLEscape(w io.Writer, b []byte) //把 b 進行轉義之後寫到 w
|
||||
- func HTMLEscapeString(s string) string //轉義 s 之後返回結果字串
|
||||
- func HTMLEscaper(args ...interface{}) string //支援多個引數一起轉義,返回結果字串
|
||||
- func HTMLEscapeString(s string) string //轉義 s 之後回傳結果字串
|
||||
- func HTMLEscaper(args ...interface{}) string //支援多個參數一起轉義,回傳結果字串
|
||||
|
||||
|
||||
我們看 4.1 小節的例子
|
||||
@@ -22,7 +22,7 @@ template.HTMLEscape(w, []byte(r.Form.Get("username"))) //輸出到客戶端
|
||||
```
|
||||
如果我們輸入的 username 是`<script>alert()</script>`,那麼我們可以在瀏覽器上面看到輸出如下所示:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 4.3 Javascript 過濾之後的輸出
|
||||
|
||||
@@ -69,4 +69,4 @@ err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一節: [驗證的輸入](<04.2.md>)
|
||||
* 下一節: [防止多次遞交表單](<04.4.md>)
|
||||
* 下一節: [防止多次提交表單](<04.4.md>)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# 4.4 防止多次遞交表單
|
||||
# 4.4 防止多次提交表單
|
||||
|
||||
不知道你是否曾經看到過一個論壇或者部落格,在一個帖子或者文章後面出現多條重複的記錄,這些大多數是因為使用者重複遞交了留言的表單引起的。由於種種原因,使用者經常會重複遞交表單。通常這只是滑鼠的誤操作,如雙擊了遞交按鈕,也可能是為了編輯或者再次核對填寫過的資訊,點選了瀏覽器的後退按鈕,然後又再次點選了遞交按鈕而不是瀏覽器的前進按鈕。當然,也可能是故意的——比如,在某項線上調查或者博彩活動中重複投票。那我們如何有效的防止使用者多次遞交相同的表單呢?
|
||||
不知道你是否曾經看到過一個論壇或者部落格,在一個帖子或者文章後面出現多條重複的記錄,這些大多數是因為使用者重複提交了留言的表單引起的。由於種種原因,使用者經常會重複提交表單。通常這只是滑鼠的誤操作,如雙擊了提交按鈕,也可能是為了編輯或者再次核對填寫過的資訊,點選了瀏覽器的後退按鈕,然後又再次點選了提交按鈕而不是瀏覽器的前進按鈕。當然,也可能是故意的——比如,在某項線上調查或者博彩活動中重複投票。那我們如何有效的防止使用者多次提交相同的表單呢?
|
||||
|
||||
解決方案是在表單中新增一個帶有唯一值的隱藏欄位。在驗證表單時,先檢查帶有該唯一值的表單是否已經遞交過了。如果是,拒絕再次遞交;如果不是,則處理表單進行邏輯處理。另外,如果是採用了 Ajax 模式遞交表單的話,當表單遞交後,透過 javascript 來禁用表單的遞交按鈕。
|
||||
解決方案是在表單中新增一個帶有唯一值的隱藏欄位。在驗證表單時,先檢查帶有該唯一值的表單是否已經提交過了。如果是,拒絕再次提交;如果不是,則處理表單進行邏輯處理。另外,如果是採用了 Ajax 模式提交表單的話,當表單提交後,透過 javascript 來禁用表單的提交按鈕。
|
||||
|
||||
我繼續拿 4.2 小節的例子優化:
|
||||
```html
|
||||
@@ -15,7 +15,7 @@
|
||||
<input type="hidden" name="token" value="{{.}}">
|
||||
<input type="submit" value="登陸">
|
||||
```
|
||||
我們在模版裡面增加了一個隱藏欄位`token`,這個值我們透過 MD5(時間戳)來取得唯一值,然後我們把這個值儲存到伺服器端(session 來控制,我們將在第六章講解如何儲存),以方便表單提交時比對判定。
|
||||
我們在模版裡面增加了一個隱藏欄位`token`,這個值我們透過 MD5(時戳) 來取得唯一值,然後我們把這個值儲存到伺服器端(session 來控制,我們將在第六章講解如何儲存),以方便表單提交時比對判定。
|
||||
```Go
|
||||
|
||||
func login(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -46,11 +46,11 @@ func login(w http.ResponseWriter, r *http.Request) {
|
||||
```
|
||||
上面的程式碼輸出到頁面的原始碼如下:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 4.4 增加 token 之後在客戶端輸出的原始碼資訊
|
||||
|
||||
我們看到 token 已經有輸出值,你可以不斷的重新整理,可以看到這個值在不斷的變化。這樣就保證了每次顯示 form 表單的時候都是唯一的,使用者遞交的表單保持了唯一性。
|
||||
我們看到 token 已經有輸出值,你可以不斷的重新整理,可以看到這個值在不斷的變化。這樣就保證了每次顯示 form 表單的時候都是唯一的,使用者提交的表單保持了唯一性。
|
||||
|
||||
我們的解決方案可以防止非惡意的攻擊,並能使惡意使用者暫時不知所措,然後,它卻不能排除所有的欺騙性的動機,對此類別情況還需要更復雜的工作。
|
||||
|
||||
|
||||
@@ -59,34 +59,34 @@ func upload(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
```
|
||||
透過上面的程式碼可以看到,處理檔案上傳我們需要呼叫`r.ParseMultipartForm`,裡面的引數表示`maxMemory`,呼叫 `ParseMultipartForm` 之後,上傳的檔案儲存在 `maxMemory` 大小的記憶體裡面,如果檔案大小超過了`maxMemory`,那麼剩下的部分將儲存在系統的臨時檔案中。我們可以透過`r.FormFile`取得上面的檔案控制代碼,然後例項中使用了`io.Copy`來儲存檔案。
|
||||
透過上面的程式碼可以看到,處理檔案上傳我們需要呼叫 `r.ParseMultipartForm`,裡面的參數表示 `maxMemory`,呼叫 `ParseMultipartForm` 之後,上傳的檔案儲存在 `maxMemory` 大小的記憶體裡面,如果檔案大小超過了 `maxMemory`,那麼剩下的部分將儲存在系統的臨時檔案中。我們可以透過 `r.FormFile` 取得上面的檔案控制代碼,然後範例中使用了 `io.Copy` 來儲存檔案。
|
||||
|
||||
>取得其他非檔案欄位資訊的時候就不需要呼叫`r.ParseForm`,因為在需要的時候 Go 自動會去呼叫。而且 `ParseMultipartForm` 呼叫一次之後,後面再次呼叫不會再有效果。
|
||||
>取得其他非檔案欄位資訊的時候就不需要呼叫 `r.ParseForm`,因為在需要的時候 Go 自動會去呼叫。而且 `ParseMultipartForm` 呼叫一次之後,後面再次呼叫不會再有效果。
|
||||
|
||||
透過上面的例項我們可以看到我們上傳檔案主要三步處理:
|
||||
透過上面的範例我們可以看到我們上傳檔案主要三步處理:
|
||||
|
||||
1. 表單中增加 enctype="multipart/form-data"
|
||||
2. 伺服器端呼叫`r.ParseMultipartForm`,把上傳的檔案儲存在記憶體和臨時檔案中
|
||||
3. 使用`r.FormFile`取得檔案控制代碼,然後對檔案進行儲存等處理。
|
||||
2. 伺服器端呼叫 `r.ParseMultipartForm`,把上傳的檔案儲存在記憶體和臨時檔案中
|
||||
3. 使用 `r.FormFile` 取得檔案控制代碼,然後對檔案進行儲存等處理。
|
||||
|
||||
檔案 handler 是 multipart.FileHeader,裡面儲存了如下結構資訊
|
||||
|
||||
檔案 handler 是 multipart.FileHeader,裡面儲存瞭如下結構資訊
|
||||
```Go
|
||||
|
||||
type FileHeader struct {
|
||||
Filename string
|
||||
Header textproto.MIMEHeader
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
我們透過上面的例項程式碼打印出來上傳檔案的資訊如下
|
||||
我們透過上面的範例程式碼顯示出來上傳檔案的資訊如下
|
||||
|
||||

|
||||

|
||||
|
||||
圖 4.5 列印檔案上傳後伺服器端接受的資訊
|
||||
|
||||
## 客戶端上傳檔案
|
||||
|
||||
我們上面的例子示範瞭如何透過表單上傳檔案,然後在伺服器端處理檔案,其實 Go 支援模擬客戶端表單功能支援檔案上傳,詳細用法請看如下範例:
|
||||
我們上面的例子示範了如何透過表單上傳檔案,然後在伺服器端處理檔案,其實 Go 支援模擬客戶端表單功能支援檔案上傳,詳細用法請看如下範例:
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -157,5 +157,5 @@ func main() {
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一節: [防止多次遞交表單](<04.4.md>)
|
||||
* 上一節: [防止多次提交表單](<04.4.md>)
|
||||
* 下一節: [小結](<04.6.md>)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 4.6 小結
|
||||
這一章裡面我們學習了 Go 如何處理表單資訊,我們透過使用者登入、上傳檔案的例子展示了 Go 處理 form 表單資訊及上傳檔案的手段。但是在處理表單過程中我們需要驗證使用者輸入的資訊,考慮到網站安全的重要性,資料過濾就顯得相當重要了,因此後面的章節中專門寫了一個小節來講解了不同方面的資料過濾,順帶講一下 Go 對字串的正則處理。
|
||||
|
||||
透過這一章能夠讓你瞭解客戶端和伺服器端是如何進行資料上的互動,客戶端將資料傳遞給伺服器系統,伺服器接受資料又把處理結果反饋給客戶端。
|
||||
透過這一章能夠讓你了解客戶端和伺服器端是如何進行資料上的互動,客戶端將資料傳遞給伺服器系統,伺服器接受資料又把處理結果反饋給客戶端。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -8,7 +8,7 @@ Go 沒有內建的驅動支援任何的資料庫,但是 Go 定義了 database/
|
||||
>[Go database/sql tutorial](http://go-database-sql.org/) 裡提供了慣用的範例及詳細的說明。
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -41,14 +41,14 @@ drivers[name] = driver
|
||||
>我們在 2.3 節流程和函式一節中介紹過 init 函式的初始化過程,套件在引入的時候會自動呼叫套件的 init 函式以完成對套件的初始化。因此,我們引入上面的資料庫驅動套件之後會自動去呼叫 init 函式,然後在 init 函式裡面註冊這個資料庫驅動,這樣我們就可以在接下來的程式碼中直接使用這個資料庫驅動了。
|
||||
|
||||
## driver.Driver
|
||||
Driver 是一個數據函式庫驅動的介面,他定義了一個 method: Open(name string),這個方法返回一個數據函式庫的 Conn 介面。
|
||||
Driver 是一個數據函式庫驅動的介面,他定義了一個 method: Open(name string),這個方法回傳一個數據函式庫的 Conn 介面。
|
||||
```Go
|
||||
|
||||
type Driver interface {
|
||||
Open(name string) (Conn, error)
|
||||
}
|
||||
```
|
||||
返回的 Conn 只能用來進行一次 goroutine 的操作,也就是說不能把這個 Conn 應用於 Go 的多個 goroutine 裡面。如下程式碼會出現錯誤
|
||||
回傳的 Conn 只能用來進行一次 goroutine 的操作,也就是說不能把這個 Conn 應用於 Go 的多個 goroutine 裡面。如下程式碼會出現錯誤
|
||||
```Go
|
||||
|
||||
...
|
||||
@@ -56,9 +56,9 @@ go goroutineA (Conn) //執行查詢操作
|
||||
go goroutineB (Conn) //執行插入操作
|
||||
...
|
||||
```
|
||||
上面這樣的程式碼可能會使 Go 不知道某個操作究竟是由哪個 goroutine 發起的,從而導致資料混亂,比如可能會把 goroutineA 裡面執行的查詢操作的結果返回給 goroutineB 從而使 B 錯誤地把此結果當成自己執行的插入資料。
|
||||
上面這樣的程式碼可能會使 Go 不知道某個操作究竟是由哪個 goroutine 發起的,從而導致資料混亂,比如可能會把 goroutineA 裡面執行的查詢操作的結果回傳給 goroutineB 從而使 B 錯誤地把此結果當成自己執行的插入資料。
|
||||
|
||||
第三方驅動都會定義這個函式,它會解析 name 引數來取得相關資料庫的連線資訊,解析完成後,它將使用此資訊來初始化一個 Conn 並返回它。
|
||||
第三方驅動都會定義這個函式,它會解析 name 參數來取得相關資料庫的連線資訊,解析完成後,它將使用此資訊來初始化一個 Conn 並回傳它。
|
||||
|
||||
## driver.Conn
|
||||
Conn 是一個數據函式庫連線的介面定義,他定義了一系列方法,這個 Conn 只能應用在一個 goroutine 裡面,不能使用在多個 goroutine 裡面,詳情請參考上面的說明。
|
||||
@@ -70,11 +70,11 @@ type Conn interface {
|
||||
Begin() (Tx, error)
|
||||
}
|
||||
```
|
||||
Prepare 函式返回與當前連線相關的執行 Sql 語句的準備狀態,可以進行查詢、刪除等操作。
|
||||
Prepare 函式回傳與當前連線相關的執行 Sql 語句的準備狀態,可以進行查詢、刪除等操作。
|
||||
|
||||
Close 函式關閉當前的連線,執行釋放連線擁有的資源等清理工作。因為驅動實現了 database/sql 裡面建議的 conn pool,所以你不用再去實現快取 conn 之類別的,這樣會容易引起問題。
|
||||
|
||||
Begin 函式返回一個代表事務處理的 Tx,透過它你可以進行查詢,更新等操作,或者對事務進行回滾、遞交。
|
||||
Begin 函式回傳一個代表交易處理的 Tx,透過它你可以進行查詢,更新等操作,或者對交易進行回復 (Rollback)、提交。
|
||||
|
||||
## driver.Stmt
|
||||
Stmt 是一種準備好的狀態,和 Conn 相關聯,而且只能應用於一個 goroutine 中,不能應用於多個 goroutine。
|
||||
@@ -87,17 +87,17 @@ type Stmt interface {
|
||||
Query(args []Value) (Rows, error)
|
||||
}
|
||||
```
|
||||
Close 函式關閉當前的連結狀態,但是如果當前正在執行 query,query 還是有效返回 rows 資料。
|
||||
Close 函式關閉當前的連結狀態,但是如果當前正在執行 query,query 還是有效回傳 rows 資料。
|
||||
|
||||
NumInput 函式返回當前預留引數的個數,當返回 >=0 時資料庫驅動就會智慧檢查呼叫者的引數。當資料庫驅動套件不知道預留引數的時候,返回-1。
|
||||
NumInput 函式回傳當前預留參數的個數,當回傳 >=0 時資料庫驅動就會智慧檢查呼叫者的參數。當資料庫驅動套件不知道預留參數的時候,回傳-1。
|
||||
|
||||
Exec 函式執行 Prepare 準備好的 sql,傳入引數執行 update/insert 等操作,返回 Result 資料
|
||||
Exec 函式執行 Prepare 準備好的 sql,傳入參數執行 update/insert 等操作,回傳 Result 資料
|
||||
|
||||
Query 函式執行 Prepare 準備好的 sql,傳入需要的引數執行 select 操作,返回 Rows 結果集
|
||||
Query 函式執行 Prepare 準備好的 sql,傳入需要的參數執行 select 操作,回傳 Rows 結果集
|
||||
|
||||
|
||||
## driver.Tx
|
||||
事務處理一般就兩個過程,遞交或者回滾。資料庫驅動裡面也只需要實現這兩個函式就可以
|
||||
交易處理一般就兩個過程,提交或者回復 (Rollback)。資料庫驅動裡面也只需要實現這兩個函式就可以
|
||||
```Go
|
||||
|
||||
type Tx interface {
|
||||
@@ -105,7 +105,7 @@ type Tx interface {
|
||||
Rollback() error
|
||||
}
|
||||
```
|
||||
這兩個函式一個用來遞交一個事務,一個用來回滾事務。
|
||||
這兩個函式一個用來提交一個交易,一個用來回復 (Rollback)交易。
|
||||
|
||||
## driver.Execer
|
||||
這是一個 Conn 可選擇實現的介面
|
||||
@@ -115,10 +115,10 @@ type Execer interface {
|
||||
Exec(query string, args []Value) (Result, error)
|
||||
}
|
||||
```
|
||||
如果這個介面沒有定義,那麼在呼叫 DB.Exec,就會首先呼叫 Prepare 返回 Stmt,然後執行 Stmt 的 Exec,然後關閉 Stmt。
|
||||
如果這個介面沒有定義,那麼在呼叫 DB.Exec,就會首先呼叫 Prepare 回傳 Stmt,然後執行 Stmt 的 Exec,然後關閉 Stmt。
|
||||
|
||||
## driver.Result
|
||||
這個是執行 Update/Insert 等操作返回的結果介面定義
|
||||
這個是執行 Update/Insert 等操作回傳的結果介面定義
|
||||
```Go
|
||||
|
||||
type Result interface {
|
||||
@@ -126,12 +126,12 @@ type Result interface {
|
||||
RowsAffected() (int64, error)
|
||||
}
|
||||
```
|
||||
LastInsertId 函式返回由資料庫執行插入操作得到的自增 ID 號。
|
||||
LastInsertId 函式回傳由資料庫執行插入操作得到的自增 ID 號。
|
||||
|
||||
RowsAffected 函式返回 query 操作影響的資料條目數。
|
||||
RowsAffected 函式回傳 query 操作影響的資料條目數。
|
||||
|
||||
## driver.Rows
|
||||
Rows 是執行查詢返回的結果集介面定義
|
||||
Rows 是執行查詢回傳的結果集介面定義
|
||||
```Go
|
||||
|
||||
type Rows interface {
|
||||
@@ -140,11 +140,11 @@ type Rows interface {
|
||||
Next(dest []Value) error
|
||||
}
|
||||
```
|
||||
Columns 函式返回查詢資料庫表的欄位資訊,這個返回的 slice 和 sql 查詢的欄位一一對應,而不是返回整個表的所有欄位。
|
||||
Columns 函式回傳查詢資料庫表的欄位資訊,這個回傳的 slice 和 sql 查詢的欄位一一對應,而不是回傳整個表的所有欄位。
|
||||
|
||||
Close 函式用來關閉 Rows 迭代器。
|
||||
|
||||
Next 函式用來返回下一條資料,把資料賦值給 dest。dest 裡面的元素必須是 driver.Value 的值除了 string,返回的資料裡面所有的 string 都必須要轉換成[]byte。如果最後沒資料了,Next 函式最後返回 io.EOF。
|
||||
Next 函式用來回傳下一條資料,把資料賦值給 dest。dest 裡面的元素必須是 driver.Value 的值除了 string,回傳的資料裡面所有的 string 都必須要轉換成[]byte。如果最後沒資料了,Next 函式最後回傳 io.EOF。
|
||||
|
||||
|
||||
## driver.RowsAffected
|
||||
@@ -170,11 +170,11 @@ int64
|
||||
float64
|
||||
bool
|
||||
[]byte
|
||||
string [*]除了 Rows.Next 返回的不能是 string.
|
||||
string [*]除了 Rows.Next 回傳的不能是 string.
|
||||
time.Time
|
||||
```
|
||||
## driver.ValueConverter
|
||||
ValueConverter 介面定義瞭如何把一個普通的值轉化成 driver.Value 的介面
|
||||
ValueConverter 介面定義了如何把一個普通的值轉化成 driver.Value 的介面
|
||||
```Go
|
||||
|
||||
type ValueConverter interface {
|
||||
@@ -188,7 +188,7 @@ type ValueConverter interface {
|
||||
- 在 scan 函式裡面如何把 driver.Value 值轉化成使用者定義的值
|
||||
|
||||
## driver.Valuer
|
||||
Valuer 介面定義了返回一個 driver.Value 的方式
|
||||
Valuer 介面定義了回傳一個 driver.Value 的方式
|
||||
```Go
|
||||
|
||||
type Valuer interface {
|
||||
@@ -197,7 +197,7 @@ type Valuer interface {
|
||||
```
|
||||
很多型別都實現了這個 Value 方法,用來自身與 driver.Value 的轉化。
|
||||
|
||||
透過上面的講解,你應該對於驅動的開發有了一個基本的瞭解,一個驅動只要實現了這些介面就能完成增刪查改等基本操作了,剩下的就是與相應的資料庫進行資料互動等細節問題了,在此不再贅述。
|
||||
透過上面的講解,你應該對於驅動的開發有了一個基本的了解,一個驅動只要實現了這些介面就能完成增刪查改等基本操作了,剩下的就是與相應的資料庫進行資料互動等細節問題了,在此不再贅述。
|
||||
|
||||
## database/sql
|
||||
database/sql 在 database/sql/driver 提供的介面基礎上定義了一些更高階的方法,用以簡化資料庫操作,同時內部還建議性地實現一個 conn pool。
|
||||
@@ -211,7 +211,7 @@ type DB struct {
|
||||
closed bool
|
||||
}
|
||||
```
|
||||
我們可以看到 Open 函式返回的是 DB 物件,裡面有一個 freeConn,它就是那個簡易的連線池。它的實現相當簡單或者說簡陋,就是當執行`db.prepare` -> `db.prepareDC`的時候會`defer dc.releaseConn`,然後呼叫`db.putConn`,也就是把這個連線放入連線池,每次呼叫`db.conn`的時候會先判斷 freeConn 的長度是否大於 0,大於 0 說明有可以複用的 conn,直接拿出來用就是了,如果不大於 0,則建立一個 conn,然後再返回之。
|
||||
我們可以看到 Open 函式回傳的是 DB 物件,裡面有一個 freeConn,它就是那個簡易的連線池。它的實現相當簡單或者說簡陋,就是當執行`db.prepare` -> `db.prepareDC`的時候會`defer dc.releaseConn`,然後呼叫`db.putConn`,也就是把這個連線放入連線池,每次呼叫`db.conn`的時候會先判斷 freeConn 的長度是否大於 0,大於 0 說明有可以複用的 conn,直接拿出來用就是了,如果不大於 0,則建立一個 conn,然後再回傳之。
|
||||
|
||||
|
||||
## links
|
||||
|
||||
@@ -118,20 +118,20 @@ func checkErr(err error) {
|
||||
|
||||
關鍵的幾個函式我解釋一下:
|
||||
|
||||
sql.Open()函式用來開啟一個註冊過的資料庫驅動,go-sql-driver 中註冊了 mysql 這個資料庫驅動,第二個引數是 DSN(Data Source Name),它是 go-sql-driver 定義的一些資料庫連結和配置資訊。它支援如下格式:
|
||||
sql.Open()函式用來開啟一個註冊過的資料庫驅動,go-sql-driver 中註冊了 mysql 這個資料庫驅動,第二個參數是 DSN(Data Source Name),它是 go-sql-driver 定義的一些資料庫連結和配置資訊。它支援如下格式:
|
||||
|
||||
user@unix(/path/to/socket)/dbname?charset=utf8
|
||||
user:password@tcp(localhost:5555)/dbname?charset=utf8
|
||||
user:password@/dbname
|
||||
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname
|
||||
|
||||
db.Prepare()函式用來返回準備要執行的 sql 操作,然後返回準備完畢的執行狀態。
|
||||
db.Prepare()函式用來回傳準備要執行的 sql 操作,然後回傳準備完畢的執行狀態。
|
||||
|
||||
db.Query()函式用來直接執行 Sql 返回 Rows 結果。
|
||||
db.Query()函式用來直接執行 Sql 回傳 Rows 結果。
|
||||
|
||||
stmt.Exec()函式用來執行 stmt 準備好的 SQL 語句
|
||||
|
||||
我們可以看到我們傳入的引數都是 =? 對應的資料,這樣做的方式可以一定程度上防止 SQL 注入。
|
||||
我們可以看到我們傳入的參數都是 =? 對應的資料,這樣做的方式可以一定程度上防止 SQL 注入。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 5.3 使用 SQLite 資料庫
|
||||
|
||||
SQLite 是一個開源的嵌入式關係資料庫,實現自套件容、零配置、支援事務的 SQL 資料庫引擎。其特點是高度便攜、使用方便、結構緊湊、高效、可靠。 與其他資料庫管理系統不同,SQLite 的安裝和執行非常簡單,在大多數情況下,只要確保 SQLite 的二進位制檔案存在即可開始建立、連線和使用資料庫。如果您正在尋找一個嵌入式資料庫專案或解決方案,SQLite 是絕對值得考慮。SQLite 可以說是開源的 Access。
|
||||
SQLite 是一個開源的嵌入式關聯式資料庫,實現自套件容、零配置、支援事務的 SQL 資料庫引擎。其特點是高度便攜、使用方便、結構緊湊、高效、可靠。 與其他資料庫管理系統不同,SQLite 的安裝和執行非常簡單,在大多數情況下,只要確保 SQLite 的二進位制檔案存在即可開始建立、連線和使用資料庫。如果您正在尋找一個嵌入式資料庫專案或解決方案,SQLite 是絕對值得考慮。SQLite 可以說是開源的 Access。
|
||||
|
||||
## 驅動
|
||||
Go 支援 sqlite 的驅動也比較多,但是好多都是不支援 database/sql 介面的
|
||||
@@ -11,7 +11,7 @@ Go 支援 sqlite 的驅動也比較多,但是好多都是不支援 database/sq
|
||||
|
||||
目前支援 database/sql 的 SQLite 資料庫驅動只有第一個,我目前也是採用它來開發專案的。採用標準介面有利於以後出現更好的驅動的時候做遷移。
|
||||
|
||||
## 例項程式碼
|
||||
## 範例程式碼
|
||||
範例的資料庫表結構如下所示,相應的建表 SQL:
|
||||
```sql
|
||||
|
||||
@@ -114,7 +114,7 @@ func checkErr(err error) {
|
||||
|
||||
>sqlite 管理工具:http://sqliteadmin.orbmu2k.de/
|
||||
|
||||
>可以方便的新建資料庫管理。
|
||||
>可以方便的建立資料庫管理。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 5.4 使用 PostgreSQL 資料庫
|
||||
|
||||
PostgreSQL 是一個自由的物件-關係資料庫伺服器(資料庫管理系統),它在靈活的 BSD-風格許可證下發行。它提供了相對其他開放原始碼資料庫系統(比如 MySQL 和 Firebird),和對專有系統比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server 的一種選擇。
|
||||
PostgreSQL 是一個自由的物件-關聯式資料庫伺服器(資料庫管理系統),它在靈活的 BSD-風格許可證下發行。它提供了相對其他開放原始碼資料庫系統(比如 MySQL 和 Firebird),和對專有系統比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server 的一種選擇。
|
||||
|
||||
PostgreSQL 和 MySQL 比較,它更加龐大一點,因為它是用來替代 Oracle 而設計的。所以在企業應用中採用 PostgreSQL 是一個明智的選擇。
|
||||
|
||||
@@ -15,7 +15,7 @@ Go 實現的支援 PostgreSQL 的驅動也很多,因為國外很多人在開
|
||||
|
||||
在下面的範例中我採用了第一個驅動,因為它目前使用的人最多,在 github 上也比較活躍。
|
||||
|
||||
## 例項程式碼
|
||||
## 範例程式碼
|
||||
資料庫建表語句:
|
||||
```sql
|
||||
|
||||
@@ -125,9 +125,9 @@ func checkErr(err error) {
|
||||
}
|
||||
```
|
||||
|
||||
從上面的程式碼我們可以看到,PostgreSQL 是透過`$1`,`$2`這種方式來指定要傳遞的引數,而不是 MySQL 中的`?`,另外在 sql.Open 中的 dsn 資訊的格式也與 MySQL 的驅動中的 dsn 格式不一樣,所以在使用時請注意它們的差異。
|
||||
從上面的程式碼我們可以看到,PostgreSQL 是透過`$1`,`$2`這種方式來指定要傳遞的參數,而不是 MySQL 中的`?`,另外在 sql.Open 中的 dsn 資訊的格式也與 MySQL 的驅動中的 dsn 格式不一樣,所以在使用時請注意它們的差異。
|
||||
|
||||
還有 pg 不支援 LastInsertId 函式,因為 PostgreSQL 內部沒有實現類似 MySQL 的自增 ID 返回,其他的程式碼幾乎是一模一樣。
|
||||
還有 pg 不支援 LastInsertId 函式,因為 PostgreSQL 內部沒有實現類似 MySQL 的自增 ID 回傳,其他的程式碼幾乎是一模一樣。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -184,14 +184,14 @@ orm.SetMaxOpenConns("default", 30)
|
||||
```Go
|
||||
|
||||
type Userinfo struct {
|
||||
Uid int `PK` //如果表的主鍵不是 id,那麼需要加上 pk 註釋,顯式的說這個欄位是主鍵
|
||||
Uid int `PK` //如果表的主鍵不是 id,那麼需要加上 pk 註釋,明確的說這個欄位是主鍵
|
||||
Username string
|
||||
Departname string
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Uid int `PK` //如果表的主鍵不是 id,那麼需要加上 pk 註釋,顯式的說這個欄位是主鍵
|
||||
Uid int `PK` //如果表的主鍵不是 id,那麼需要加上 pk 註釋,明確的說這個欄位是主鍵
|
||||
Name string
|
||||
Profile *Profile `orm:"rel(one)"` // OneToOne relation
|
||||
Post []*Post `orm:"reverse(many)"` // 設定一對多的反向關係
|
||||
@@ -226,7 +226,7 @@ func init() {
|
||||
>注意一點,beego orm 針對駝峰命名會自動幫你轉化成下劃線欄位,例如你定義了 Struct 名字為`UserInfo`,那麼轉化成底層實現的時候是`user_info`,欄位命名也遵循該規則。
|
||||
|
||||
## 插入資料
|
||||
下面的程式碼示範瞭如何插入一條記錄,可以看到我們操作的是 struct 物件,而不是原生的 sql 語句,最後透過呼叫 Insert 介面將資料儲存到資料庫。
|
||||
下面的程式碼示範了如何插入一條記錄,可以看到我們操作的是 struct 物件,而不是原生的 sql 語句,最後透過呼叫 Insert 介面將資料儲存到資料庫。
|
||||
```Go
|
||||
|
||||
o := orm.NewOrm()
|
||||
@@ -249,9 +249,9 @@ if err == nil {
|
||||
|
||||
insert into table (name, age) values("slene", 28),("astaxie", 30),("unknown", 20)
|
||||
```
|
||||
第一個引數 bulk 為並列插入的數量,第二個為物件的 slice
|
||||
第一個參數 bulk 為並列插入的數量,第二個為物件的 slice
|
||||
|
||||
返回值為成功插入的數量
|
||||
回傳值為成功插入的數量
|
||||
```Go
|
||||
|
||||
users := []User{
|
||||
@@ -287,7 +287,7 @@ o.Update(&user, "Name")
|
||||
// o.Update(&user, "Field1", "Field2", ...)
|
||||
```
|
||||
|
||||
//Where:用來設定條件,支援多個引數,第一個引數如果為整數,相當於呼叫了 Where("主鍵=?",值)。
|
||||
//Where:用來設定條件,支援多個參數,第一個參數如果為整數,相當於呼叫了 Where("主鍵=?",值)。
|
||||
|
||||
## 查詢資料
|
||||
beego orm 的查詢介面比較靈活,具體使用請看下面的例子
|
||||
@@ -317,7 +317,7 @@ if err == orm.ErrNoRows {
|
||||
o := orm.NewOrm()
|
||||
var user User
|
||||
|
||||
qs := o.QueryTable(user) // 返回 QuerySeter
|
||||
qs := o.QueryTable(user) // 回傳 QuerySeter
|
||||
qs.Filter("id", 1) // WHERE id = 1
|
||||
qs.Filter("profile__age", 18) // WHERE profile.age = 18
|
||||
```
|
||||
@@ -353,19 +353,22 @@ qs.Limit(10, 20)
|
||||
```
|
||||
|
||||
## 刪除資料
|
||||
|
||||
beedb 提供了豐富的刪除資料介面,請看下面的例子
|
||||
|
||||
例子 1,刪除單條資料
|
||||
```Go
|
||||
例子 1,刪除一筆資料
|
||||
|
||||
```Go
|
||||
o := orm.NewOrm()
|
||||
if num, err := o.Delete(&User{Id: 1}); err == nil {
|
||||
fmt.Println(num)
|
||||
}
|
||||
```
|
||||
|
||||
Delete 操作會對反向關係進行操作,此例中 Post 擁有一個到 User 的外來鍵。刪除 User 的時候。如果 on_delete 設定為預設的級聯操作,將刪除對應的 Post
|
||||
|
||||
## 關聯查詢
|
||||
|
||||
有些應用卻需要用到連線查詢,所以現在 beego orm 提供了一個簡陋的實現方案:
|
||||
```Go
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# 5.6 NOSQL 資料庫操作
|
||||
NoSQL(Not Only SQL),指的是非關係型的資料庫。隨著 Web2.0 的興起,傳統的關係資料庫在應付 Web2.0 網站,特別是超大規模和高併發的 SNS 型別的 Web2.0 純動態網站已經顯得力不從心,暴露了很多難以克服的問題,而非關係型的資料庫則由於其本身的特點得到了非常迅速的發展。
|
||||
|
||||
NoSQL(Not Only SQL),指的是非關聯資料庫。隨著 Web2.0 的興起,傳統的關聯式資料庫在應付 Web2.0 網站,特別是超大規模和高併發的 SNS 型別的 Web2.0 純動態網站已經顯得力不從心,暴露了很多難以克服的問題,而非關聯資料庫則由於其本身的特點得到了非常迅速的發展。
|
||||
|
||||
而 Go 語言作為 21 世紀的 C 語言,對 NOSQL 的支援也是很好,目前流行的 NOSQL 主要有 redis、mongoDB、Cassandra 和 Membase 等。這些資料庫都有高效能、高併發讀寫等特點,目前已經廣泛應用於各種應用中。我接下來主要講解一下 redis 和 mongoDB 的操作。
|
||||
|
||||
@@ -137,11 +138,11 @@ func main() {
|
||||
|
||||
## mongoDB
|
||||
|
||||
MongoDB 是一個高效能,開源,無模式的文件型資料庫,是一個介於關係資料庫和非關係資料庫之間的產品,是非關係資料庫當中功能最豐富,最像關係資料庫的。他支援的資料結構非常鬆散,採用的是類似 json 的 bjson 格式來儲存資料,因此可以儲存比較複雜的資料型別。Mongo 最大的特點是他支援的查詢語言非常強大,其語法有點類似於物件導向的查詢語言,幾乎可以實現類似關係資料庫單表查詢的絕大部分功能,而且還支援對資料建立索引。
|
||||
MongoDB 是一個高效能、開源的文件型資料庫,是一個介於關聯式資料庫和非關聯式資料庫之間的產品,是非關聯式資料庫當中功能最豐富,最像關聯式資料庫的。他支援的資料結構非常鬆散,採用的是類似 json 的 bjson 格式來儲存資料,因此可以儲存比較複雜的資料型別。Mongo 最大的特點是他支援的查詢語言非常強大,其語法有點類似於物件導向的查詢語言,幾乎可以實現類似關聯式資料庫單表查詢的絕大部分功能,而且還支援對資料建立索引。
|
||||
|
||||
下圖展示了 mysql 和 mongoDB 之間的對應關係,我們可以看出來非常的方便,但是 mongoDB 的效能非常好。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 5.1 MongoDB 和 Mysql 的操作對比圖
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Web 開發中一個很重要的議題就是如何做好使用者的整個瀏覽
|
||||
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -9,20 +9,20 @@ session 和 cookie 是網站瀏覽中較為常見的兩個概念,也是比較
|
||||
|
||||
cookie,簡而言之就是在本地計算機儲存一些使用者操作的歷史資訊(當然包括登入資訊),並在使用者再次訪問該站點時瀏覽器透過 HTTP 協議將本地 cookie 內容傳送給伺服器,從而完成驗證,或繼續上一步操作。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 6.1 cookie 的原理圖
|
||||
|
||||
session,簡而言之就是在伺服器上儲存使用者操作的歷史資訊。伺服器使用 session id 來標識 session,session id 由伺服器負責產生,保證隨機性與唯一性,相當於一個隨機金鑰,避免在握手或傳輸中暴露使用者真實密碼。但該方式下,仍然需要將傳送請求的客戶端與 session 進行對應,所以可以藉助 cookie 機制來取得客戶端的標識(即 session id),也可以透過 GET 方式將 id 提交給伺服器。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 6.2 session 的原理圖
|
||||
|
||||
## cookie
|
||||
Cookie 是由瀏覽器維持的,儲存在客戶端的一小段文字資訊,伴隨著使用者請求和頁面在 Web 伺服器和瀏覽器之間傳遞。使用者每次訪問站點時,Web 應用程式都可以讀取 cookie 包含的資訊。瀏覽器設定裡面有 cookie 隱私資料選項,開啟它,可以看到很多已訪問網站的 cookies,如下圖所示:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 6.3 瀏覽器端儲存的 cookie 資訊
|
||||
|
||||
@@ -71,7 +71,7 @@ http.SetCookie(w, &cookie)
|
||||
```
|
||||
|
||||
### Go 讀取 cookie
|
||||
上面的例子示範瞭如何設定 cookie 資料,我們這裡來示範一下如何讀取 cookie
|
||||
上面的例子示範了如何設定 cookie 資料,我們這裡來示範一下如何讀取 cookie
|
||||
```Go
|
||||
|
||||
cookie, _ := r.Cookie("username")
|
||||
@@ -94,7 +94,7 @@ session 在 Web 開發環境下的語義又有了新的擴充套件,它的含
|
||||
|
||||
session 機制是一種伺服器端的機制,伺服器使用一種類似於散列表的結構(也可能就是使用散列表)來儲存資訊。
|
||||
|
||||
但程式需要為某個客戶端的請求建立一個 session 的時候,伺服器首先檢查這個客戶端的請求裡是否包含了一個 session 標識-稱為 session id,如果已經包含一個 session id 則說明以前已經為此客戶建立過 session,伺服器就按照 session id 把這個 session 檢索出來使用(如果檢索不到,可能會新建一個,這種情況可能出現在伺服器端已經刪除了該使用者對應的 session 物件,但使用者人為地在請求的 URL 後面附加上一個 JSESSION 的引數)。如果客戶請求不包含 session id,則為此客戶建立一個 session 並且同時產生一個與此 session 相關聯的 session id,這個 session id 將在本次響應中返回給客戶端儲存。
|
||||
但程式需要為某個客戶端的請求建立一個 session 的時候,伺服器首先檢查這個客戶端的請求裡是否包含了一個 session 標識-稱為 session id,如果已經包含一個 session id 則說明以前已經為此客戶建立過 session,伺服器就按照 session id 把這個 session 檢索出來使用(如果檢索不到,可能會建立一個,這種情況可能出現在伺服器端已經刪除了該使用者對應的 session 物件,但使用者人為地在請求的 URL 後面附加上一個 JSESSION 的參數)。如果客戶請求不包含 session id,則為此客戶建立一個 session 並且同時產生一個與此 session 相關聯的 session id,這個 session id 將在本次回應中回傳給客戶端儲存。
|
||||
|
||||
session 機制本身並不複雜,然而其實現和配置上的靈活性卻使得具體情況複雜多變。這也要求我們不能把僅僅某一次的經驗或者某一個瀏覽器,伺服器的經驗當作普遍適用的。
|
||||
|
||||
@@ -103,7 +103,7 @@ session 機制本身並不複雜,然而其實現和配置上的靈活性卻使
|
||||
如上文所述,session 和 cookie 的目的相同,都是為了克服 http 協議無狀態的缺陷,但完成的方法不同。session 透過 cookie,在客戶端儲存 session id,而將使用者的其他會話訊息儲存在伺服器端的 session 物件中,與此相對的,cookie 需要將所有資訊都儲存在客戶端。因此 cookie 存在著一定的安全隱患,例如本地 cookie 中儲存的使用者名稱密碼被破譯,或 cookie 被其他網站收集(例如:1. appA 主動設定域 B cookie,讓域 B cookie 取得;2. XSS,在 appA 上透過 javascript 取得 document.cookie,並傳遞給自己的 appB)。
|
||||
|
||||
|
||||
透過上面的一些簡單介紹我們瞭解了 cookie 和 session 的一些基礎知識,知道他們之間的聯絡和區別,做 web 開發之前,有必要將一些必要知識瞭解清楚,才不會在用到時捉襟見肘,或是在調 bug 時如無頭蒼蠅亂轉。接下來的幾小節我們將詳細介紹 session 相關的知識。
|
||||
透過上面的一些簡單介紹我們了解了 cookie 和 session 的一些基礎知識,知道他們之間的聯絡和區別,做 web 開發之前,有必要將一些必要知識了解清楚,才不會在用到時捉襟見肘,或是在調 bug 時如無頭蒼蠅亂轉。接下來的幾小節我們將詳細介紹 session 相關的知識。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -11,9 +11,9 @@ session 的基本原理是由伺服器為每個會話維護一份資訊資料,
|
||||
以上三個步驟中,最關鍵的是如何傳送這個 session 的唯一標識這一步上。考慮到 HTTP 協議的定義,資料無非可以放到請求行、頭域或 Body 裡,所以一般來說會有兩種常用的方式:cookie 和 URL 重寫。
|
||||
|
||||
1. Cookie
|
||||
伺服器端透過設定 Set-cookie 頭就可以將 session 的識別符號傳送到客戶端,而客戶端此後的每一次請求都會帶上這個識別符號,另外一般包含 session 資訊的 cookie 會將失效時間設定為 0(會話 cookie),即瀏覽器程序有效時間。至於瀏覽器怎麼處理這個 0,每個瀏覽器都有自己的方案,但差別都不會太大(一般體現在新建瀏覽器視窗的時候);
|
||||
伺服器端透過設定 Set-cookie 頭就可以將 session 的識別符號傳送到客戶端,而客戶端此後的每一次請求都會帶上這個識別符號,另外一般包含 session 資訊的 cookie 會將失效時間設定為 0(會話 cookie),即瀏覽器程序有效時間。至於瀏覽器怎麼處理這個 0,每個瀏覽器都有自己的方案,但差別都不會太大(一般體現在建立瀏覽器視窗的時候);
|
||||
2. URL 重寫
|
||||
所謂 URL 重寫,就是在返回給使用者的頁面裡的所有的 URL 後面追加 session 識別符號,這樣使用者在收到響應之後,無論點選響應頁面裡的哪個連結或提交表單,都會自動帶上 session 識別符號,從而就實現了會話的保持。雖然這種做法比較麻煩,但是,如果客戶端禁用了 cookie 的話,此種方案將會是首選。
|
||||
所謂 URL 重寫,就是在回傳給使用者的頁面裡的所有的 URL 後面追加 session 識別符號,這樣使用者在收到回應之後,無論點選回應頁面裡的哪個連結或提交表單,都會自動帶上 session 識別符號,從而就實現了會話的保持。雖然這種做法比較麻煩,但是,如果客戶端禁用了 cookie 的話,此種方案將會是首選。
|
||||
|
||||
## Go 實現 session 管理
|
||||
透過上面 session 建立過程的講解,讀者應該對 session 有了一個大體的認識,但是具體到動態頁面技術裡面,又是怎麼實現 session 的呢?下面我們將結合 session 的生命週期(lifecycle),來實現 go 語言版本的 session 管理。
|
||||
@@ -69,9 +69,9 @@ type Provider interface {
|
||||
SessionGC(maxLifeTime int64)
|
||||
}
|
||||
```
|
||||
- SessionInit 函式實現 Session 的初始化,操作成功則返回此新的 Session 變數
|
||||
- SessionRead 函式返回 sid 所代表的 Session 變數,如果不存在,那麼將以 sid 為引數呼叫 SessionInit 函式建立並返回一個新的 Session 變數
|
||||
- SessionDestroy 函式用來銷燬 sid 對應的 Session 變數
|
||||
- SessionInit 函式實現 Session 的初始化,操作成功則回傳此新的 Session 變數
|
||||
- SessionRead 函式回傳 sid 所代表的 Session 變數,如果不存在,那麼將以 sid 為參數呼叫 SessionInit 函式建立並回傳一個新的 Session 變數
|
||||
- SessionDestroy 函式用來刪除 sid 對應的 Session 變數
|
||||
- SessionGC 根據 maxLifeTime 來刪除過期的資料
|
||||
|
||||
那麼 Session 介面需要實現什麼樣的功能呢?有過 Web 開發經驗的讀者知道,對 Session 的處理基本就 設定值、讀取值、刪除值以及取得當前 sessionID 這四個操作,所以我們的 Session 介面也就實現這四個操作。
|
||||
@@ -105,7 +105,7 @@ func Register(name string, provider Provider) {
|
||||
```
|
||||
### 全域性唯一的 Session ID
|
||||
|
||||
Session ID 是用來識別訪問 Web 應用的每一個使用者,因此必須保證它是全域性唯一的(GUID),下面程式碼展示瞭如何滿足這一需求:
|
||||
Session ID 是用來識別訪問 Web 應用的每一個使用者,因此必須保證它是全域性唯一的(GUID),下面程式碼展示了如何滿足這一需求:
|
||||
|
||||
```Go
|
||||
|
||||
@@ -154,7 +154,7 @@ func login(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
```
|
||||
### 操作值:設定、讀取和刪除
|
||||
SessionStart 函式返回的是一個滿足 Session 介面的變數,那麼我們該如何用他來對 session 資料進行操作呢?
|
||||
SessionStart 函式回傳的是一個滿足 Session 介面的變數,那麼我們該如何用他來對 session 資料進行操作呢?
|
||||
|
||||
上面的例子中的程式碼`session.Get("uid")`已經展示了基本的讀取資料的操作,現在我們再來看一下詳細的操作:
|
||||
```Go
|
||||
@@ -184,7 +184,7 @@ func count(w http.ResponseWriter, r *http.Request) {
|
||||
因為 Session 有過期的概念,所以我們定義了 GC 操作,當訪問過期時間滿足 GC 的觸發條件後將會引起 GC,但是當我們進行了任意一個 session 操作,都會對 Session 實體進行更新,都會觸發對最後訪問時間的修改,這樣當 GC 的時候就不會誤刪除還在使用的 Session 實體。
|
||||
|
||||
### session 重置
|
||||
我們知道,Web 應用中有使用者退出這個操作,那麼當用戶退出應用的時候,我們需要對該使用者的 session 資料進行銷燬操作,上面的程式碼已經示範瞭如何使用 session 重置操作,下面這個函式就是實現了這個功能:
|
||||
我們知道,Web 應用中有使用者退出這個操作,那麼當用戶退出應用的時候,我們需要對該使用者的 session 資料進行刪除操作,上面的程式碼已經示範了如何使用 session 重置操作,下面這個函式就是實現了這個功能:
|
||||
```Go
|
||||
|
||||
//Destroy sessionid
|
||||
@@ -203,8 +203,8 @@ func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request){
|
||||
}
|
||||
|
||||
```
|
||||
### session 銷燬
|
||||
我們來看一下 Session 管理器如何來管理銷燬,只要我們在 Main 啟動的時候啟動:
|
||||
### session 刪除
|
||||
我們來看一下 Session 管理器如何來管理刪除,只要我們在 Main 啟動的時候啟動:
|
||||
```Go
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 6.4 預防 session 劫持
|
||||
session 劫持是一種廣泛存在的比較嚴重的安全威脅,在 session 技術中,客戶端和伺服器端透過 session 的識別符號來維護會話, 但這個識別符號很容易就能被嗅探到,從而被其他人利用。它是中間人攻擊的一種型別。
|
||||
|
||||
本節將透過一個例項來示範會話劫持,希望透過這個例項,能讓讀者更好地理解 session 的本質。
|
||||
本節將透過一個範例來示範會話劫持,希望透過這個範例,能讓讀者更好地理解 session 的本質。
|
||||
## session 劫持過程
|
||||
我們寫了如下的程式碼來展示一個 count 計數器:
|
||||
```Go
|
||||
@@ -27,37 +27,37 @@ Hi. Now count:{{.}}
|
||||
```
|
||||
然後我們在瀏覽器裡面重新整理可以看到如下內容:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 6.4 瀏覽器端顯示 count 數
|
||||
|
||||
隨著重新整理,數字將不斷增長,當數字顯示為 6 的時候,開啟瀏覽器(以 chrome 為例)的 cookie 管理器,可以看到類似如下的資訊:
|
||||
|
||||
|
||||

|
||||

|
||||
|
||||
圖 6.5 取得瀏覽器端儲存的 cookie
|
||||
|
||||
下面這個步驟最為關鍵: 開啟另一個瀏覽器(這裡我打開了 firefox 瀏覽器),複製 chrome 位址列裡的地址到新開啟的瀏覽器的位址列中。然後開啟 firefox 的 cookie 模擬外掛,新建一個 cookie,把按上圖中 cookie 內容原樣在 firefox 中重建一份:
|
||||
下面這個步驟最為關鍵: 開啟另一個瀏覽器(這裡我打開了 firefox 瀏覽器),複製 chrome 位址列裡的地址到新開啟的瀏覽器的位址列中。然後開啟 firefox 的 cookie 模擬外掛,建立一個 cookie,把按上圖中 cookie 內容原樣在 firefox 中重建一份:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 6.6 模擬 cookie
|
||||
|
||||
Enter 後,你將看到如下內容:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 6.7 劫持 session 成功
|
||||
|
||||
可以看到雖然換了瀏覽器,但是我們卻獲得了 sessionID,然後模擬了 cookie 儲存的過程。這個例子是在同一臺計算機上做的,不過即使換用兩臺來做,其結果仍然一樣。此時如果交替點選兩個瀏覽器裡的連結你會發現它們其實操縱的是同一個計數器。不必驚訝,此處 firefox 盜用了 chrome 和 goserver 之間的維持會話的鑰匙,即 gosessionid,這是一種型別的“會話劫持”。在 goserver 看來,它從 http 請求中得到了一個 gosessionid,由於 HTTP 協議的無狀態性,它無法得知這個 gosessionid 是從 chrome 那裡“劫持”來的,它依然會去查詢對應的 session,並執行相關計算。與此同時 chrome 也無法得知自己保持的會話已經被“劫持”。
|
||||
## session 劫持防範
|
||||
### cookieonly 和 token
|
||||
透過上面 session 劫持的簡單示範可以瞭解到 session 一旦被其他人劫持,就非常危險,劫持者可以假裝成被劫持者進行很多非法操作。那麼如何有效的防止 session 劫持呢?
|
||||
透過上面 session 劫持的簡單示範可以了解到 session 一旦被其他人劫持,就非常危險,劫持者可以假裝成被劫持者進行很多非法操作。那麼如何有效的防止 session 劫持呢?
|
||||
|
||||
其中一個解決方案就是 sessionID 的值只允許 cookie 設定,而不是透過 URL 重置方式設定,同時設定 cookie 的 httponly 為 true,這個屬性是設定是否可透過客戶端指令碼訪問這個設定的 cookie,第一這個可以防止這個 cookie 被 XSS 讀取從而引起 session 劫持,第二 cookie 設定不會像 URL 重置方式那麼容易取得 sessionID。
|
||||
|
||||
第二步就是在每個請求裡面加上 token,實現類似前面章節裡面講的防止 form 重複遞交類似的功能,我們在每個請求裡面加上一個隱藏的 token,然後每次驗證這個 token,從而保證使用者的請求都是唯一性。
|
||||
第二步就是在每個請求裡面加上 token,實現類似前面章節裡面講的防止 form 重複提交類似的功能,我們在每個請求裡面加上一個隱藏的 token,然後每次驗證這個 token,從而保證使用者的請求都是唯一性。
|
||||
```Go
|
||||
|
||||
h := md5.New()
|
||||
@@ -71,7 +71,7 @@ sess.Set("token",token)
|
||||
|
||||
```
|
||||
### 間隔產生新的 SID
|
||||
還有一個解決方案就是,我們給 session 額外設定一個建立時間的值,一旦過了一定的時間,我們銷燬這個 sessionID,重新產生新的 session,這樣可以一定程度上防止 session 劫持的問題。
|
||||
還有一個解決方案就是,我們給 session 額外設定一個建立時間的值,一旦過了一定的時間,我們刪除這個 sessionID,重新產生新的 session,這樣可以一定程度上防止 session 劫持的問題。
|
||||
```Go
|
||||
|
||||
createtime := sess.Get("createtime")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 6.5 小結
|
||||
這章我們學習了什麼是 session,什麼是 cookie,以及他們兩者之間的關係。但是目前 Go 官方標準套件裡面不支援 session,所以我們設計了一個 session 管理器,實現了 session 從建立到銷燬的整個過程。然後定義了 Provider 的介面,使得可以支援各種後端的 session 儲存,然後我們在第三小節裡面介紹瞭如何使用記憶體儲存來實現 session 的管理。第四小節我們講解了 session 劫持的過程,以及我們如何有效的來防止 session 劫持。透過這一章的講解,希望能夠讓讀者瞭解整個 sesison 的執行原理以及如何實現,而且是如何更加安全的使用 session。
|
||||
這章我們學習了什麼是 session,什麼是 cookie,以及他們兩者之間的關係。但是目前 Go 官方標準套件裡面不支援 session,所以我們設計了一個 session 管理器,實現了 session 從建立到刪除的整個過程。然後定義了 Provider 的介面,使得可以支援各種後端的 session 儲存,然後我們在第三小節裡面介紹了如何使用記憶體儲存來實現 session 的管理。第四小節我們講解了 session 劫持的過程,以及我們如何有效的來防止 session 劫持。透過這一章的講解,希望能夠讓讀者了解整個 sesison 的執行原理以及如何實現,而且是如何更加安全的使用 session。
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一節: [session 儲存](<06.4.md>)
|
||||
|
||||
@@ -4,7 +4,7 @@ Web 開發中對於文字處理是非常重要的一部分,我們往往需要
|
||||
XML 是目前很多標準介面的互動語言,很多時候和一些 Java 編寫的 webserver 進行互動都是基於 XML 標準進行互動,7.1 小節將介紹如何處理 XML 文字,我們使用 XML 之後發現它太複雜了,現在很多網際網路企業對外的 API 大多數採用了 JSON 格式,這種格式描述簡單,但是又能很好的表達意思,7.2 小節我們將講述如何來處理這樣的 JSON 格式資料。正則是一個讓人又愛又恨的工具,它處理文字的能力非常強大,我們在前面表單驗證裡面已經有所領略它的強大,7.3 小節將詳細的更深入的講解如何利用好 Go 的正則。Web 開發中一個很重要的部分就是 MVC 分離,在 Go 語言的 Web 開發中 V 有一個專門的套件來支援`template`,7.4 小節將詳細的講解如何使用模版來進行輸出內容。7.5 小節將詳細介紹如何進行檔案和資料夾的操作。7.6 小結介紹了字串的相關操作。
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 7.1 XML 處理
|
||||
XML 作為一種資料交換和資訊傳遞的格式已經十分普及。而隨著 Web 服務日益廣泛的應用,現在 XML 在日常的開發工作中也扮演了愈發重要的角色。這一小節, 我們將就 Go 語言標準套件中的 XML 相關處理的套件進行介紹。
|
||||
|
||||
這個小節不會涉及 XML 規範相關的內容(如需瞭解相關知識請參考其他文獻),而是介紹如何用 Go 語言來編解碼 XML 檔案相關的知識。
|
||||
這個小節不會涉及 XML 規範相關的內容(如需了解相關知識請參考其他文獻),而是介紹如何用 Go 語言來編解碼 XML 檔案相關的知識。
|
||||
|
||||
假如你是一名運維人員,你為你所管理的所有伺服器生成了如下內容的 xml 的配置檔案:
|
||||
```xml
|
||||
@@ -96,7 +96,7 @@ XML 本質上是一種樹形的資料格式,而我們可以定義與之匹配
|
||||
|
||||
func Unmarshal(data []byte, v interface{}) error
|
||||
```
|
||||
我們看到函式定義了兩個引數,第一個是 XML 資料流,第二個是儲存的對應型別,目前支援 struct、slice 和 string,XML 套件內部採用了反射來進行資料的對映,所以 v 裡面的欄位必須是匯出的。`Unmarshal`解析的時候 XML 元素和欄位怎麼對應起來的呢?這是有一個優先順序讀取流程的,首先會讀取 struct tag,如果沒有,那麼就會對應欄位名。必須注意一點的是解析的時候 tag、欄位名、XML 元素都是大小寫敏感的,所以必須一一對應欄位。
|
||||
我們看到函式定義了兩個參數,第一個是 XML 資料流,第二個是儲存的對應型別,目前支援 struct、slice 和 string,XML 套件內部採用了反射來進行資料的對映,所以 v 裡面的欄位必須是匯出的。`Unmarshal`解析的時候 XML 元素和欄位怎麼對應起來的呢?這是有一個優先順序讀取流程的,首先會讀取 struct tag,如果沒有,那麼就會對應欄位名。必須注意一點的是解析的時候 tag、欄位名、XML 元素都是區分大小寫的的,所以必須一一對應欄位。
|
||||
|
||||
Go 語言的反射機制,可以利用這些 tag 資訊來將來自 XML 檔案中的資料反射成對應的 struct 物件,關於反射如何利用 struct tag 的更多內容請參閱 reflect 中的相關內容。
|
||||
|
||||
@@ -125,7 +125,7 @@ Go 語言的反射機制,可以利用這些 tag 資訊來將來自 XML 檔案
|
||||
- 如果 struct 欄位後面的 tag 定義了`",any"`,如果他的子元素在不滿足其他的規則的時候就會匹配到這個欄位。
|
||||
- 如果某個 XML 元素包含一條或者多條註釋,那麼這些註釋將被累加到第一個 tag 含有",comments"的欄位上,這個欄位的型別可能是 []byte 或 string,如果沒有這樣的欄位存在,那麼註釋將會被拋棄。
|
||||
|
||||
上面詳細講述瞭如何定義 struct 的 tag。 只要設定對了 tag,那麼 XML 解析就如上面範例般簡單,tag 和 XML 的 element 是一一對應的關係,如上所示,我們還可以透過 slice 來表示多個同級元素。
|
||||
上面詳細講述了如何定義 struct 的 tag。 只要設定對了 tag,那麼 XML 解析就如上面範例般簡單,tag 和 XML 的 element 是一一對應的關係,如上所示,我們還可以透過 slice 來表示多個同級元素。
|
||||
|
||||
>注意: 為了正確解析,go 語言的 xml 套件要求 struct 定義中的所有欄位必須是可匯出的(即首字母大寫)
|
||||
|
||||
@@ -136,7 +136,7 @@ Go 語言的反射機制,可以利用這些 tag 資訊來將來自 XML 檔案
|
||||
func Marshal(v interface{}) ([]byte, error)
|
||||
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
|
||||
```
|
||||
兩個函式第一個引數是用來產生 XML 的結構定義型別資料,都是返回產生的 XML 資料流。
|
||||
兩個函式第一個參數是用來產生 XML 的結構定義型別資料,都是回傳產生的 XML 資料流。
|
||||
|
||||
下面我們來看一下如何輸出如上的 XML:
|
||||
```Go
|
||||
@@ -192,7 +192,7 @@ func main() {
|
||||
```
|
||||
和我們之前定義的檔案的格式一模一樣,之所以會有`os.Stdout.Write([]byte(xml.Header))` 這句程式碼的出現,是因為`xml.MarshalIndent`或者`xml.Marshal`輸出的資訊都是不帶 XML 頭的,為了產生正確的 xml 檔案,我們使用了 xml 套件預定義的 Header 變數。
|
||||
|
||||
我們看到 `Marshal` 函式接收的引數 v 是 interface{}型別的,即它可以接受任意型別的引數,那麼 xml 套件,根據什麼規則來產生相應的 XML 檔案呢?
|
||||
我們看到 `Marshal` 函式接收的參數 v 是 interface{}型別的,即它可以接受任意型別的參數,那麼 xml 套件,根據什麼規則來產生相應的 XML 檔案呢?
|
||||
|
||||
- 如果 v 是 array 或者 slice,那麼輸出每一個元素,類似<type>value</type>
|
||||
- 如果 v 是指標,那麼會 Marshal 指標指向的內容,如果指標為空,什麼都不輸出
|
||||
@@ -229,7 +229,7 @@ func main() {
|
||||
</name>
|
||||
|
||||
```
|
||||
上面我們介紹瞭如何使用 Go 語言的 xml 套件來編/解碼 XML 檔案,重要的一點是對 XML 的所有操作都是透過 struct tag 來實現的,所以學會對 struct tag 的運用變得非常重要,在文章中我們簡要的列舉了如何定義 tag。更多內容或 tag 定義請參看相應的官方資料。
|
||||
上面我們介紹了如何使用 Go 語言的 xml 套件來編/解碼 XML 檔案,重要的一點是對 XML 的所有操作都是透過 struct tag 來實現的,所以學會對 struct tag 的運用變得非常重要,在文章中我們簡要的列舉了如何定義 tag。更多內容或 tag 定義請參看相應的官方資料。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -226,7 +226,7 @@ os.Stdout.Write(b)
|
||||
{"serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.0\\\" \""}
|
||||
|
||||
```
|
||||
Marshal 函式只有在轉換成功的時候才會返回資料,在轉換的過程中我們需要注意幾點:
|
||||
Marshal 函式只有在轉換成功的時候才會回傳資料,在轉換的過程中我們需要注意幾點:
|
||||
|
||||
|
||||
- JSON 物件只支援 string 作為 key,所以要編碼一個 map,那麼必須是 map[string]T 這種型別(T 是 Go 語言中任意的型別)
|
||||
@@ -235,7 +235,7 @@ Marshal 函式只有在轉換成功的時候才會返回資料,在轉換的過
|
||||
- 指標在編碼的時候會輸出指標指向的內容,而空指標會輸出 null
|
||||
|
||||
|
||||
本小節,我們介紹瞭如何使用 Go 語言的 json 標準套件來編解碼 JSON 資料,同時也簡要介紹瞭如何使用第三方套件`go-simplejson`來在一些情況下簡化操作,學會並熟練運用它們將對我們接下來的 Web 開發相當重要。
|
||||
本小節,我們介紹了如何使用 Go 語言的 json 標準套件來編解碼 JSON 資料,同時也簡要介紹了如何使用第三方套件`go-simplejson`來在一些情況下簡化操作,學會並熟練運用它們將對我們接下來的 Web 開發相當重要。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
Go 語言透過 `regexp` 標準套件為正則表示式提供了官方支援,如果你已經使用過其他程式語言提供的正則相關功能,那麼你應該對 Go 語言版本的不會太陌生,但是它們之間也有一些小的差異,因為 Go 實現的是 RE2 標準,除了\C,詳細的語法描述參考:`http://code.google.com/p/re2/wiki/Syntax`
|
||||
|
||||
其實字串處理我們可以使用 `strings` 套件來進行搜尋(Contains、Index)、替換(Replace)和解析(Split、Join)等操作,但是這些都是簡單的字串操作,他們的搜尋都是大小寫敏感,而且固定的字串,如果我們需要匹配可變的那種就沒辦法實現了,當然如果 `strings` 套件能解決你的問題,那麼就儘量使用它來解決。因為他們足夠簡單、而且效能和可讀性都會比正則好。
|
||||
其實字串處理我們可以使用 `strings` 套件來進行搜尋(Contains、Index)、替換(Replace)和解析(Split、Join)等操作,但是這些都是簡單的字串操作,他們的搜尋都是區分大小寫的,而且固定的字串,如果我們需要匹配可變的那種就沒辦法實現了,當然如果 `strings` 套件能解決你的問題,那麼就儘量使用它來解決。因為他們足夠簡單、而且效能和可讀性都會比正則好。
|
||||
|
||||
如果你還記得,在前面表單驗證的小節裡,我們已經接觸過正則處理,在那裡我們利用了它來驗證輸入的資訊是否滿足某些預設的條件。在使用中需要注意的一點就是:所有的字元都是 UTF-8 編碼的。接下來讓我們更加深入的來學習 Go 語言的 `regexp` 套件相關知識吧。
|
||||
|
||||
## 透過正則判斷是否匹配
|
||||
`regexp`套件中含有三個函式用來判斷是否匹配,如果匹配返回 true,否則返回 false
|
||||
`regexp`套件中含有三個函式用來判斷是否匹配,如果匹配回傳 true,否則回傳 false
|
||||
```Go
|
||||
|
||||
func Match(pattern string, b []byte) (matched bool, error error)
|
||||
@@ -16,7 +16,7 @@ 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。
|
||||
上面的三個函式實現了同一個功能,就是判斷 `pattern` 是否和輸入源匹配,匹配的話就回傳 true,如果解析正則出錯則回傳 error。三個函式的輸入源分別是 byte slice、RuneReader 和 string。
|
||||
|
||||
如果要驗證一個輸入是不是 IP 地址,那麼如何來判斷呢?請看如下實現
|
||||
```Go
|
||||
@@ -98,7 +98,7 @@ func main() {
|
||||
}
|
||||
|
||||
```
|
||||
從這個範例可以看出,使用複雜的正則首先是 Compile,它會解析正則表示式是否合法,如果正確,那麼就會返回一個 Regexp,然後就可以利用返回的 Regexp 在任意的字串上面執行需要的操作。
|
||||
從這個範例可以看出,使用複雜的正則首先是 Compile,它會解析正則表示式是否合法,如果正確,那麼就會回傳一個 Regexp,然後就可以利用回傳的 Regexp 在任意的字串上面執行需要的操作。
|
||||
|
||||
解析正則表示式的有如下幾個方法:
|
||||
```Go
|
||||
@@ -108,9 +108,9 @@ func CompilePOSIX(expr string) (*Regexp, error)
|
||||
func MustCompile(str string) *Regexp
|
||||
func MustCompilePOSIX(str string) *Regexp
|
||||
```
|
||||
CompilePOSIX 和 Compile 的不同點在於 POSIX 必須使用 POSIX 語法,它使用最左最長方式搜尋,而 Compile 是採用的則只採用最左方式搜尋(例如[a-z]{2,4}這樣一個正則表示式,應用於"aa09aaa88aaaa"這個文字串時,CompilePOSIX 返回了 aaaa,而 Compile 的返回的是 aa)。字首有 Must 的函式表示,在解析正則語法的時候,如果匹配模式串不滿足正確的語法則直接 panic,而不加 Must 的則只是返回錯誤。
|
||||
CompilePOSIX 和 Compile 的不同點在於 POSIX 必須使用 POSIX 語法,它使用最左最長方式搜尋,而 Compile 是採用的則只採用最左方式搜尋(例如[a-z]{2,4}這樣一個正則表示式,應用於"aa09aaa88aaaa"這個文字串時,CompilePOSIX 回傳了 aaaa,而 Compile 的回傳的是 aa)。字首有 Must 的函式表示,在解析正則語法的時候,如果匹配模式串不滿足正確的語法則直接 panic,而不加 Must 的則只是回傳錯誤。
|
||||
|
||||
在瞭解瞭如何新建一個 Regexp 之後,我們再來看一下這個 struct 提供了哪些方法來輔助我們操作字串,首先我們來看下面這些用來搜尋的函式:
|
||||
在了解了如何建立一個 Regexp 之後,我們再來看一下這個 struct 提供了哪些方法來輔助我們操作字串,首先我們來看下面這些用來搜尋的函式:
|
||||
```Go
|
||||
|
||||
func (re *Regexp) Find(b []byte) []byte
|
||||
@@ -163,7 +163,7 @@ func main() {
|
||||
one := re.Find([]byte(a))
|
||||
fmt.Println("Find:", string(one))
|
||||
|
||||
//查詢符合正則的所有 slice,n 小於 0 表示返回全部符合的字串,不然就是返回指定的長度
|
||||
//查詢符合正則的所有 slice,n 小於 0 表示回傳全部符合的字串,不然就是回傳指定的長度
|
||||
all := re.FindAll([]byte(a), -1)
|
||||
fmt.Println("FindAll", all)
|
||||
|
||||
@@ -177,7 +177,7 @@ func main() {
|
||||
|
||||
re2, _ := regexp.Compile("am(.*)lang(.*)")
|
||||
|
||||
//查詢 Submatch,返回陣列,第一個元素是匹配的全部元素,第二個元素是第一個()裡面的,第三個是第二個()裡面的
|
||||
//查詢 Submatch,回傳陣列,第一個元素是匹配的全部元素,第二個元素是第一個()裡面的,第三個是第二個()裡面的
|
||||
//下面的輸出第一個元素是"am learning Go language"
|
||||
//第二個元素是" learning Go ",注意包含空格的輸出
|
||||
//第三個元素是"uage"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
透過下面這個圖可以說明範本的機制
|
||||
|
||||

|
||||

|
||||
|
||||
圖 7.1 範本機制圖
|
||||
|
||||
@@ -30,7 +30,7 @@ func handler(w http.ResponseWriter, r *http.Request) {
|
||||
- 使用`os.Stdout`代替`http.ResponseWriter`,因為`os.Stdout`實現了`io.Writer`介面
|
||||
|
||||
## 範本中如何插入資料?
|
||||
上面我們示範瞭如何解析並渲染範本,接下來讓我們來更加詳細的瞭解如何把資料渲染出來。一個範本都是應用在一個 Go 的物件之上,Go 物件的欄位如何插入到範本中呢?
|
||||
上面我們示範了如何解析並渲染範本,接下來讓我們來更加詳細的了解如何把資料渲染出來。一個範本都是應用在一個 Go 的物件之上,Go 物件的欄位如何插入到範本中呢?
|
||||
|
||||
### 欄位操作
|
||||
Go 語言的範本透過 `{{}}` 來包含需要在渲染時被替換的欄位,`{{.}}`表示當前的物件,這和 Java 或者 C++中的 this 類似,如果要訪問當前物件的欄位透過`{{.FieldName}}`,但是需要注意一點:這個欄位必須是匯出的(欄位首字母必須是大寫的),否則在渲染的時候就會報錯,請看下面的這個例子:
|
||||
@@ -69,7 +69,7 @@ t, _ = t.Parse("hello {{.UserName}}! {{.email}}")
|
||||
如果範本中輸出`{{.}}`,這個一般應用於字串物件,預設會呼叫 fmt 套件輸出字串的內容。
|
||||
|
||||
### 輸出巢狀欄位內容
|
||||
上面我們例子展示瞭如何針對一個物件的欄位輸出,那麼如果欄位裡面還有物件,如何來迴圈的輸出這些內容呢?我們可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`來進行資料的輸出。
|
||||
上面我們例子展示了如何針對一個物件的欄位輸出,那麼如果欄位裡面還有物件,如何來迴圈的輸出這些內容呢?我們可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`來進行資料的輸出。
|
||||
|
||||
- {{range}} 這個和 Go 語法裡面的 range 類似,迴圈操作資料
|
||||
- {{with}}操作是指當前物件的值,類似上下文的概念
|
||||
@@ -115,7 +115,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
### 條件處理
|
||||
在 Go 範本裡面如果需要進行條件判斷,那麼我們可以使用和 Go 語言的`if-else`語法類似的方式來處理,如果 pipeline 為空,那麼 if 就認為是 false,下面的例子展示瞭如何使用`if-else`語法:
|
||||
在 Go 範本裡面如果需要進行條件判斷,那麼我們可以使用和 Go 語言的`if-else`語法類似的方式來處理,如果 pipeline 為空,那麼 if 就認為是 false,下面的例子展示了如何使用`if-else`語法:
|
||||
```Go
|
||||
|
||||
package main
|
||||
@@ -153,7 +153,7 @@ Unix 使用者已經很熟悉什麼是 `pipe` 了,`ls | grep "beego"`類似這
|
||||
在 email 輸出的地方我們可以採用如上方式可以把輸出全部轉化 html 的實體,上面的這種方式和我們平常寫 Unix 的方式是不是一模一樣,操作起來相當的簡便,呼叫其他的函式也是類似的方式。
|
||||
|
||||
### 範本變數
|
||||
有時候,我們在範本使用過程中需要定義一些區域性變數,我們可以在一些操作中申明區域性變數,例如 `with``range``if` 過程中申明區域性變數,這個變數的作用域是 `{{end}}` 之前,Go 語言透過申明的區域性變數格式如下所示:
|
||||
有時候,我們在範本使用過程中需要定義一些區域性變數,我們可以在一些操作中宣告區域性變數,例如 `with``range``if` 過程中宣告區域性變數,這個變數的作用域是 `{{end}}` 之前,Go 語言透過宣告的區域性變數格式如下所示:
|
||||
```Go
|
||||
|
||||
$variable := pipeline
|
||||
@@ -179,7 +179,7 @@ type FuncMap map[string]interface{}
|
||||
|
||||
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
|
||||
```
|
||||
`EmailDealWith`這個函式的引數和返回值定義如下:
|
||||
`EmailDealWith`這個函式的參數和回傳值定義如下:
|
||||
```Go
|
||||
|
||||
func EmailDealWith(args …interface{}) string
|
||||
@@ -246,7 +246,7 @@ func main() {
|
||||
}
|
||||
|
||||
```
|
||||
上面示範瞭如何自訂函式,其實,在範本套件內部已經有內建的實現函式,下面程式碼擷取自範本套件裡面
|
||||
上面示範了如何自訂函式,其實,在範本套件內部已經有內建的實現函式,下面程式碼擷取自範本套件裡面
|
||||
```Go
|
||||
|
||||
var builtins = FuncMap{
|
||||
@@ -300,7 +300,7 @@ The next one ought to fail.
|
||||
panic: template: check parse error with Must:1: unexpected "}" in command
|
||||
```
|
||||
## 巢狀範本
|
||||
我們平常開發 Web 應用的時候,經常會遇到一些範本有些部分是固定不變的,然後可以抽取出來作為一個獨立的部分,例如一個部落格的頭部和尾部是不變的,而唯一改變的是中間的內容部分。所以我們可以定義成`header`、`content`、`footer`三個部分。Go 語言中透過如下的語法來申明
|
||||
我們平常開發 Web 應用的時候,經常會遇到一些範本有些部分是固定不變的,然後可以抽取出來作為一個獨立的部分,例如一個部落格的頭部和尾部是不變的,而唯一改變的是中間的內容部分。所以我們可以定義成`header`、`content`、`footer`三個部分。Go 語言中透過如下的語法來宣告
|
||||
```Go
|
||||
|
||||
{{define "子範本名稱"}}內容{{end}}
|
||||
@@ -366,7 +366,7 @@ func main() {
|
||||
>同一個集合類別的範本是互相知曉的,如果同一範本被多個集合使用,則它需要在多個集合中分別解析
|
||||
|
||||
## 總結
|
||||
透過上面對範本的詳細介紹,我們瞭解瞭如何把動態資料與範本融合:如何輸出迴圈資料、如何自訂函式、如何巢狀範本等等。透過範本技術的應用,我們可以完成 MVC 模式中 V 的處理,接下來的章節我們將介紹如何來處理 M 和 C。
|
||||
透過上面對範本的詳細介紹,我們了解了如何把動態資料與範本融合:如何輸出迴圈資料、如何自訂函式、如何巢狀範本等等。透過範本技術的應用,我們可以完成 MVC 模式中 V 的處理,接下來的章節我們將介紹如何來處理 M 和 C。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 7.5 檔案操作
|
||||
在任何計算機裝置中,檔案是都是必須的物件,而在 Web 程式設計中,檔案的操作一直是 Web 程式設計師經常遇到的問題,檔案操作在 Web 應用中是必須的,非常有用的,我們經常遇到產生檔案目錄,檔案(夾)編輯等操作,現在我把 Go 中的這些操作做一詳細總結並例項示範如何使用。
|
||||
在任何計算機裝置中,檔案是都是必須的物件,而在 Web 程式設計中,檔案的操作一直是 Web 程式設計師經常遇到的問題,檔案操作在 Web 應用中是必須的,非常有用的,我們經常遇到產生檔案目錄,檔案(夾)編輯等操作,現在我把 Go 中的這些操作做一詳細總結並範例示範如何使用。
|
||||
## 目錄操作
|
||||
檔案操作的大多數函式都是在 os 套件裡面,下面列舉了幾個目錄操作的:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
- func MkdirAll(path string, perm FileMode) error
|
||||
|
||||
根據 path 建立多級子目錄,例如 astaxie/test1/test2。
|
||||
根據 path 建立多階層子目錄,例如 astaxie/test1/test2。
|
||||
|
||||
- func Remove(name string) error
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
- func RemoveAll(path string) error
|
||||
|
||||
根據 path 刪除多級子目錄,如果 path 是單個名稱,那麼該目錄下的子目錄全部刪除。
|
||||
根據 path 刪除多階層子目錄,如果 path 是單個名稱,那麼該目錄下的子目錄全部刪除。
|
||||
|
||||
|
||||
下面是示範程式碼:
|
||||
@@ -46,15 +46,15 @@ func main() {
|
||||
## 檔案操作
|
||||
|
||||
### 建立與開啟檔案
|
||||
新建檔案可以透過如下兩個方法
|
||||
建立檔案可以透過如下兩個方法
|
||||
|
||||
- func Create(name string) (file *File, err Error)
|
||||
|
||||
根據提供的檔名建立新的檔案,返回一個檔案物件,預設許可權是 0666 的檔案,返回的檔案物件是可讀寫的。
|
||||
根據提供的檔名建立新的檔案,回傳一個檔案物件,預設許可權是 0666 的檔案,回傳的檔案物件是可讀寫的。
|
||||
|
||||
- func NewFile(fd uintptr, name string) *File
|
||||
|
||||
根據檔案描述符建立相應的檔案,返回一個檔案物件
|
||||
根據檔案描述符建立相應的檔案,回傳一個檔案物件
|
||||
|
||||
|
||||
透過如下兩個方法來開啟檔案:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
- func Contains(s, substr string) bool
|
||||
|
||||
字串 s 中是否包含 substr,返回 bool 值
|
||||
字串 s 中是否包含 substr,回傳 bool 值
|
||||
|
||||
```Go
|
||||
|
||||
@@ -34,7 +34,7 @@ fmt.Println(strings.Join(s, ", "))
|
||||
|
||||
- func Index(s, sep string) int
|
||||
|
||||
在字串 s 中查詢 sep 所在的位置,返回位置值,找不到返回-1
|
||||
在字串 s 中查詢 sep 所在的位置,回傳位置值,找不到回傳-1
|
||||
|
||||
```Go
|
||||
|
||||
@@ -45,7 +45,7 @@ fmt.Println(strings.Index("chicken", "dmr"))
|
||||
```
|
||||
- func Repeat(s string, count int) string
|
||||
|
||||
重複 s 字串 count 次,最後返回重複的字串
|
||||
重複 s 字串 count 次,最後回傳重複的字串
|
||||
|
||||
```Go
|
||||
|
||||
@@ -65,7 +65,7 @@ fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1))
|
||||
```
|
||||
- func Split(s, sep string) []string
|
||||
|
||||
把 s 字串按照 sep 分割,返回 slice
|
||||
把 s 字串按照 sep 分割,回傳 slice
|
||||
|
||||
|
||||
```Go
|
||||
@@ -92,7 +92,7 @@ fmt.Printf("[%q]", strings.Trim(" !!! Achtung !!! ", "! "))
|
||||
|
||||
- func Fields(s string) []string
|
||||
|
||||
去除 s 字串的空格符,並且按照空格分割返回 slice
|
||||
去除 s 字串的空格符,並且按照空格分割回傳 slice
|
||||
|
||||
|
||||
```Go
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 8 Web 服務
|
||||
Web 服務可以讓你在 HTTP 協議的基礎上透過 XML 或者 JSON 來交換資訊。如果你想知道上海的天氣預報、中國石油的股價或者淘寶商家的一個商品資訊,你可以編寫一段簡短的程式碼,透過抓取這些資訊然後透過標準的介面開放出來,就如同你呼叫一個本地函式並返回一個值。
|
||||
Web 服務可以讓你在 HTTP 協議的基礎上透過 XML 或者 JSON 來交換資訊。如果你想知道上海的天氣預報、中國石油的股價或者淘寶商家的一個商品資訊,你可以編寫一段簡短的程式碼,透過抓取這些資訊然後透過標準的介面開放出來,就如同你呼叫一個本地函式並回傳一個值。
|
||||
|
||||
Web 服務背後的關鍵在於平臺的無關性,你可以執行你的服務在 Linux 系統,可以與其他 Windows 的 asp.net 程式互動,同樣的,也可以透過同一個介面和執行在 FreeBSD 上面的 JSP 無障礙地通訊。
|
||||
|
||||
@@ -12,7 +12,7 @@ SOAP 是 W3C 在跨網路資訊傳遞和遠端計算機函式呼叫方面的一
|
||||
Go 語言是 21 世紀的 C 語言,我們追求的是效能、簡單,所以我們在 8.1 小節裡面介紹如何使用 Socket 程式設計,很多遊戲服務都是採用 Socket 來編寫伺服器端,因為 HTTP 協議相對而言比較耗費效能,讓我們看看 Go 語言如何來 Socket 程式設計。目前隨著 HTML5 的發展,webSockets 也逐漸的成為很多頁遊公司接下來開發的一些手段,我們將在 8.2 小節裡面講解 Go 語言如何編寫 webSockets 的程式碼。
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
在很多底層網路應用開發者的眼裡一切程式設計都是 Socket,話雖然有點誇張,但卻也幾乎如此了,現在的網路程式設計幾乎都是用 Socket 來程式設計。你想過這些情景麼?我們每天開啟瀏覽器瀏覽網頁時,瀏覽器程序怎麼和 Web 伺服器進行通訊的呢?當你用 QQ 聊天時,QQ 程序怎麼和伺服器或者是你的好友所在的 QQ 程序進行通訊的呢?當你開啟 PPstream 觀看視訊時,PPstream 程序如何與視訊伺服器進行通訊的呢? 如此種種,都是靠 Socket 來進行通訊的,以一斑窺全豹,可見 Socket 程式設計在現代程式設計中佔據了多麼重要的地位,這一節我們將介紹 Go 語言中如何進行 Socket 程式設計。
|
||||
|
||||
## 什麼是 Socket?
|
||||
Socket 起源於 Unix,而 Unix 基本哲學之一就是“一切皆檔案”,都可以用“開啟 open –> 讀寫 write/read –> 關閉 close”模式來操作。Socket 就是該模式的一個實現,網路的 Socket 資料傳輸是一種特殊的 I/O,Socket 也是一種檔案描述符。Socket 也具有一個類似於開啟檔案的函式呼叫:Socket(),該函式返回一個整型的 Socket 描述符,隨後的連線建立、資料傳輸等操作都是透過該 Socket 實現的。
|
||||
Socket 起源於 Unix,而 Unix 基本哲學之一就是“一切皆檔案”,都可以用“開啟 open –> 讀寫 write/read –> 關閉 close”模式來操作。Socket 就是該模式的一個實現,網路的 Socket 資料傳輸是一種特殊的 I/O,Socket 也是一種檔案描述符。Socket 也具有一個類似於開啟檔案的函式呼叫:Socket(),該函式回傳一個整型的 Socket 描述符,隨後的連線建立、資料傳輸等操作都是透過該 Socket 實現的。
|
||||
|
||||
常用的 Socket 型別有兩種:串流式的 Socket(SOCK_STREAM)和資料報式的 Socket(SOCK_DGRAM)。串流式是一種連線導向的 Socket,針對於連線導向的 TCP 服務應用;資料報式 Socket 是一種無連線的 Socket,對應於無連線的 UDP 服務應用。
|
||||
|
||||
## Socket 如何通訊
|
||||
網路中的程序之間如何透過 Socket 通訊呢?首要解決的問題是如何唯一標識一個程序,否則通訊無從談起!在本地可以透過程序 PID 來唯一標識一個程序,但是在網路中這是行不通的。其實 TCP/IP 協議族已經幫我們解決了這個問題,網路層的“ip 地址”可以唯一標識網路中的主機,而傳輸層的“協議+埠”可以唯一標識主機中的應用程式(程序)。這樣利用三元組(ip 地址,協議,埠)就可以標識網路的程序了,網路中需要互相通訊的程序,就可以利用這個標誌在他們之間進行互動。請看下面這個 TCP/IP 協議結構圖
|
||||
|
||||

|
||||

|
||||
|
||||
圖 8.1 七層網路協議圖
|
||||
|
||||
@@ -95,18 +95,18 @@ type TCPAddr struct {
|
||||
func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
|
||||
```
|
||||
|
||||
- net 引數是"tcp4"、"tcp6"、"tcp"中的任意一個,分別表示 TCP(IPv4-only), TCP(IPv6-only)或者 TCP(IPv4, IPv6 的任意一個)。
|
||||
- net 參數是"tcp4"、"tcp6"、"tcp"中的任意一個,分別表示 TCP(IPv4-only), TCP(IPv6-only)或者 TCP(IPv4, IPv6 的任意一個)。
|
||||
- addr 表示域名或者 IP 地址,例如"www.google.com:80" 或者"127.0.0.1:22"。
|
||||
|
||||
|
||||
### TCP client
|
||||
Go 語言中透過 net 套件中的 `DialTCP` 函式來建立一個 TCP 連線,並返回一個 `TCPConn` 型別的物件,當連線建立時伺服器端也建立一個同類型的物件,此時客戶端和伺服器端透過各自擁有的 `TCPConn` 物件來進行資料交換。一般而言,客戶端透過 `TCPConn` 物件將請求資訊傳送到伺服器端,讀取伺服器端響應的資訊。伺服器端讀取並解析來自客戶端的請求,並返回應答資訊,這個連線只有當任一端關閉了連線之後才失效,不然這連線可以一直在使用。建立連線的函式定義如下:
|
||||
Go 語言中透過 net 套件中的 `DialTCP` 函式來建立一個 TCP 連線,並回傳一個 `TCPConn` 型別的物件,當連線建立時伺服器端也建立一個同類型的物件,此時客戶端和伺服器端透過各自擁有的 `TCPConn` 物件來進行資料交換。一般而言,客戶端透過 `TCPConn` 物件將請求資訊傳送到伺服器端,讀取伺服器端回應的資訊。伺服器端讀取並解析來自客戶端的請求,並回傳回應資訊,這個連線只有當任一端關閉了連線之後才失效,不然這連線可以一直在使用。建立連線的函式定義如下:
|
||||
|
||||
```Go
|
||||
|
||||
func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
|
||||
```
|
||||
- network 引數是"tcp4"、"tcp6"、"tcp"中的任意一個,分別表示 TCP(IPv4-only)、TCP(IPv6-only)或者 TCP(IPv4,IPv6 的任意一個)
|
||||
- network 參數是"tcp4"、"tcp6"、"tcp"中的任意一個,分別表示 TCP(IPv4-only)、TCP(IPv6-only)或者 TCP(IPv4,IPv6 的任意一個)
|
||||
- laddr 表示本機地址,一般設定為 nil
|
||||
|
||||
- raddr 表示遠端的服務地址
|
||||
@@ -115,7 +115,7 @@ func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
|
||||
|
||||
"HEAD / HTTP/1.0\r\n\r\n"
|
||||
|
||||
從伺服器端接收到的響應資訊格式可能如下:
|
||||
從伺服器端接收到的回應資訊格式可能如下:
|
||||
```Go
|
||||
|
||||
HTTP/1.0 200 OK
|
||||
@@ -163,7 +163,7 @@ func checkError(err error) {
|
||||
}
|
||||
|
||||
```
|
||||
透過上面的程式碼我們可以看出:首先程式將使用者的輸入作為引數 `service` 傳入`net.ResolveTCPAddr`取得一個 tcpAddr,然後把 tcpAddr 傳入 DialTCP 後建立了一個 TCP 連線`conn`,透過 `conn` 來發送請求資訊,最後透過`ioutil.ReadAll`從 `conn` 中讀取全部的文字,也就是伺服器端響應反饋的資訊。
|
||||
透過上面的程式碼我們可以看出:首先程式將使用者的輸入作為參數 `service` 傳入`net.ResolveTCPAddr`取得一個 tcpAddr,然後把 tcpAddr 傳入 DialTCP 後建立了一個 TCP 連線`conn`,透過 `conn` 來發送請求資訊,最後透過`ioutil.ReadAll`從 `conn` 中讀取全部的文字,也就是伺服器端回應反饋的資訊。
|
||||
|
||||
### TCP server
|
||||
上面我們編寫了一個 TCP 的客戶端程式,也可以透過 net 套件來建立一個伺服器端程式,在伺服器端我們需要繫結服務到指定的非啟用埠,並監聽此埠,當有客戶端請求到達的時候可以接收到來自客戶端連線的請求。net 套件中有相應功能的函式,函式定義如下:
|
||||
@@ -172,7 +172,7 @@ func checkError(err error) {
|
||||
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
|
||||
func (l *TCPListener) Accept() (Conn, error)
|
||||
```
|
||||
引數說明同 DialTCP 的引數一樣。下面我們實現一個簡單的時間同步服務,監聽 7777 埠
|
||||
參數說明同 DialTCP 的參數一樣。下面我們實現一個簡單的時間同步服務,監聽 7777 埠
|
||||
```Go
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 8.2 WebSocket
|
||||
WebSocket 是 HTML5 的重要特性,它實現了基於瀏覽器的遠端 socket,它使瀏覽器和伺服器可以進行全雙工通訊,許多瀏覽器(Firefox、Google Chrome 和 Safari)都已對此做了支援。
|
||||
|
||||
在 WebSocket 出現之前,為了實現即時通訊,採用的技術都是“輪詢”,即在特定的時間間隔內,由瀏覽器對伺服器發出 HTTP Request,伺服器在收到請求後,返回最新的資料給瀏覽器重新整理,“輪詢”使得瀏覽器需要對伺服器不斷髮出請求,這樣會佔用大量頻寬。
|
||||
在 WebSocket 出現之前,為了實現即時通訊,採用的技術都是“輪詢”,即在特定的時間間隔內,由瀏覽器對伺服器發出 HTTP Request,伺服器在收到請求後,回傳最新的資料給瀏覽器重新整理,“輪詢”使得瀏覽器需要對伺服器不斷髮出請求,這樣會佔用大量頻寬。
|
||||
|
||||
WebSocket 採用了一些特殊的報頭,使得瀏覽器和伺服器只需要做一個握手的動作,就可以在瀏覽器和伺服器之間建立一條連線通道。且此連線會保持在活動狀態,你可以使用 JavaScript 來向連線寫入或從中接收資料,就像在使用一個常規的 TCP Socket 一樣。它解決了 Web 即時化的問題,相比傳統 HTTP 有如下好處:
|
||||
|
||||
@@ -11,7 +11,7 @@ WebSocket 採用了一些特殊的報頭,使得瀏覽器和伺服器只需要
|
||||
|
||||
WebSocket URL 的起始輸入是 ws://或是 wss://(在 SSL 上)。下圖展示了 WebSocket 的通訊過程,一個帶有特定報頭的 HTTP 握手被髮送到了伺服器端,接著在伺服器端或是客戶端就可以透過 JavaScript 來使用某種套介面(socket),這一套介面可被用來透過事件控制代碼非同步地接收資料。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 8.2 WebSocket 原理圖
|
||||
|
||||
@@ -20,7 +20,7 @@ WebSocket 的協議頗為簡單,在第一次 handshake 透過以後,連線
|
||||
|
||||
瀏覽器發出 WebSocket 連線請求,然後伺服器發出迴應,然後連線建立成功,這個過程通常稱為“握手” (handshaking)。請看下面的請求和反饋資訊:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 8.3 WebSocket 的 request 和 response 資訊
|
||||
|
||||
@@ -141,9 +141,9 @@ func main() {
|
||||
}
|
||||
|
||||
```
|
||||
當客戶端將使用者輸入的資訊 Send 之後,伺服器端透過 Receive 接收到了相應資訊,然後透過 Send 傳送了應答資訊。
|
||||
當客戶端將使用者輸入的資訊 Send 之後,伺服器端透過 Receive 接收到了相應資訊,然後透過 Send 傳送了回應資訊。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 8.4 WebSocket 伺服器端接收到的資訊
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ REST(REpresentational State Transfer)這個概念,首次出現是在 2000 年
|
||||
|
||||
訪問一個網站,就代表了客戶端和伺服器的一個互動過程。在這個過程中,肯定涉及到資料和狀態的變化。而 HTTP 協議是無狀態的,那麼這些狀態肯定儲存在伺服器端,所以如果客戶端想要通知伺服器端改變資料和狀態的變化,肯定要透過某種方式來通知它。
|
||||
|
||||
客戶端能通知伺服器端的手段,只能是 HTTP 協議。具體來說,就是 HTTP 協議裡面,四個表示操作方式的動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操作:GET 用來取得資源,POST 用來新建資源(也可以用於更新資源),PUT 用來更新資源,DELETE 用來刪除資源。
|
||||
客戶端能通知伺服器端的手段,只能是 HTTP 協議。具體來說,就是 HTTP 協議裡面,四個表示操作方式的動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操作:GET 用來取得資源,POST 用來建立資源(也可以用於更新資源),PUT 用來更新資源,DELETE 用來刪除資源。
|
||||
|
||||
綜合上面的解釋,我們總結一下什麼是 RESTful 架構:
|
||||
|
||||
@@ -31,11 +31,11 @@ REST(REpresentational State Transfer)這個概念,首次出現是在 2000 年
|
||||
|
||||
Web 應用要滿足 REST 最重要的原則是 : 客戶端和伺服器之間的互動在請求之間是無狀態的,即從客戶端到伺服器的每個請求都必須包含理解請求所必需的資訊。如果伺服器在請求之間的任何時間點重啟,客戶端不會得到通知。此外此請求可以由任何可用伺服器回答,這十分適合雲端計算之類別的環境。因為是無狀態的,所以客戶端可以快取資料以改進效能。
|
||||
|
||||
另一個重要的 REST 原則是系統分層,這表示元件無法瞭解除了與它直接互動的層次以外的元件。透過將系統知識限制在單個層,可以限制整個系統的複雜性,從而促進了底層的獨立性。
|
||||
另一個重要的 REST 原則是系統分層,這表示元件無法了解除了與它直接互動的層次以外的元件。透過將系統知識限制在單個層,可以限制整個系統的複雜性,從而促進了底層的獨立性。
|
||||
|
||||
下圖即是 REST 的架構圖:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 8.5 REST 架構圖
|
||||
|
||||
@@ -43,14 +43,14 @@ Web 應用要滿足 REST 最重要的原則是 : 客戶端和伺服器之間的
|
||||
|
||||
下圖展示了 REST 的擴充套件性:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 8.6 REST 的擴充套件性
|
||||
|
||||
## RESTful 的實現
|
||||
Go 沒有為 REST 提供直接支援,但是因為 RESTful 是基於 HTTP 協議實現的,所以我們可以利用`net/http`套件來自己實現,當然需要針對 REST 做一些改造,REST 是根據不同的 method 來處理相應的資源,目前已經存在的很多自稱是 REST 的應用,其實並沒有真正的實現 REST,我暫且把這些應用根據實現的 method 分成幾個級別,請看下圖:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 8.7 REST 的 level 分級
|
||||
|
||||
@@ -117,7 +117,7 @@ func main() {
|
||||
}
|
||||
|
||||
```
|
||||
上面的程式碼示範瞭如何編寫一個 REST 的應用,我們訪問的資源是使用者,我們透過不同的 method 來訪問不同的函式,這裡使用了第三方函式庫`github.com/julienschmidt/httprouter`,在前面章節我們介紹過如何實現自訂的路由器,這個函式庫實現了自訂路由和方便的路由規則對映,透過它,我們可以很方便的實現 REST 的架構。透過上面的程式碼可知,REST 就是根據不同的 method 訪問同一個資源的時候實現不同的邏輯處理。
|
||||
上面的程式碼示範了如何編寫一個 REST 的應用,我們訪問的資源是使用者,我們透過不同的 method 來訪問不同的函式,這裡使用了第三方函式庫`github.com/julienschmidt/httprouter`,在前面章節我們介紹過如何實現自訂的路由器,這個函式庫實現了自訂路由和方便的路由規則對映,透過它,我們可以很方便的實現 REST 的架構。透過上面的程式碼可知,REST 就是根據不同的 method 訪問同一個資源的時候實現不同的邏輯處理。
|
||||
|
||||
## 總結
|
||||
REST 是一種架構風格,汲取了 WWW 的成功經驗:無狀態,以資源為中心,充分利用 HTTP 協議和 URI 協議,提供統一的介面定義,使得它作為一種設計 Web 服務的方法而變得流行。在某種意義上,透過強調 URI 和 HTTP 等早期 Internet 標準,REST 是對大型應用程式伺服器時代之前的 Web 方式的迴歸。目前 Go 對於 REST 的支援還是很簡單的,透過實現自訂的路由規則,我們就可以為不同的 method 實現不同的 handle,這樣就實現了 REST 的架構。
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# 8.4 RPC
|
||||
前面幾個小節我們介紹瞭如何基於 Socket 和 HTTP 來編寫網路應用,透過學習我們瞭解了 Socket 和 HTTP 採用的是類似"資訊交換"模式,即客戶端傳送一條資訊到伺服器端,然後(一般來說)伺服器端都會返回一定的資訊以表示響應。客戶端和伺服器端之間約定了互動資訊的格式,以便雙方都能夠解析互動所產生的資訊。但是很多獨立的應用並沒有採用這種模式,而是採用類似常規的函式呼叫的方式來完成想要的功能。
|
||||
前面幾個小節我們介紹了如何基於 Socket 和 HTTP 來編寫網路應用,透過學習我們了解了 Socket 和 HTTP 採用的是類似"資訊交換"模式,即客戶端傳送一條資訊到伺服器端,然後(一般來說)伺服器端都會回傳一定的資訊以表示回應。客戶端和伺服器端之間約定了互動資訊的格式,以便雙方都能夠解析互動所產生的資訊。但是很多獨立的應用並沒有採用這種模式,而是採用類似常規的函式呼叫的方式來完成想要的功能。
|
||||
|
||||
RPC 就是想實現函式呼叫模式的網路化。客戶端就像呼叫本地函式一樣,然後客戶端把這些引數打套件之後透過網路傳遞到伺服器端,伺服器端解套件到處理過程中執行,然後執行的結果反饋給客戶端。
|
||||
RPC 就是想實現函式呼叫模式的網路化。客戶端就像呼叫本地函式一樣,然後客戶端把這些參數打套件之後透過網路傳遞到伺服器端,伺服器端解套件到處理過程中執行,然後執行的結果反饋給客戶端。
|
||||
|
||||
RPC(Remote Procedure Call Protocol)——遠端過程呼叫協議,是一種透過網路從遠端計算機程式上請求服務,而不需要了解底層網路技術的協議。它假定某些傳輸協議的存在,如 TCP 或 UDP,以便為通訊程式之間攜帶資訊資料。透過它可以使函式呼叫模式網路化。在 OSI 網路通訊模型中,RPC 跨越了傳輸層和應用層。RPC 使得開發包括網路分散式多程式在內的應用程式更加容易。
|
||||
|
||||
## RPC 工作原理
|
||||
|
||||

|
||||

|
||||
|
||||
圖 8.8 RPC 工作流程圖
|
||||
|
||||
執行時,一次客戶機對伺服器的 RPC 呼叫,其內部操作大致有如下十步:
|
||||
執行時,一次客戶端對伺服器的 RPC 呼叫,其內部操作大致有如下十步:
|
||||
|
||||
- 1.呼叫客戶端控制代碼;執行傳送引數
|
||||
- 1.呼叫客戶端控制代碼;執行傳送參數
|
||||
- 2.呼叫本地系統核心傳送網路訊息
|
||||
- 3.訊息傳送到遠端主機
|
||||
- 4.伺服器控制代碼得到訊息並取得引數
|
||||
- 4.伺服器控制代碼得到訊息並取得參數
|
||||
- 5.執行遠端過程
|
||||
- 6.執行的過程將結果返回伺服器控制代碼
|
||||
- 7.伺服器控制代碼返回結果,呼叫遠端系統核心
|
||||
- 6.執行的過程將結果回傳伺服器控制代碼
|
||||
- 7.伺服器控制代碼回傳結果,呼叫遠端系統核心
|
||||
- 8.訊息傳回本地主機
|
||||
- 9.客戶控制代碼由核心接收訊息
|
||||
- 10.客戶接收控制代碼返回的資料
|
||||
- 10.客戶接收控制代碼回傳的資料
|
||||
|
||||
## Go RPC
|
||||
Go 標準套件中已經提供了對 RPC 的支援,而且支援三個級別的 RPC:TCP、HTTP、JSONRPC。但 Go 的 RPC 套件是獨一無二的 RPC,它和傳統的 RPC 系統不同,它只支援 Go 開發的伺服器與客戶端之間的互動,因為在內部,它們採用了 Gob 來編碼。
|
||||
@@ -30,9 +30,9 @@ Go 標準套件中已經提供了對 RPC 的支援,而且支援三個級別的
|
||||
Go RPC 的函式只有符合下面的條件才能被遠端訪問,不然會被忽略,詳細的要求如下:
|
||||
|
||||
- 函式必須是匯出的(首字母大寫)
|
||||
- 必須有兩個匯出型別的引數,
|
||||
- 第一個引數是接收的引數,第二個引數是返回給客戶端的引數,第二個引數必須是指標型別的
|
||||
- 函式還要有一個返回值 error
|
||||
- 必須有兩個匯出型別的參數,
|
||||
- 第一個參數是接收的參數,第二個參數是回傳給客戶端的參數,第二個參數必須是指標型別的
|
||||
- 函式還要有一個回傳值 error
|
||||
|
||||
舉個例子,正確的 RPC 函式格式如下:
|
||||
|
||||
@@ -154,7 +154,7 @@ Arith: 17*8=136
|
||||
Arith: 17/8=2 remainder 1
|
||||
|
||||
```
|
||||
透過上面的呼叫可以看到引數和返回值是我們定義的 struct 型別,在伺服器端我們把它們當做呼叫函式的引數的型別,在客戶端作為`client.Call`的第 2,3 兩個引數的型別。客戶端最重要的就是這個 Call 函式,它有 3 個引數,第 1 個要呼叫的函式的名字,第 2 個是要傳遞的引數,第 3 個要返回的引數(注意是指標型別),透過上面的程式碼例子我們可以發現,使用 Go 的 RPC 實現相當的簡單,方便。
|
||||
透過上面的呼叫可以看到參數和回傳值是我們定義的 struct 型別,在伺服器端我們把它們當做呼叫函式的參數的型別,在客戶端作為`client.Call`的第 2,3 兩個參數的型別。客戶端最重要的就是這個 Call 函式,它有 3 個參數,第 1 個要呼叫的函式的名字,第 2 個是要傳遞的參數,第 3 個要回傳的參數(注意是指標型別),透過上面的程式碼例子我們可以發現,使用 Go 的 RPC 實現相當的簡單,方便。
|
||||
### TCP RPC
|
||||
上面我們實現了基於 HTTP 協議的 RPC,接下來我們要實現基於 TCP 協議的 RPC,伺服器端的實現程式碼如下所示:
|
||||
```Go
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 8.5 小結
|
||||
這一章我們介紹了目前流行的幾種主要的網路應用開發方式,第一小節介紹了網路程式設計中的基礎 :Socket 程式設計,因為現在網路正在朝雲的方向快速進化,作為這一技術演進的基石的的 socket 知識,作為開發者的你,是必須要掌握的。第二小節介紹了正愈發流行的 HTML5 中一個重要的特性 WebSocket,透過它,伺服器可以實現主動的 push 訊息,以簡化以前 ajax 輪詢的模式。第三小節介紹了 REST 編寫模式,這種模式特別適合來開發網路應用 API,目前移動應用的快速發展,我覺得將來會是一個潮流。第四小節介紹了 Go 實現的 RPC 相關知識,對於上面四種開發方式,Go 都已經提供了良好的支援,net 套件及其子套件,是所有涉及到網路程式設計的工具的所在地。如果你想更加深入的瞭解相關實現細節,可以嘗試閱讀這個套件下面的原始碼。
|
||||
這一章我們介紹了目前流行的幾種主要的網路應用開發方式,第一小節介紹了網路程式設計中的基礎 :Socket 程式設計,因為現在網路正在朝雲的方向快速進化,作為這一技術演進的基石的的 socket 知識,作為開發者的你,是必須要掌握的。第二小節介紹了正愈發流行的 HTML5 中一個重要的特性 WebSocket,透過它,伺服器可以實現主動的 push 訊息,以簡化以前 ajax 輪詢的模式。第三小節介紹了 REST 編寫模式,這種模式特別適合來開發網路應用 API,目前移動應用的快速發展,我覺得將來會是一個潮流。第四小節介紹了 Go 實現的 RPC 相關知識,對於上面四種開發方式,Go 都已經提供了良好的支援,net 套件及其子套件,是所有涉及到網路程式設計的工具的所在地。如果你想更加深入的了解相關實現細節,可以嘗試閱讀這個套件下面的原始碼。
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一節: [RPC](<08.4.md>)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
加密的本質就是擾亂資料,某些不可恢復的資料擾亂我們稱為單向加密或者雜湊演算法。另外還有一種雙向加密方式,也就是可以對加密後的資料進行解密。我們將會在 9.6 小節介紹如何實現這種雙向加密方式。
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -12,7 +12,7 @@ CSRF(Cross-site request forgery),中文名稱:跨站請求偽造,也
|
||||
|
||||
下圖簡單闡述了 CSRF 攻擊的思想
|
||||
|
||||

|
||||

|
||||
|
||||
圖 9.1 CSRF 的攻擊過程
|
||||
|
||||
@@ -55,13 +55,13 @@ mux.Get("/user/:uid", getuser)
|
||||
mux.Post("/user/:uid", modifyuser)
|
||||
|
||||
```
|
||||
這樣處理後,因為我們限定了修改只能使用 POST,當 GET 方式請求時就拒絕響應,所以上面圖示中 GET 方式的 CSRF 攻擊就可以防止了,但這樣就能全部解決問題了嗎?當然不是,因為 POST 也是可以模擬的。
|
||||
這樣處理後,因為我們限定了修改只能使用 POST,當 GET 方式請求時就拒絕回應,所以上面圖示中 GET 方式的 CSRF 攻擊就可以防止了,但這樣就能全部解決問題了嗎?當然不是,因為 POST 也是可以模擬的。
|
||||
|
||||
因此我們需要實施第二步,在非 GET 方式的請求中增加隨機數,這個大概有三種方式來進行:
|
||||
|
||||
- 為每個使用者產生一個唯一的 cookie token,所有表單都包含同一個偽隨機值,這種方案最簡單,因為攻擊者不能獲得第三方的 Cookie(理論上),所以表單中的資料也就構造失敗,但是由於使用者的 Cookie 很容易由於網站的 XSS 漏洞而被盜取,所以這個方案必須要在沒有 XSS 的情況下才安全。
|
||||
- 每個請求使用驗證碼,這個方案是完美的,因為要多次輸入驗證碼,所以使用者友好性很差,所以不適合實際運用。
|
||||
- 不同的表單包含一個不同的偽隨機值,我們在 4.4 小節介紹“如何防止表單多次遞交”時介紹過此方案,複用相關程式碼,實現如下:
|
||||
- 不同的表單包含一個不同的偽隨機值,我們在 4.4 小節介紹“如何防止表單多次提交”時介紹過此方案,複用相關程式碼,實現如下:
|
||||
|
||||
產生隨機數 token
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
過濾資料主要採用如下一些函式庫來操作:
|
||||
|
||||
- strconv 套件下面的字串轉化相關函式,因為從 Request 中的`r.Form`返回的是字串,而有些時候我們需要將之轉化成整/浮點數,`Atoi`、`ParseBool`、`ParseFloat`、`ParseInt`等函式就可以派上用場了。
|
||||
- strconv 套件下面的字串轉化相關函式,因為從 Request 中的`r.Form`回傳的是字串,而有些時候我們需要將之轉化成整/浮點數,`Atoi`、`ParseBool`、`ParseFloat`、`ParseInt`等函式就可以派上用場了。
|
||||
- string 套件下面的一些過濾函式`Trim`、`ToLower`、`ToTitle`等函式,能夠幫助我們按照指定的格式取得資訊。
|
||||
- regexp 套件用來處理一些複雜的需求,例如判定輸入是否是 Email、生日之類別。
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</form>
|
||||
|
||||
```
|
||||
在處理這個表單的程式設計邏輯中,非常容易犯的錯誤是認為只能提交三個選擇中的一個。其實攻擊者可以模擬 POST 操作,遞交 `name=attack` 這樣的資料,所以在此時我們需要做類似白名單的處理
|
||||
在處理這個表單的程式設計邏輯中,非常容易犯的錯誤是認為只能提交三個選擇中的一個。其實攻擊者可以模擬 POST 操作,提交 `name=attack` 這樣的資料,所以在此時我們需要做類似白名單的處理
|
||||
```Go
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
XSS 攻擊:跨站指令碼攻擊(Cross-Site Scripting),為了不和層疊樣式表(Cascading Style Sheets, CSS)的縮寫混淆,故將跨站指令碼攻擊縮寫為 XSS。XSS 是一種常見的 web 安全漏洞,它允許攻擊者將惡意程式碼植入到提供給其它使用者使用的頁面中。不同於大多數攻擊(一般只涉及攻擊者和受害者),XSS 涉及到三方,即攻擊者、客戶端與 Web 應用。XSS 的攻擊目標是為了盜取儲存在客戶端的 cookie 或者其他網站用於識別客戶端身份的敏感資訊。一旦取得到合法使用者的資訊後,攻擊者甚至可以假冒合法使用者與網站進行互動。
|
||||
|
||||
XSS 通常可以分為兩大類別:一類別是儲存型 XSS,主要出現在讓使用者輸入資料,供其他瀏覽此頁的使用者進行檢視的地方,包括留言、評論、部落格日誌和各類別表單等。應用程式從資料庫中查詢資料,在頁面中顯示出來,攻擊者在相關頁面輸入惡意的指令碼資料後,使用者瀏覽此類別頁面時就可能受到攻擊。這個流程簡單可以描述為 : 惡意使用者的 Html 輸入 Web 程式->進入資料庫->Web 程式->使用者瀏覽器。另一類別是反射型 XSS,主要做法是將指令碼程式碼加入 URL 地址的請求引數裡,請求引數進入程式後在頁面直接輸出,使用者點選類似的惡意連結就可能受到攻擊。
|
||||
XSS 通常可以分為兩大類別:一類別是儲存型 XSS,主要出現在讓使用者輸入資料,供其他瀏覽此頁的使用者進行檢視的地方,包括留言、評論、部落格日誌和各類別表單等。應用程式從資料庫中查詢資料,在頁面中顯示出來,攻擊者在相關頁面輸入惡意的指令碼資料後,使用者瀏覽此類別頁面時就可能受到攻擊。這個流程簡單可以描述為 : 惡意使用者的 Html 輸入 Web 程式->進入資料庫->Web 程式->使用者瀏覽器。另一類別是反射型 XSS,主要做法是將指令碼程式碼加入 URL 地址的請求參數裡,請求參數進入程式後在頁面直接輸出,使用者點選類似的惡意連結就可能受到攻擊。
|
||||
|
||||
XSS 目前主要的手段和目的如下:
|
||||
|
||||
@@ -18,7 +18,7 @@ XSS 目前主要的手段和目的如下:
|
||||
## XSS 的原理
|
||||
Web 應用未對使用者提交請求的資料做充分的檢查過濾,允許使用者在提交的資料中摻入 HTML 程式碼(最主要的是“>”、“<”),並將未經轉義的惡意程式碼輸出到第三方使用者的瀏覽器解釋執行,是導致 XSS 漏洞的產生原因。
|
||||
|
||||
接下來以反射性 XSS 舉例說明 XSS 的過程:現在有一個網站,根據引數輸出使用者的名稱,例如訪問 url:`http://127.0.0.1/?name=astaxie`,就會在瀏覽器輸出如下資訊:
|
||||
接下來以反射性 XSS 舉例說明 XSS 的過程:現在有一個網站,根據參數輸出使用者的名稱,例如訪問 url:`http://127.0.0.1/?name=astaxie`,就會在瀏覽器輸出如下資訊:
|
||||
|
||||
hello astaxie
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
SQL 注入攻擊(SQL Injection),簡稱注入攻擊,是 Web 開發中最常見的一種安全漏洞。可以用它來從資料庫取得敏感資訊,或者利用資料庫的特性執行新增使用者,匯出檔案等一系列惡意操作,甚至有可能取得資料庫乃至系統使用者最高許可權。
|
||||
|
||||
而造成 SQL 注入的原因是因為程式沒有有效過濾使用者的輸入,使攻擊者成功的向伺服器提交惡意的 SQL 查詢程式碼,程式在接收後錯誤的將攻擊者的輸入作為查詢語句的一部分執行,導致原始的查詢邏輯被改變,額外的執行了攻擊者精心構造的惡意程式碼。
|
||||
## SQL 注入例項
|
||||
## SQL 注入範例
|
||||
很多 Web 開發者沒有意識到 SQL 查詢是可以被篡改的,從而把 SQL 查詢當作可信任的命令。殊不知,SQL 查詢是可以繞開訪問控制,從而繞過身份驗證和許可權檢查的。更有甚者,有可能透過 SQL 查詢去執行主機系統級的命令。
|
||||
|
||||
下面將透過一些真實的例子來詳細講解 SQL 注入的方式。
|
||||
@@ -65,9 +65,9 @@ SQL 注入攻擊的危害這麼大,那麼該如何來防治呢 ? 下面這些
|
||||
1. 嚴格限制 Web 應用的資料庫的操作許可權,給此使用者提供僅僅能夠滿足其工作的最低許可權,從而最大限度的減少注入攻擊對資料庫的危害。
|
||||
2. 檢查輸入的資料是否具有所期望的資料格式,嚴格限制變數的型別,例如使用 regexp 套件進行一些匹配處理,或者使用 strconv 套件對字串轉化成其他基本型別的資料進行判斷。
|
||||
3. 對進入資料庫的特殊字元('"\尖括號&*;等)進行轉義處理,或編碼轉換。Go 的`text/template`套件裡面的 `HTMLEscapeString` 函式可以對字串進行轉義處理。
|
||||
4. 所有的查詢語句建議使用資料庫提供的引數化查詢介面,引數化的語句使用引數而不是將使用者輸入變數嵌入到 SQL 語句中,即不要直接拼接 SQL 語句。例如使用`database/sql`裡面的查詢函式 `Prepare` 和`Query`,或者`Exec(query string, args ...interface{})`。
|
||||
4. 所有的查詢語句建議使用資料庫提供的參數化查詢介面,參數化的語句使用參數而不是將使用者輸入變數嵌入到 SQL 語句中,即不要直接拼接 SQL 語句。例如使用`database/sql`裡面的查詢函式 `Prepare` 和`Query`,或者`Exec(query string, args ...interface{})`。
|
||||
5. 在應用釋出之前建議使用專業的 SQL 注入檢測工具進行檢測,以及時修補被發現的 SQL 注入漏洞。網上有很多這方面的開源工具,例如 sqlmap、SQLninja 等。
|
||||
6. 避免網站打印出 SQL 錯誤資訊,比如型別錯誤、欄位不匹配等,把程式碼裡的 SQL 語句暴露出來,以防止攻擊者利用這些錯誤資訊進行 SQL 注入。
|
||||
6. 避免網站顯示出 SQL 錯誤資訊,比如型別錯誤、欄位不匹配等,把程式碼裡的 SQL 語句暴露出來,以防止攻擊者利用這些錯誤資訊進行 SQL 注入。
|
||||
|
||||
## 總結
|
||||
透過上面的範例我們可以知道,SQL 注入是危害相當大的安全漏洞。所以對於我們平常編寫的 Web 應用,應該對於每一個小細節都要非常重視,細節決定命運,生活如此,編寫 Web 應用也是這樣。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 9.6 加密和解密資料
|
||||
前面小節介紹瞭如何儲存密碼,但是有的時候,我們想把一些敏感資料加密後儲存起來,在將來的某個時候,隨需將它們解密出來,此時我們應該在選用對稱加密演算法來滿足我們的需求。
|
||||
前面小節介紹了如何儲存密碼,但是有的時候,我們想把一些敏感資料加密後儲存起來,在將來的某個時候,隨需將它們解密出來,此時我們應該在選用對稱加密演算法來滿足我們的需求。
|
||||
|
||||
## base64 加解密
|
||||
如果 Web 應用足夠簡單,資料的安全性沒有那麼嚴格的要求,那麼可以採用一種比較簡單的加解密方法是`base64`,這種方式實現起來比較簡單,Go 語言的 `base64` 套件已經很好的支援了這個,請看下面的例子:
|
||||
@@ -99,7 +99,7 @@ func main() {
|
||||
}
|
||||
|
||||
```
|
||||
上面透過呼叫函式`aes.NewCipher`(引數 key 必須是 16、24 或者 32 位的[]byte,分別對應 AES-128, AES-192 或 AES-256 演算法),返回了一個`cipher.Block`介面,這個介面實現了三個功能:
|
||||
上面透過呼叫函式`aes.NewCipher`(參數 key 必須是 16、24 或者 32 位的[]byte,分別對應 AES-128, AES-192 或 AES-256 演算法),回傳了一個`cipher.Block`介面,這個介面實現了三個功能:
|
||||
|
||||
```Go
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 9.7 小結
|
||||
這一章主要介紹瞭如:CSRF 攻擊、XSS 攻擊、SQL 注入攻擊等一些 Web 應用中典型的攻擊手法,它們都是由於應用對使用者的輸入沒有很好的過濾引起的,所以除了介紹攻擊的方法外,我們也介紹了了如何有效的進行資料過濾,以防止這些攻擊的發生的方法。然後針對日異嚴重的密碼洩漏事件,介紹了在設計 Web 應用中可採用的從基本到專家的加密方案。最後針對敏感資料的加解密簡要介紹了,Go 語言提供三種對稱加密演算法:base64、aes 和 des 的實現。
|
||||
這一章主要介紹了如:CSRF 攻擊、XSS 攻擊、SQL 注入攻擊等一些 Web 應用中典型的攻擊手法,它們都是由於應用對使用者的輸入沒有很好的過濾引起的,所以除了介紹攻擊的方法外,我們也介紹了了如何有效的進行資料過濾,以防止這些攻擊的發生的方法。然後針對日異嚴重的密碼洩漏事件,介紹了在設計 Web 應用中可採用的從基本到專家的加密方案。最後針對敏感資料的加解密簡要介紹了,Go 語言提供三種對稱加密演算法:base64、aes 和 des 的實現。
|
||||
|
||||
編寫這一章的目的是希望讀者能夠在意識裡面加強安全概念,在編寫 Web 應用的時候多留心一點,以使我們編寫的 Web 應用能遠離黑客們的攻擊。Go 語言在支援防攻擊方面已經提供大量的工具套件,我們可以充分的利用這些套件來做出一個安全的 Web 應用。
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
|
||||
3、如何根據 locale 提取字串和其它相應的資訊。
|
||||
|
||||
在第一小節裡,我們將介紹如何設定正確的 locale 以便讓訪問站點的使用者能夠獲得與其語言相應的頁面。第二小節將介紹如何處理或儲存字串、貨幣、時間日期等與 locale 相關的資訊,第三小節將介紹如何實現國際化站點,即如何根據不同 locale 返回不同合適的內容。透過這三個小節的學習,我們將獲得一個完整的 i18n 方案。
|
||||
在第一小節裡,我們將介紹如何設定正確的 locale 以便讓訪問站點的使用者能夠獲得與其語言相應的頁面。第二小節將介紹如何處理或儲存字串、貨幣、時間日期等與 locale 相關的資訊,第三小節將介紹如何實現國際化站點,即如何根據不同 locale 回傳不同合適的內容。透過這三個小節的學習,我們將獲得一個完整的 i18n 方案。
|
||||
|
||||
## 目錄
|
||||
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -6,7 +6,7 @@ Locale 是一組描述世界上某一特定區域文字格式和語言習慣的
|
||||
GO 語言預設採用"UTF-8"編碼集,所以我們實現 i18n 時不考慮第三部分,接下來我們都採用 locale 描述的前面兩部分來作為 i18n 標準的 locale 名。
|
||||
|
||||
|
||||
>在 Linux 和 Solaris 系統中可以透過`locale -a`命令列舉所有支援的地區名,讀者可以看到這些地區名的命名規範。對於 BSD 等系統,沒有 locale 命令,但是地區資訊儲存在/usr/share/locale 中。
|
||||
>在 Linux 和 Solaris 系統中可以透過 `locale -a` 命令列舉所有支援的地區名,讀者可以看到這些地區名的命名規範。對於 BSD 等系統,沒有 locale 命令,但是地區資訊儲存在/usr/share/locale 中。
|
||||
|
||||
## 設定 Locale
|
||||
|
||||
@@ -46,13 +46,13 @@ if prefix[0] == "en" {
|
||||
i18n.SetLocale("zh-TW")
|
||||
}
|
||||
```
|
||||
透過域名設定 Locale 有如上所示的優點,但是我們一般開發 Web 應用的時候不會採用這種方式,因為首先域名成本比較高,開發一個 Locale 就需要一個域名,而且往往統一名稱的域名不一定能申請的到,其次我們不願意為每個站點去本地化一個配置,而更多的是採用 url 後面帶引數的方式,請看下面的介紹。
|
||||
透過域名設定 Locale 有如上所示的優點,但是我們一般開發 Web 應用的時候不會採用這種方式,因為首先域名成本比較高,開發一個 Locale 就需要一個域名,而且往往統一名稱的域名不一定能申請的到,其次我們不願意為每個站點去本地化一個配置,而更多的是採用 url 後面帶參數的方式,請看下面的介紹。
|
||||
|
||||
### 從域名引數設定 Locale
|
||||
### 從域名參數設定 Locale
|
||||
|
||||
目前最常用的設定 Locale 的方式是在 URL 裡面帶上引數,例如 www.asta.com/hello?locale=zh 或者 www.asta.com/zh/hello 。這樣我們就可以設定地區:`i18n.SetLocale(params["locale"])`。
|
||||
目前最常用的設定 Locale 的方式是在 URL 裡面帶上參數,例如 www.asta.com/hello?locale=zh 或者 www.asta.com/zh/hello 。這樣我們就可以設定地區:`i18n.SetLocale(params["locale"])`。
|
||||
|
||||
這種設定方式幾乎擁有前面講的透過域名設定 Locale 的所有優點,它採用 RESTful 的方式,以使得我們不需要增加額外的方法來處理。但是這種方式需要在每一個的 link 裡面增加相應的引數 locale,這也許有點複雜而且有時候甚至相當的繁瑣。不過我們可以寫一個通用的函式 url,讓所有的 link 地址都透過這個函式來產生,然後在這個函式裡面增加`locale=params["locale"]`引數來緩解一下。
|
||||
這種設定方式幾乎擁有前面講的透過域名設定 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
|
||||
@@ -79,7 +79,7 @@ if AL == "en" {
|
||||
當然在實際應用中,可能需要更加嚴格的判斷來進行設定地區
|
||||
- IP 地址
|
||||
|
||||
另一種根據客戶端來設定地區就是使用者訪問的 IP,我們根據相應的 IP 函式庫,對應訪問的 IP 到地區,目前全球比較常用的就是 GeoIP Lite Country 這個函式庫。這種設定地區的機制非常簡單,我們只需要根據 IP 資料庫查詢使用者的 IP 然後返回國家地區,根據返回的結果設定對應的地區。
|
||||
另一種根據客戶端來設定地區就是使用者訪問的 IP,我們根據相應的 IP 函式庫,對應訪問的 IP 到地區,目前全球比較常用的就是 GeoIP Lite Country 這個函式庫。這種設定地區的機制非常簡單,我們只需要根據 IP 資料庫查詢使用者的 IP 然後回傳國家地區,根據回傳的結果設定對應的地區。
|
||||
|
||||
- 使用者 profile
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- {% raw %} -->
|
||||
# 10.2 本地化資源
|
||||
前面小節我們介紹瞭如何設定 Locale,設定好 Locale 之後我們需要解決的問題就是如何儲存相應的 Locale 對應的資訊呢?這裡面的資訊包括:文字資訊、時間和日期、貨幣值、圖片、包含檔案以及檢視等資源。那麼接下來我們將對這些資訊一一進行介紹,Go 語言中我們把這些格式資訊儲存在 JSON 中,然後透過合適的方式展現出來。(接下來以中文和英文兩種語言對比舉例,儲存格式檔案 en.json 和 zh-CN.json)
|
||||
前面小節我們介紹了如何設定 Locale,設定好 Locale 之後我們需要解決的問題就是如何儲存相應的 Locale 對應的資訊呢?這裡面的資訊包括:文字資訊、時間和日期、貨幣值、圖片、包含檔案以及檢視等資源。那麼接下來我們將對這些資訊一一進行介紹,Go 語言中我們把這些格式資訊儲存在 JSON 中,然後透過合適的方式展現出來。(接下來以中文和英文兩種語言對比舉例,儲存格式檔案 en.json 和 zh-CN.json)
|
||||
## 本地化文字訊息
|
||||
文字資訊是編寫 Web 應用中最常用到的,也是本地化資源中最多的資訊,想要以適合本地語言的方式來顯示文字資訊,可行的一種方案是 : 建立需要的語言相應的 map 來維護一個 key-value 的關係,在輸出之前按需從適合的 map 中去取得相應的文字,如下是一個簡單的範例:
|
||||
|
||||
@@ -79,7 +79,7 @@ fmt.Println(date(msg(lang,"date_format"),t))
|
||||
func date(fomate string,t time.Time) string{
|
||||
year, month, day = t.Date()
|
||||
hour, min, sec = t.Clock()
|
||||
//解析相應的%Y %m %d %H %M %S 然後返回資訊
|
||||
//解析相應的%Y %m %d %H %M %S 然後回傳資訊
|
||||
//%Y 替換成 2012
|
||||
|
||||
//%m 替換成 10
|
||||
@@ -142,7 +142,7 @@ s1.Execute(os.Stdout, VV)
|
||||
採用這種方式來本地化檢視以及資源時,我們就可以很容易的進行擴充套件了。
|
||||
|
||||
## 總結
|
||||
本小節介紹瞭如何使用及儲存本地資源,有時需要透過轉換函式來實現,有時透過 lang 來設定,但是最終都是透過 key-value 的方式來儲存 Locale 對應的資料,在需要時取出相應於 Locale 的資訊後,如果是文字資訊就直接輸出,如果是時間日期或者貨幣,則需要先透過`fmt.Printf`或其他格式化函式來處理,而對於不同 Locale 的檢視和資源則是最簡單的,只要在路徑裡面增加 lang 就可以實現了。
|
||||
本小節介紹了如何使用及儲存本地資源,有時需要透過轉換函式來實現,有時透過 lang 來設定,但是最終都是透過 key-value 的方式來儲存 Locale 對應的資料,在需要時取出相應於 Locale 的資訊後,如果是文字資訊就直接輸出,如果是時間日期或者貨幣,則需要先透過`fmt.Printf`或其他格式化函式來處理,而對於不同 Locale 的檢視和資源則是最簡單的,只要在路徑裡面增加 lang 就可以實現了。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- {% raw %} -->
|
||||
# 10.3 國際化站點
|
||||
前面小節介紹瞭如何處理本地化資源,即 Locale 一個相應的配置檔案,那麼如果處理多個的本地化資源呢?而對於一些我們經常用到的例如:簡單的文字翻譯、時間日期、數字等如果處理呢?本小節將一一解決這些問題。
|
||||
前面小節介紹了如何處理本地化資源,即 Locale 一個相應的配置檔案,那麼如果處理多個的本地化資源呢?而對於一些我們經常用到的例如:簡單的文字翻譯、時間日期、數字等如果處理呢?本小節將一一解決這些問題。
|
||||
## 管理多個本地包
|
||||
在開發一個應用的時候,首先我們要決定是隻支援一種語言,還是多種語言,如果要支援多種語言,我們則需要制定一個組織結構,以方便將來更多語言的新增。在此我們設計如下:Locale 有關的檔案放置在 config/locales 下,假設你要支援中文和英文,那麼你需要在這個資料夾下放置 en.json 和 zh.json。大概的內容如下所示:
|
||||
```json
|
||||
@@ -43,7 +43,7 @@ fmt.Println(Tr.Translate("submit"))
|
||||
```
|
||||
## 自動載入本地套件
|
||||
|
||||
上面我們介紹瞭如何自動載入自訂語言套件,其實 go-i18n 函式庫已經預載入了很多預設的格式資訊,例如時間格式、貨幣格式,使用者可以在自訂配置時改寫這些預設配置,請看下面的處理過程:
|
||||
上面我們介紹了如何自動載入自訂語言套件,其實 go-i18n 函式庫已經預載入了很多預設的格式資訊,例如時間格式、貨幣格式,使用者可以在自訂配置時改寫這些預設配置,請看下面的處理過程:
|
||||
```Go
|
||||
|
||||
//載入預設配置檔案,這些檔案都放在 go-i18n/locales 下面
|
||||
@@ -106,7 +106,7 @@ fmt.Println(Tr.Money(11.11))
|
||||
//輸出:¥11.11
|
||||
```
|
||||
## template mapfunc
|
||||
上面我們實現了多個語言套件的管理和載入,而一些函式的實現是基於邏輯層的,例如:"Tr.Translate"、"Tr.Time"、"Tr.Money"等,雖然我們在邏輯層可以利用這些函式把需要的引數進行轉換後在範本層渲染的時候直接輸出,但是如果我們想在模版層直接使用這些函式該怎麼實現呢?不知你是否還記得,在前面介紹範本的時候說過:Go 語言的範本支援自訂範本函式,下面是我們實現的方便操作的 mapfunc:
|
||||
上面我們實現了多個語言套件的管理和載入,而一些函式的實現是基於邏輯層的,例如:"Tr.Translate"、"Tr.Time"、"Tr.Money"等,雖然我們在邏輯層可以利用這些函式把需要的參數進行轉換後在範本層渲染的時候直接輸出,但是如果我們想在模版層直接使用這些函式該怎麼實現呢?不知你是否還記得,在前面介紹範本的時候說過:Go 語言的範本支援自訂範本函式,下面是我們實現的方便操作的 mapfunc:
|
||||
|
||||
1. 文字資訊
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 10.4 小結
|
||||
透過這一章的介紹,讀者應該對如何操作 i18n 有了深入的瞭解,我也根據這一章介紹的內容實現了一個開源的解決方案 go-i18n:https://github.com/astaxie/go-i18n 透過這個開源函式庫我們可以很方便的實現多語言版本的 Web 應用,使得我們的應用能夠輕鬆的實現國際化。如果你發現這個開源函式庫中的錯誤或者那些缺失的地方,請一起參與到這個開源專案中來,讓我們的這個函式庫爭取成為 Go 的標準函式庫。
|
||||
透過這一章的介紹,讀者應該對如何操作 i18n 有了深入的了解,我也根據這一章介紹的內容實現了一個開源的解決方案 go-i18n:https://github.com/astaxie/go-i18n 透過這個開源函式庫我們可以很方便的實現多語言版本的 Web 應用,使得我們的應用能夠輕鬆的實現國際化。如果你發現這個開源函式庫中的錯誤或者那些缺失的地方,請一起參與到這個開源專案中來,讓我們的這個函式庫爭取成為 Go 的標準函式庫。
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一節: [國際化站點](<10.3.md>)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
但是遺憾的是很多程式設計師不願意在錯誤處理、除錯和測試能力上下工夫,導致後面應用上線之後查詢錯誤、定位問題花費更多的時間。所以我們在設計應用之前就做好錯誤處理規劃、測試案例等,那麼將來修改程式碼、升級系統都將變得簡單。
|
||||
|
||||
開發 Web 應用過程中,錯誤自然難免,那麼如何更好的找到錯誤原因,解決問題呢?11.1 小節將介紹 Go 語言中如何處理錯誤,如何設計自己的套件、函式的錯誤處理,11.2 小節將介紹如何使用 GDB 來除錯我們的程式,動態執行情況下各種變數資訊,執行情況的監控和除錯。
|
||||
開發 Web 應用過程中,錯誤自然難免,那麼如何更好的找到錯誤原因,解決問題呢?11.1 小節將介紹 Go 語言中如何處理錯誤,如何設計自己的套件、函式的錯誤處理,11.2 小節將介紹如何使用 GDB 來除錯我們的程式,動態執行情況下各種變數資訊,執行情況的監聽和除錯。
|
||||
|
||||
11.3 小節將對 Go 語言中的單元測試進行深入的探討,並範例如何來編寫單元測試,Go 的單元測試規則規範如何定義,以保證以後升級修改執行相應的測試程式碼就可以進行最小化的測試。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
## 目錄
|
||||
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 11.1 錯誤處理
|
||||
Go 語言主要的設計準則是:簡潔、明白,簡潔是指語法和 C 類似,相當的簡單,明白是指任何語句都是很明顯的,不含有任何隱含的東西,在錯誤處理方案的設計中也貫徹了這一思想。我們知道在 C 語言裡面是透過返回-1 或者 NULL 之類別的資訊來表示錯誤,但是對於使用者來說,不檢視相應的 API 說明文件,根本搞不清楚這個返回值究竟代表什麼意思,比如 : 返回 0 是成功,還是失敗,而 Go 定義了一個叫做 error 的型別,來顯式表達錯誤。在使用時,透過把返回的 error 變數與 nil 的比較,來判定操作是否成功。例如`os.Open`函式在開啟檔案失敗時將返回一個不為 nil 的 error 變數
|
||||
Go 語言主要的設計準則是:簡潔、明白,簡潔是指語法和 C 類似,相當的簡單,明白是指任何語句都是很明顯的,不含有任何隱含的東西,在錯誤處理方案的設計中也貫徹了這一思想。我們知道在 C 語言裡面是透過回傳-1 或者 NULL 之類別的資訊來表示錯誤,但是對於使用者來說,不檢視相應的 API 說明文件,根本搞不清楚這個回傳值究竟代表什麼意思,比如 : 回傳 0 是成功,還是失敗,而 Go 定義了一個叫做 error 的型別,來明確的表達錯誤。在使用時,透過把回傳的 error 變數與 nil 的比較,來判定操作是否成功。例如`os.Open`函式在開啟檔案失敗時將回傳一個不為 nil 的 error 變數
|
||||
```Go
|
||||
|
||||
func Open(name string) (file *File, err error)
|
||||
@@ -12,7 +12,7 @@ if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
類似於`os.Open`函式,標準套件中所有可能出錯的 API 都會返回一個 error 變數,以方便錯誤處理,這個小節將詳細地介紹 error 型別的設計,和討論開發 Web 應用中如何更好地處理 error。
|
||||
類似於`os.Open`函式,標準套件中所有可能出錯的 API 都會回傳一個 error 變數,以方便錯誤處理,這個小節將詳細地介紹 error 型別的設計,和討論開發 Web 應用中如何更好地處理 error。
|
||||
## Error 型別
|
||||
error 型別是一個介面型別,這是它的定義:
|
||||
```Go
|
||||
@@ -42,7 +42,7 @@ func New(text string) error {
|
||||
return &errorString{text}
|
||||
}
|
||||
```
|
||||
下面這個例子示範瞭如何使用`errors.New`:
|
||||
下面這個例子示範了如何使用`errors.New`:
|
||||
```Go
|
||||
|
||||
func Sqrt(f float64) (float64, error) {
|
||||
@@ -83,7 +83,7 @@ if err := dec.Decode(&val); err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
需要注意的是,函式返回自訂錯誤時,返回值推薦設定為 error 型別,而非自訂錯誤型別,特別需要注意的是不應預宣告自訂錯誤型別的變數。例如:
|
||||
需要注意的是,函式回傳自訂錯誤時,回傳值推薦設定為 error 型別,而非自訂錯誤型別,特別需要注意的是不應預宣告自訂錯誤型別的變數。例如:
|
||||
```Go
|
||||
|
||||
func Decode() *SyntaxError { // 錯誤,將可能導致上層呼叫者 err!=nil 的判斷永遠為 true。
|
||||
@@ -97,7 +97,7 @@ func Decode() *SyntaxError { // 錯誤,將可能導致上層呼叫者 err!=nil
|
||||
```
|
||||
原因見 http://golang.org/doc/faq#nil_error
|
||||
|
||||
上面例子簡單的示範瞭如何自訂 Error 型別。但是如果我們還需要更復雜的錯誤處理呢?此時,我們來參考一下 net 套件採用的方法:
|
||||
上面例子簡單的示範了如何自訂 Error 型別。但是如果我們還需要更復雜的錯誤處理呢?此時,我們來參考一下 net 套件採用的方法:
|
||||
```Go
|
||||
|
||||
package net
|
||||
@@ -121,7 +121,7 @@ if err != nil {
|
||||
}
|
||||
```
|
||||
## 錯誤處理
|
||||
Go 在錯誤處理上採用了與 C 類似的檢查返回值的方式,而不是其他多數主流語言採用的異常方式,這造成了程式碼編寫上的一個很大的缺點 : 錯誤處理程式碼的冗餘,對於這種情況是我們透過複用檢測函式來減少類似的程式碼。
|
||||
Go 在錯誤處理上採用了與 C 類似的檢查回傳值的方式,而不是其他多數主流語言採用的異常方式,這造成了程式碼編寫上的一個很大的缺點 : 錯誤處理程式碼的冗餘,對於這種情況是我們透過複用檢測函式來減少類似的程式碼。
|
||||
|
||||
請看下面這個例子程式碼:
|
||||
```Go
|
||||
@@ -143,7 +143,7 @@ func viewRecord(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
```
|
||||
上面的例子中取得資料和範本展示呼叫時都有檢測錯誤,當有錯誤發生時,呼叫了統一的處理函式`http.Error`,返回給客戶端 500 錯誤碼,並顯示相應的錯誤資料。但是當越來越多的 HandleFunc 加入之後,這樣的錯誤處理邏輯程式碼就會越來越多,其實我們可以透過自訂路由器來縮減程式碼(實現的思路可以參考第三章的 HTTP 詳解)。
|
||||
上面的例子中取得資料和範本展示呼叫時都有檢測錯誤,當有錯誤發生時,呼叫了統一的處理函式`http.Error`,回傳給客戶端 500 錯誤碼,並顯示相應的錯誤資料。但是當越來越多的 HandleFunc 加入之後,這樣的錯誤處理邏輯程式碼就會越來越多,其實我們可以透過自訂路由器來縮減程式碼(實現的思路可以參考第三章的 HTTP 詳解)。
|
||||
```Go
|
||||
|
||||
type appHandler func(http.ResponseWriter, *http.Request) error
|
||||
@@ -174,7 +174,7 @@ func viewRecord(w http.ResponseWriter, r *http.Request) error {
|
||||
return viewTemplate.Execute(w, record)
|
||||
}
|
||||
```
|
||||
上面的例子錯誤處理的時候所有的錯誤返回給使用者的都是 500 錯誤碼,然後打印出來相應的錯誤程式碼,其實我們可以把這個錯誤資訊定義的更加友好,除錯的時候也方便定位問題,我們可以自訂返回的錯誤型別:
|
||||
上面的例子錯誤處理的時候所有的錯誤回傳給使用者的都是 500 錯誤碼,然後顯示出來相應的錯誤程式碼,其實我們可以把這個錯誤資訊定義的更加友好,除錯的時候也方便定位問題,我們可以自訂回傳的錯誤型別:
|
||||
```Go
|
||||
|
||||
type appError struct {
|
||||
|
||||
@@ -15,15 +15,15 @@ GDB 是 FSF(自由軟體基金會)釋出的一個強大的類別 UNIX 系統下
|
||||
|
||||
編譯 Go 程式的時候需要注意以下幾點
|
||||
|
||||
1. 傳遞引數-ldflags "-s",忽略 debug 的列印資訊
|
||||
2. 傳遞-gcflags "-N -l" 引數,這樣可以忽略 Go 內部做的一些優化,聚合變數和函式等優化,這樣對於 GDB 除錯來說非常困難,所以在編譯的時候加入這兩個引數避免這些優化。
|
||||
1. 傳遞參數-ldflags "-s",忽略 debug 的列印資訊
|
||||
2. 傳遞-gcflags "-N -l" 參數,這樣可以忽略 Go 內部做的一些優化,聚合變數和函式等優化,這樣對於 GDB 除錯來說非常困難,所以在編譯的時候加入這兩個參數避免這些優化。
|
||||
|
||||
## 常用命令
|
||||
GDB 的一些常用命令如下所示
|
||||
|
||||
- list
|
||||
|
||||
簡寫命令`l`,用來顯示原始碼,預設顯示十行程式碼,後面可以帶上引數顯示的具體行,例如:`list 15`,顯示十行程式碼,其中第 15 行在顯示的十行裡面的中間,如下所示。
|
||||
簡寫命令`l`,用來顯示原始碼,預設顯示十行程式碼,後面可以帶上參數顯示的具體行,例如:`list 15`,顯示十行程式碼,其中第 15 行在顯示的十行裡面的中間,如下所示。
|
||||
|
||||
10 time.Sleep(2 * time.Second)
|
||||
11 c <- i
|
||||
@@ -39,7 +39,7 @@ GDB 的一些常用命令如下所示
|
||||
|
||||
- break
|
||||
|
||||
簡寫命令 `b`,用來設定斷點,後面跟上引數設定斷點的行數,例如`b 10`在第十行設定斷點。
|
||||
簡寫命令 `b`,用來設定斷點,後面跟上參數設定斷點的行數,例如`b 10`在第十行設定斷點。
|
||||
|
||||
- delete
|
||||
簡寫命令 `d`,用來刪除斷點,後面跟上斷點設定的序號,這個序號可以透過`info breakpoints`取得相應的設定的斷點序號,如下是顯示的設定斷點序號。
|
||||
@@ -58,7 +58,7 @@ GDB 的一些常用命令如下所示
|
||||
#3 0x0000000000000000 in ?? ()
|
||||
- info
|
||||
|
||||
info 命令用來顯示資訊,後面有幾種引數,我們常用的有如下幾種:
|
||||
info 命令用來顯示資訊,後面有幾種參數,我們常用的有如下幾種:
|
||||
|
||||
- `info locals`
|
||||
|
||||
@@ -76,7 +76,7 @@ GDB 的一些常用命令如下所示
|
||||
4 runnable runtime.gosched
|
||||
- print
|
||||
|
||||
簡寫命令`p`,用來列印變數或者其他資訊,後面跟上需要列印的變數名,當然還有一些很有用的函式$len()和$cap(),用來返回當前 string、slices 或者 maps 的長度和容量。
|
||||
簡寫命令`p`,用來列印變數或者其他資訊,後面跟上需要列印的變數名,當然還有一些很有用的函式$len()和$cap(),用來回傳當前 string、slices 或者 maps 的長度和容量。
|
||||
|
||||
- whatis
|
||||
|
||||
@@ -88,7 +88,7 @@ GDB 的一些常用命令如下所示
|
||||
簡寫命令 `n`,用來單步除錯,跳到下一步,當有斷點之後,可以輸入 `n` 跳轉到下一步繼續執行
|
||||
- continue
|
||||
|
||||
簡稱命令 `c`,用來跳出當前斷點處,後面可以跟引數 N,跳過多少次斷點
|
||||
簡稱命令 `c`,用來跳出當前斷點處,後面可以跟參數 N,跳過多少次斷點
|
||||
|
||||
- set variable
|
||||
|
||||
@@ -174,7 +174,7 @@ func main() {
|
||||
24 }
|
||||
25 }
|
||||
|
||||
現在 GDB 在運行當前的程式的環境中已經保留了一些有用的除錯資訊,我們只需打印出相應的變數,檢視相應變數的型別及值:
|
||||
現在 GDB 在運行當前的程式的環境中已經保留了一些有用的除錯資訊,我們只需顯示出相應的變數,檢視相應變數的型別及值:
|
||||
|
||||
(gdb) info locals
|
||||
count = 0
|
||||
@@ -204,7 +204,7 @@ func main() {
|
||||
Breakpoint 1, main.main () at /home/xiemengjun/gdbfile.go:23
|
||||
23 fmt.Println("count:", count)
|
||||
|
||||
每次輸入 `c` 之後都會執行一次程式碼,又跳到下一次 for 迴圈,繼續打印出來相應的資訊。
|
||||
每次輸入 `c` 之後都會執行一次程式碼,又跳到下一次 for 迴圈,繼續顯示出來相應的資訊。
|
||||
|
||||
設想目前需要改變上下文相關變數的資訊,跳過一些過程,並繼續執行下一步,得出修改後想要的結果:
|
||||
|
||||
@@ -241,7 +241,7 @@ func main() {
|
||||
#5 0x000000000040d16a in schedunlock () at /home/xiemengjun/go/src/pkg/runtime/proc.c:267
|
||||
#6 0x0000000000000000 in ?? ()
|
||||
|
||||
透過檢視 goroutines 的命令我們可以清楚地瞭解 goruntine 內部是怎麼執行的,每個函式的呼叫順序已經明明白白地顯示出來了。
|
||||
透過檢視 goroutines 的命令我們可以清楚地了解 goruntine 內部是怎麼執行的,每個函式的呼叫順序已經明明白白地顯示出來了。
|
||||
|
||||
## 小結
|
||||
本小節我們介紹了 GDB 除錯 Go 程式的一些基本命令,包括`run`、`print`、`info`、`set variable`、`coutinue`、`list`、`break` 等經常用到的除錯命令,透過上面的例子示範,我相信讀者已經對於透過 GDB 除錯 Go 程式有了基本的理解,如果你想取得更多的除錯技巧請參考官方網站的 GDB 除錯手冊,還有 GDB 官方網站的手冊。
|
||||
|
||||
@@ -11,7 +11,7 @@ go get -u -v github.com/cweill/gotests/...
|
||||
```
|
||||
|
||||
## 如何編寫測試案例
|
||||
由於`go test`命令只能在一個相應的目錄下執行所有檔案,所以我們接下來新建一個專案目錄`gotest`,這樣我們所有的程式碼和測試程式碼都在這個目錄下。
|
||||
由於`go test`命令只能在一個相應的目錄下執行所有檔案,所以我們接下來建立一個專案目錄`gotest`,這樣我們所有的程式碼和測試程式碼都在這個目錄下。
|
||||
|
||||
接下來我們在該目錄下面建立兩個檔案:gotest.go 和 gotest_test.go
|
||||
|
||||
@@ -41,7 +41,7 @@ go get -u -v github.com/cweill/gotests/...
|
||||
- 你必須 import `testing`這個包
|
||||
- 所有的測試案例函式必須是 `Test` 開頭
|
||||
- 測試案例會按照原始碼中寫的順序依次執行
|
||||
- 測試函式`TestXxx()`的引數是`testing.T`,我們可以使用該型別來記錄錯誤或者是測試狀態
|
||||
- 測試函式`TestXxx()`的參數是`testing.T`,我們可以使用該型別來記錄錯誤或者是測試狀態
|
||||
- 測試格式:`func TestXxx (t *testing.T)`,`Xxx`部分可以為任意的字母數字的組合,但是首字母不能是小寫字母[a-z],例如 `Testintdiv` 是錯誤的函式名。
|
||||
- 函式中透過呼叫`testing.T`的`Error`, `Errorf`, `FailNow`, `Fatal`, `FatalIf`方法,說明測試不透過,呼叫 `Log` 方法用來記錄測試的資訊。
|
||||
|
||||
@@ -76,7 +76,7 @@ go get -u -v github.com/cweill/gotests/...
|
||||
FAIL
|
||||
exit status 1
|
||||
FAIL gotest 0.013s
|
||||
從這個結果顯示測試沒有透過,因為在第二個測試函式中我們寫死了測試不透過的程式碼`t.Error`,那麼我們的第一個函式執行的情況怎麼樣呢?預設情況下執行`go test`是不會顯示測試透過的資訊的,我們需要帶上引數`go test -v`,這樣就會顯示如下資訊:
|
||||
從這個結果顯示測試沒有透過,因為在第二個測試函式中我們寫死了測試不透過的程式碼`t.Error`,那麼我們的第一個函式執行的情況怎麼樣呢?預設情況下執行`go test`是不會顯示測試透過的資訊的,我們需要帶上參數`go test -v`,這樣就會顯示如下資訊:
|
||||
|
||||
=== RUN Test_Division_1
|
||||
--- PASS: Test_Division_1 (0.00 seconds)
|
||||
@@ -120,11 +120,11 @@ go get -u -v github.com/cweill/gotests/...
|
||||
func BenchmarkXXX(b *testing.B) { ... }
|
||||
```
|
||||
|
||||
- `go test`不會預設執行壓力測試的函式,如果要執行壓力測試需要帶上引數`-test.bench`,語法:`-test.bench="test_name_regex"`,例如`go test -test.bench=".*"`表示測試全部的壓力測試函式
|
||||
- `go test`不會預設執行壓力測試的函式,如果要執行壓力測試需要帶上參數`-test.bench`,語法:`-test.bench="test_name_regex"`,例如`go test -test.bench=".*"`表示測試全部的壓力測試函式
|
||||
- 在壓力測試案例中,請記得在迴圈體內使用`testing.B.N`,以使測試可以正常的執行
|
||||
- 檔名也必須以`_test.go`結尾
|
||||
|
||||
下面我們新建一個壓力測試檔案 webbench_test.go,程式碼如下所示:
|
||||
下面我們建立一個壓力測試檔案 webbench_test.go,程式碼如下所示:
|
||||
|
||||
```Go
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 11.4 小結
|
||||
本章我們透過三個小節分別介紹了 Go 語言中如何處理錯誤,如何設計錯誤處理,然後第二小節介紹瞭如何透過 GDB 來除錯程式,透過 GDB 我們可以單步除錯、可以檢視變數、修改變數、列印執行過程等,最後我們介紹瞭如何利用 Go 語言自帶的輕量級框架 `testing` 來編寫單元測試和壓力測試,使用`go test`就可以方便的執行這些測試,使得我們將來程式碼升級修改之後很方便的進行迴歸測試。這一章也許對於你編寫程式邏輯沒有任何幫助,但是對於你編寫出來的程式程式碼保持高品質是至關重要的,因為一個好的 Web 應用必定有良好的錯誤處理機制(錯誤提示的友好、可擴充套件性)、有好的單元測試和壓力測試以保證上線之後程式碼能夠保持良好的效能和按預期的執行。
|
||||
本章我們透過三個小節分別介紹了 Go 語言中如何處理錯誤,如何設計錯誤處理,然後第二小節介紹了如何透過 GDB 來除錯程式,透過 GDB 我們可以單步除錯、可以檢視變數、修改變數、列印執行過程等,最後我們介紹了如何利用 Go 語言自帶的輕量級框架 `testing` 來編寫單元測試和壓力測試,使用`go test`就可以方便的執行這些測試,使得我們將來程式碼升級修改之後很方便的進行迴歸測試。這一章也許對於你編寫程式邏輯沒有任何幫助,但是對於你編寫出來的程式程式碼保持高品質是至關重要的,因為一個好的 Web 應用必定有良好的錯誤處理機制(錯誤提示的友好、可擴充套件性)、有好的單元測試和壓力測試以保證上線之後程式碼能夠保持良好的效能和按預期的執行。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# 12 部署與維護
|
||||
到目前為止,我們前面已經介紹瞭如何開發程式、除錯程式以及測試程式,正如人們常說的:開發最後的 10%需要花費 90%的時間,所以這一章我們將強調這最後的 10%部分,要真正成為讓人信任並使用的優秀應用,需要考慮到一些細節,以上所說的 10%就是指這些小細節。
|
||||
到目前為止,我們前面已經介紹了如何開發程式、除錯程式以及測試程式,正如人們常說的:開發最後的 10%需要花費 90%的時間,所以這一章我們將強調這最後的 10%部分,要真正成為讓人信任並使用的優秀應用,需要考慮到一些細節,以上所說的 10%就是指這些小細節。
|
||||
|
||||
本章我們將透過四個小節來介紹這些小細節的處理,第一小節介紹如何在生產服務上記錄程式產生的日誌,如何記錄日誌,第二小節介紹發生錯誤時我們的程式如何處理,如何保證儘量少的影響到使用者的訪問,第三小節介紹如何來部署 Go 的獨立程式,由於目前 Go 程式還無法像 C 那樣寫成 daemon,那麼我們如何管理這樣的程序程式後臺執行呢?第四小節將介紹應用資料的備份和恢復,儘量保證應用在崩潰的情況能夠保持資料的完整性。
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -68,7 +68,7 @@ func main() {
|
||||
}).Fatal("The ice breaks!")
|
||||
|
||||
// 透過日誌語句重用欄位
|
||||
// logrus.Entry 返回自 WithFields()
|
||||
// logrus.Entry 回傳自 WithFields()
|
||||
contextLogger := log.WithFields(log.Fields{
|
||||
"common": "this is a common field",
|
||||
"other": "I also should be logged always",
|
||||
@@ -187,7 +187,7 @@ func UseLogger(newLogger seelog.LoggerInterface) {
|
||||
|
||||
- seelog
|
||||
|
||||
minlevel 引數可選,如果被配置,高於或等於此級別的日誌會被記錄,同理 maxlevel。
|
||||
minlevel 參數可選,如果被配置,高於或等於此級別的日誌會被記錄,同理 maxlevel。
|
||||
- outputs
|
||||
|
||||
輸出資訊的目的地,這裡分成了兩份資料,一份記錄到 log rotate 檔案裡面。另一份設定了 filter,如果這個錯誤級別是 critical,那麼將傳送報警郵件。
|
||||
@@ -220,7 +220,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
## 發生錯誤傳送郵件
|
||||
上面的例子解釋瞭如何設定傳送郵件,我們透過如下的 smtp 配置用來發送郵件:
|
||||
上面的例子解釋了如何設定傳送郵件,我們透過如下的 smtp 配置用來發送郵件:
|
||||
```html
|
||||
|
||||
<smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword">
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
在實現錯誤處理之前,我們必須明確錯誤處理想要達到的目標是什麼,錯誤處理系統應該完成以下工作:
|
||||
|
||||
- 通知訪問使用者出現錯誤了:不論出現的是一個系統錯誤還是使用者錯誤,使用者都應當知道 Web 應用出了問題,使用者的這次請求無法正確的完成了。例如,對於使用者的錯誤請求,我們顯示一個統一的錯誤頁面(404.html)。出現系統錯誤時,我們透過自訂的錯誤頁面顯示系統暫時不可用之類別的錯誤頁面(error.html)。
|
||||
- 記錄錯誤:系統出現錯誤,一般就是我們呼叫函式的時候返回 err 不為 nil 的情況,可以使用前面小節介紹的日誌系統記錄到日誌檔案。如果是一些致命錯誤,則透過郵件通知系統管理員。一般 404 之類別的錯誤不需要傳送郵件,只需要記錄到日誌系統。
|
||||
- 回滾當前的請求操作:如果一個使用者請求過程中出現了一個伺服器錯誤,那麼已完成的操作需要回滾。下面來看一個例子:一個系統將使用者遞交的表單儲存到資料庫,並將這個資料遞交到一個第三方伺服器,但是第三方伺服器掛了,這就導致一個錯誤,那麼先前儲存到資料庫的表單資料應該刪除(應告知無效),而且應該通知使用者系統出現錯誤了。
|
||||
- 記錄錯誤:系統出現錯誤,一般就是我們呼叫函式的時候回傳 err 不為 nil 的情況,可以使用前面小節介紹的日誌系統記錄到日誌檔案。如果是一些致命錯誤,則透過郵件通知系統管理員。一般 404 之類別的錯誤不需要傳送郵件,只需要記錄到日誌系統。
|
||||
- 回復 (Rollback)當前的請求操作:如果一個使用者請求過程中出現了一個伺服器錯誤,那麼已完成的操作需要回復 (Rollback)。下面來看一個例子:一個系統將使用者提交的表單儲存到資料庫,並將這個資料提交到一個第三方伺服器,但是第三方伺服器掛了,這就導致一個錯誤,那麼先前儲存到資料庫的表單資料應該刪除(應告知無效),而且應該通知使用者系統出現錯誤了。
|
||||
- 保證現有程式可執行可服務:我們知道沒有人能保證程式一定能夠一直正常的執行著,萬一哪一天程式崩潰了,那麼我們就需要記錄錯誤,然後立刻讓程式重新執行起來,讓程式繼續提供服務,然後再通知系統管理員,透過日誌等找出問題。
|
||||
|
||||
## 如何處理錯誤
|
||||
@@ -109,9 +109,9 @@
|
||||
|
||||
```
|
||||
## 如何處理異常
|
||||
我們知道在很多其他語言中有 try...catch 關鍵詞,用來捕獲異常情況,但是其實很多錯誤都是可以預期發生的,而不需要異常處理,應該當做錯誤來處理,這也是為什麼 Go 語言採用了函式返回錯誤的設計,這些函式不會 panic,例如如果一個檔案找不到,os.Open 返回一個錯誤,它不會 panic;如果你向一箇中斷的網路連線寫資料,net.Conn 系列型別的 Write 函式返回一個錯誤,它們不會 panic。這些狀態在這樣的程式裡都是可以預期的。你知道這些操作可能會失敗,因為設計者已經用返回錯誤清楚地表明瞭這一點。這就是上面所講的可以預期發生的錯誤。
|
||||
我們知道在很多其他語言中有 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 機制。
|
||||
但是還有一種情況,有一些操作幾乎不可能失敗,而且在一些特定的情況下也沒有辦法回傳錯誤,也無法繼續執行,這樣情況就應該 panic。舉個例子:如果一個程式計算 x[j],但是 j 越界了,這部分程式碼就會導致 panic,像這樣的一個不可預期嚴重錯誤就會引起 panic,在預設情況下它會殺掉程序,它允許一個正在執行這部分程式碼的 goroutine 從發生錯誤的 panic 中恢復執行,發生 panic 之後,這部分程式碼後面的函式和程式碼都不會繼續執行,這是 Go 特意這樣設計的,因為要區別於錯誤和異常,panic 其實就是異常處理。如下程式碼,我們期望透過 uid 來取得 User 中的 username 資訊,但是如果 uid 越界了就會丟擲異常,這個時候如果我們沒有 recover 機制,程序就會被殺死,從而導致程式不可服務。因此為了程式的健壯性,在一些地方需要建立 recover 機制。
|
||||
```Go
|
||||
|
||||
func GetUser(uid int) (username string) {
|
||||
@@ -125,10 +125,10 @@ func GetUser(uid int) (username string) {
|
||||
return
|
||||
}
|
||||
```
|
||||
上面介紹了錯誤和異常的區別,那麼我們在開發程式的時候如何來設計呢?規則很簡單:如果你定義的函式有可能失敗,它就應該返回一個錯誤。當我呼叫其他 package 的函式時,如果這個函式實現的很好,我不需要擔心它會 panic,除非有真正的異常情況發生,即使那樣也不應該是我去處理它。而 panic 和 recover 是針對自己開發 package 裡面實現的邏輯,針對一些特殊情況來設計。
|
||||
上面介紹了錯誤和異常的區別,那麼我們在開發程式的時候如何來設計呢?規則很簡單:如果你定義的函式有可能失敗,它就應該回傳一個錯誤。當我呼叫其他 package 的函式時,如果這個函式實現的很好,我不需要擔心它會 panic,除非有真正的異常情況發生,即使那樣也不應該是我去處理它。而 panic 和 recover 是針對自己開發 package 裡面實現的邏輯,針對一些特殊情況來設計。
|
||||
|
||||
## 小結
|
||||
本小節總結了當我們的 Web 應用部署之後如何處理各種錯誤:網路錯誤、資料庫錯誤、作業系統錯誤等,當錯誤發生時,我們的程式如何來正確處理:顯示友好的出錯介面、回滾操作、記錄日誌、通知管理員等操作,最後介紹瞭如何來正確處理錯誤和異常。一般的程式中錯誤和異常很容易混淆的,但是在 Go 中錯誤和異常是有明顯的區分,所以告訴我們在程式設計中處理錯誤和異常應該遵循怎麼樣的原則。
|
||||
本小節總結了當我們的 Web 應用部署之後如何處理各種錯誤:網路錯誤、資料庫錯誤、作業系統錯誤等,當錯誤發生時,我們的程式如何來正確處理:顯示友好的出錯介面、回復 (Rollback)操作、記錄日誌、通知管理員等操作,最後介紹了如何來正確處理錯誤和異常。一般的程式中錯誤和異常很容易混淆的,但是在 Go 中錯誤和異常是有明顯的區分,所以告訴我們在程式設計中處理錯誤和異常應該遵循怎麼樣的原則。
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
* 上一章: [應用日誌](<12.1.md>)
|
||||
|
||||
@@ -30,7 +30,7 @@ rsync 主要有以下三個配置檔案 rsyncd.conf(主配置檔案)、rsyncd.se
|
||||
|
||||
#/usr/bin/rsync --daemon --config=/etc/rsyncd.conf
|
||||
|
||||
--daemon 引數方式,是讓 rsync 以伺服器模式執行。把 rsync 加入開機啟動
|
||||
--daemon 參數方式,是讓 rsync 以伺服器模式執行。把 rsync 加入開機啟動
|
||||
|
||||
echo 'rsync --daemon' >> /etc/rc.d/rc.local
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 12.5 小結
|
||||
本章討論瞭如何部署和維護我們開發的 Web 應用相關的一些話題。這些內容非常重要,要建立一個能夠基於最小維護平滑執行的應用,必須考慮這些問題。
|
||||
本章討論了如何部署和維護我們開發的 Web 應用相關的一些話題。這些內容非常重要,要建立一個能夠基於最小維護平滑執行的應用,必須考慮這些問題。
|
||||
|
||||
具體而言,本章討論的內容包括:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- 如何讓部署的應用程式具有高可用
|
||||
- 備份和恢復檔案以及資料庫
|
||||
|
||||
讀完本章內容後,對於從頭開始開發一個 Web 應用需要考慮那些問題,你應該已經有了全面的瞭解。本章內容將有助於你在實際環境中管理前面各章介紹開發的程式碼。
|
||||
讀完本章內容後,對於從頭開始開發一個 Web 應用需要考慮那些問題,你應該已經有了全面的了解。本章內容將有助於你在實際環境中管理前面各章介紹開發的程式碼。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 13 如何設計一個 Web 框架
|
||||
前面十二章介紹瞭如何透過 Go 來開發 Web 應用,介紹了很多基礎知識、開發工具和開發技巧,那麼我們這一章透過這些知識來實現一個簡易的 Web 框架。透過 Go 語言來實現一個完整的框架設計,這框架中主要內容有第一小節介紹的 Web 框架的結構規劃,例如採用 MVC 模式來進行開發,程式的執行流程設計等內容;第二小節介紹框架的第一個功能:路由,如何讓訪問的 URL 對映到相應的處理邏輯;第三小節介紹處理邏輯,如何設計一個公共的 controller,物件繼承之後處理函式中如何處理 response 和 request;第四小節介紹框架的一些輔助功能,例如日誌處理、配置資訊等;第五小節介紹如何基於 Web 框架實現一個部落格,包括博文的發表、修改、刪除、顯示列表等操作。
|
||||
前面十二章介紹了如何透過 Go 來開發 Web 應用,介紹了很多基礎知識、開發工具和開發技巧,那麼我們這一章透過這些知識來實現一個簡易的 Web 框架。透過 Go 語言來實現一個完整的框架設計,這框架中主要內容有第一小節介紹的 Web 框架的結構規劃,例如採用 MVC 模式來進行開發,程式的執行流程設計等內容;第二小節介紹框架的第一個功能:路由,如何讓訪問的 URL 對映到相應的處理邏輯;第三小節介紹處理邏輯,如何設計一個公共的 controller,物件繼承之後處理函式中如何處理 response 和 request;第四小節介紹框架的一些輔助功能,例如日誌處理、配置資訊等;第五小節介紹如何基於 Web 框架實現一個部落格,包括博文的發表、修改、刪除、顯示列表等操作。
|
||||
|
||||
透過這麼一個完整的專案例子,我期望能夠讓讀者瞭解如何開發 Web 應用,如何建立自己的目錄結構,如何實現路由,如何實現 MVC 模式等各方面的開發內容。在框架盛行的今天,MVC 也不再是神話。經常聽到很多程式設計師討論哪個框架好,哪個框架不好, 其實框架只是工具,沒有好與不好,只有適合與不適合,適合自己的就是最好的,所以教會大家自己動手寫框架,那麼不同的需求都可以用自己的思路去實現。
|
||||
透過這麼一個完整的專案例子,我期望能夠讓讀者了解如何開發 Web 應用,如何建立自己的目錄結構,如何實現路由,如何實現 MVC 模式等各方面的開發內容。在框架盛行的今天,MVC 也不再是神話。經常聽到很多程式設計師討論哪個框架好,哪個框架不好, 其實框架只是工具,沒有好與不好,只有適合與不適合,適合自己的就是最好的,所以教會大家自己動手寫框架,那麼不同的需求都可以用自己的思路去實現。
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# 13.1 專案規劃
|
||||
做任何事情都需要做好規劃,那麼我們在開發部落格系統之前,同樣需要做好專案的規劃,如何設定目錄結構,如何理解整個專案的流程圖,當我們理解了應用的執行過程,那麼接下來的設計編碼就會變得相對容易了
|
||||
## gopath 以及專案設定
|
||||
假設指定 gopath 是檔案系統的普通目錄名,當然我們可以隨便設定一個目錄名,然後將其路徑存入 GOPATH。前面介紹過 GOPATH 可以是多個目錄:在 window 系統設定環境變數;在 linux/MacOS 系統只要輸入終端命令`export gopath=/home/astaxie/gopath`,但是必須保證 gopath 這個程式碼目錄下面有三個目錄 pkg、bin、src。新建專案的原始碼放在 src 目錄下面,現在暫定我們的部落格目錄叫做 beeblog,下面是在 window 下的環境變數和目錄結構的截圖:
|
||||
假設指定 gopath 是檔案系統的普通目錄名,當然我們可以隨便設定一個目錄名,然後將其路徑存入 GOPATH。前面介紹過 GOPATH 可以是多個目錄:在 window 系統設定環境變數;在 linux/MacOS 系統只要輸入終端命令`export gopath=/home/astaxie/gopath`,但是必須保證 gopath 這個程式碼目錄下面有三個目錄 pkg、bin、src。建立專案的原始碼放在 src 目錄下面,現在暫定我們的部落格目錄叫做 beeblog,下面是在 window 下的環境變數和目錄結構的截圖:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 13.1 環境變數 GOPATH 設定
|
||||
|
||||

|
||||

|
||||
|
||||
圖 13.2 工作目錄在$gopath/src 下
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
下圖顯示了專案設計中框架的資料流是如何貫穿整個系統:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 13.3 框架的資料流
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
|
||||
```
|
||||
上面的例子呼叫了 http 預設的 DefaultServeMux 來新增路由,需要提供兩個引數,第一個引數是希望使用者訪問此資源的 URL 路徑(儲存在 r.URL.Path),第二引數是即將要執行的函式,以提供使用者訪問的資源。路由的思路主要集中在兩點:
|
||||
上面的例子呼叫了 http 預設的 DefaultServeMux 來新增路由,需要提供兩個參數,第一個參數是希望使用者訪問此資源的 URL 路徑(儲存在 r.URL.Path),第二參數是即將要執行的函式,以提供使用者訪問的資源。路由的思路主要集中在兩點:
|
||||
|
||||
- 新增路由資訊
|
||||
- 根據使用者請求轉發到要執行的函式
|
||||
@@ -48,14 +48,14 @@ for k, v := range mux.m {
|
||||
## beego 框架路由實現
|
||||
目前幾乎所有的 Web 應用路由實現都是基於 http 預設的路由器,但是 Go 自帶的路由器有幾個限制:
|
||||
|
||||
- 不支援引數設定,例如/user/:uid 這種泛型別匹配
|
||||
- 不支援參數設定,例如/user/:uid 這種泛型別匹配
|
||||
- 無法很好的支援 REST 模式,無法限制訪問的方法,例如上面的例子中,使用者訪問/foo,可以用 GET、POST、DELETE、HEAD 等方式訪問
|
||||
- 一般網站的路由規則太多了,編寫繁瑣。我前面自己開發了一個 API 應用,路由規則有三十幾條,這種路由多了之後其實可以進一步簡化,透過 struct 的方法進行一種簡化
|
||||
|
||||
beego 框架的路由器基於上面的幾點限制考慮設計了一種 REST 方式的路由實現,路由設計也是基於上面 Go 預設設計的兩點來考慮:儲存路由和轉發路由
|
||||
|
||||
### 儲存路由
|
||||
針對前面所說的限制點,我們首先要解決引數支援就需要用到正則,第二和第三點我們透過一種變通的方法來解決,REST 的方法對應到 struct 的方法中去,然後路由到 struct 而不是函式,這樣在轉發路由的時候就可以根據 method 來執行不同的方法。
|
||||
針對前面所說的限制點,我們首先要解決參數支援就需要用到正則,第二和第三點我們透過一種變通的方法來解決,REST 的方法對應到 struct 的方法中去,然後路由到 struct 而不是函式,這樣在轉發路由的時候就可以根據 method 來執行不同的方法。
|
||||
|
||||
根據上面的思路,我們設計了兩個資料型別 controllerInfo(儲存路徑和對應的 struct,這裡是一個 reflect.Type 型別)和 ControllerRegistor(routers 是一個 slice 用來儲存使用者新增的路由資訊,以及 beego 框架的應用資訊)
|
||||
```Go
|
||||
@@ -260,7 +260,7 @@ func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
beego.BeeApp.RegisterController("/", &controllers.MainController{})
|
||||
```
|
||||
引數註冊:
|
||||
參數註冊:
|
||||
```Go
|
||||
|
||||
beego.BeeApp.RegisterController("/:param", &controllers.UserController{})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
傳統的 MVC 框架大多數是基於 Action 設計的字尾式對映,然而,現在 Web 流行 REST 風格的架構。儘管使用 Filter 或者 rewrite 能夠透過 URL 重寫實現 REST 風格的 URL,但是為什麼不直接設計一個全新的 REST 風格的 MVC 框架呢?本小節就是基於這種思路來講述如何從頭設計一個基於 REST 風格的 MVC 框架中的 controller,最大限度地簡化 Web 應用的開發,甚至編寫一行程式碼就可以實現“Hello, world”。
|
||||
|
||||
## controller 作用
|
||||
MVC 設計模式是目前 Web 應用開發中最常見的架構模式,透過分離 Model(模型)、View(檢視)和 Controller(控制器),可以更容易實現易於擴充套件的使用者介面(UI)。Model 指後臺返回的資料;View 指需要渲染的頁面,通常是範本頁面,渲染後的內容通常是 HTML;Controller 指 Web 開發人員編寫的處理不同 URL 的控制器,如前面小節講述的路由就是 URL 請求轉發到控制器的過程,controller 在整個的 MVC 框架中起到了一個核心的作用,負責處理業務邏輯,因此控制器是整個框架中必不可少的一部分,Model 和 View 對於有些業務需求是可以不寫的,例如沒有資料處理的邏輯處理,沒有頁面輸出的 302 調整之類別的就不需要 Model 和 View,但是 controller 這一環節是必不可少的。
|
||||
MVC 設計模式是目前 Web 應用開發中最常見的架構模式,透過分離 Model(模型)、View(檢視)和 Controller(控制器),可以更容易實現易於擴充套件的使用者介面(UI)。Model 指後臺回傳的資料;View 指需要渲染的頁面,通常是範本頁面,渲染後的內容通常是 HTML;Controller 指 Web 開發人員編寫的處理不同 URL 的控制器,如前面小節講述的路由就是 URL 請求轉發到控制器的過程,controller 在整個的 MVC 框架中起到了一個核心的作用,負責處理業務邏輯,因此控制器是整個框架中必不可少的一部分,Model 和 View 對於有些業務需求是可以不寫的,例如沒有資料處理的邏輯處理,沒有頁面輸出的 302 調整之類別的就不需要 Model 和 View,但是 controller 這一環節是必不可少的。
|
||||
|
||||
## beego 的 REST 設計
|
||||
前面小節介紹了路由實現了註冊 struct 的功能,而 struct 中實現了 REST 方式,因此我們需要設計一個用於邏輯處理 controller 的基底類別,這裡主要設計了兩個型別,一個 struct、一個 interface
|
||||
@@ -147,9 +147,9 @@ func (this *MainController) Get() {
|
||||
this.TplNames = "index.tpl"
|
||||
}
|
||||
```
|
||||
上面的方式我們實現了子類別 MainController,實現了 Get 方法,那麼如果使用者透過其他的方式(POST/HEAD 等)來訪問該資源都將返回 405,而如果是 Get 來訪問,因為我們設定了 AutoRender=true,那麼在執行完 Get 方法之後會自動執行 Render 函式,就會顯示如下介面:
|
||||
上面的方式我們實現了子類別 MainController,實現了 Get 方法,那麼如果使用者透過其他的方式(POST/HEAD 等)來訪問該資源都將回傳 405,而如果是 Get 來訪問,因為我們設定了 AutoRender=true,那麼在執行完 Get 方法之後會自動執行 Render 函式,就會顯示如下介面:
|
||||
|
||||

|
||||

|
||||
|
||||
index.tpl 的程式碼如下所示,我們可以看到資料的設定和顯示都是相當的簡單方便:
|
||||
```html
|
||||
|
||||
@@ -116,11 +116,11 @@ func Critical(v ...interface{}) {
|
||||
可以看到每個函式裡面都有對 level 的判斷,所以如果我們在部署的時候設定了 level=LevelWarning,那麼 Trace、Debug、Info 這三個函式都不會有任何的輸出,以此類推。
|
||||
|
||||
## beego 的配置設計
|
||||
配置資訊的解析,beego 實現了一個 key=value 的配置檔案讀取,類似 ini 配置檔案的格式,就是一個檔案解析的過程,然後把解析的資料儲存到 map 中,最後在呼叫的時候通過幾個 string、int 之類別的函式呼叫返回相應的值,具體的實現請看下面:
|
||||
|
||||
首先定義了一些 ini 配置檔案的一些全域性性常量 :
|
||||
配置資訊的解析,beego 實現了一個 key=value 的配置檔案讀取,類似 ini 配置檔案的格式,就是一個檔案解析的過程,然後把解析的資料儲存到 map 中,最後在呼叫的時候通過幾個 string、int 之類別的函式呼叫回傳相應的值,具體的實現請看下面:
|
||||
|
||||
首先定義了一些 ini 配置檔案的一些全域常數:
|
||||
```Go
|
||||
|
||||
var (
|
||||
bComment = []byte{'#'}
|
||||
bEmpty = []byte{}
|
||||
@@ -140,7 +140,7 @@ type Config struct {
|
||||
sync.RWMutex
|
||||
}
|
||||
```
|
||||
定義瞭解析檔案的函式,解析檔案的過程是開啟檔案,然後一行一行的讀取,解析註釋、空行和 key=value 資料:
|
||||
定義了解析檔案的函式,解析檔案的過程是開啟檔案,然後一行一行的讀取,解析註釋、空行和 key=value 資料:
|
||||
```Go
|
||||
|
||||
// ParseFile creates a new Config and parses the file configuration from the
|
||||
@@ -202,7 +202,7 @@ func LoadConfig(name string) (*Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
```
|
||||
下面實現了一些讀取配置檔案的函式,返回的值確定為 bool、int、float64 或 string:
|
||||
下面實現了一些讀取配置檔案的函式,回傳的值確定為 bool、int、float64 或 string:
|
||||
```Go
|
||||
|
||||
// Bool returns the boolean value for a given key.
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
beego.Router("/", &controllers.IndexController{})
|
||||
//檢視部落格詳細資訊
|
||||
beego.Router("/view/:id([0-9]+)", &controllers.ViewController{})
|
||||
//新建部落格博文
|
||||
//建立部落格博文
|
||||
beego.Router("/new", &controllers.NewController{})
|
||||
//刪除博文
|
||||
beego.Router("/delete/:id([0-9]+)", &controllers.DeleteController{})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 13.6 小結
|
||||
這一章我們主要介紹瞭如何實現一個基礎的 Go 語言框架,框架包含有路由設計,由於 Go 內建的 http 套件中路由的一些不足點,我們設計了動態路由規則,然後介紹了 MVC 模式中的 Controller 設計,controller 實現了 REST 的實現,這個主要思路來源於 tornado 框架,然後設計實現了範本的 layout 以及自動化渲染等技術,主要採用了 Go 內建的範本引擎,最後我們介紹了一些輔助的日誌、配置等資訊的設計,透過這些設計我們實現了一個基礎的框架 beego,目前該框架已經開源在 github,最後我們透過 beego 實現了一個部落格系統,透過例項程式碼詳細的展現瞭如何快速的開發一個站點。
|
||||
這一章我們主要介紹了如何實現一個基礎的 Go 語言框架,框架包含有路由設計,由於 Go 內建的 http 套件中路由的一些不足點,我們設計了動態路由規則,然後介紹了 MVC 模式中的 Controller 設計,controller 實現了 REST 的實現,這個主要思路來源於 tornado 框架,然後設計實現了範本的 layout 以及自動化渲染等技術,主要採用了 Go 內建的範本引擎,最後我們介紹了一些輔助的日誌、配置等資訊的設計,透過這些設計我們實現了一個基礎的框架 beego,目前該框架已經開源在 github,最後我們透過 beego 實現了一個部落格系統,透過範例程式碼詳細的展現了如何快速的開發一個站點。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# 14 擴充套件 Web 框架
|
||||
第十三章介紹瞭如何開發一個 Web 框架,透過介紹 MVC、路由、日誌處理、配置處理完成了一個基本的框架系統,但是一個好的框架需要一些方便的輔助工具來快速的開發 Web,那麼我們這一章將就如何提供一些快速開發 Web 的工具進行介紹,第一小節介紹如何處理靜態檔案,如何利用現有的 twitter 開源的 bootstrap 進行快速的開發美觀的站點,第二小節介紹如何利用前面介紹的 session 來進行使用者登入處理,第三小節介紹如何方便的輸出表單、這些表單如何進行資料驗證,如何快速的結合 model 進行資料的增刪改操作,第四小節介紹如何進行一些使用者認證,包括 http basic 認證、http digest 認證,第五小節介紹如何利用前面介紹的 i18n 支援多語言的應用開發。第六小節介紹瞭如何整合 Go 的 pprof 套件用於效能除錯。
|
||||
第十三章介紹了如何開發一個 Web 框架,透過介紹 MVC、路由、日誌處理、配置處理完成了一個基本的框架系統,但是一個好的框架需要一些方便的輔助工具來快速的開發 Web,那麼我們這一章將就如何提供一些快速開發 Web 的工具進行介紹,第一小節介紹如何處理靜態檔案,如何利用現有的 twitter 開源的 bootstrap 進行快速的開發美觀的站點,第二小節介紹如何利用前面介紹的 session 來進行使用者登入處理,第三小節介紹如何方便的輸出表單、這些表單如何進行資料驗證,如何快速的結合 model 進行資料的增刪改操作,第四小節介紹如何進行一些使用者認證,包括 http basic 認證、http digest 認證,第五小節介紹如何利用前面介紹的 i18n 支援多語言的應用開發。第六小節介紹了如何整合 Go 的 pprof 套件用於效能除錯。
|
||||
|
||||
透過本章的擴充套件,beego 框架將具有快速開發 Web 的特性,最後我們將講解如何利用這些擴充套件的特性擴充套件開發第十三章開發的部落格系統,透過開發一個完整、美觀的部落格系統讓讀者瞭解 beego 開發帶給你的快速。
|
||||
透過本章的擴充套件,beego 框架將具有快速開發 Web 的特性,最後我們將講解如何利用這些擴充套件的特性擴充套件開發第十三章開發的部落格系統,透過開發一個完整、美觀的部落格系統讓讀者了解 beego 開發帶給你的快速。
|
||||
|
||||
## 目錄
|
||||

|
||||

|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -36,7 +36,7 @@ Bootstrap 是 Twitter 推出的一個開源的用於前端開發的工具套件
|
||||
- 訂製自己的框架程式碼
|
||||
可以對 Bootstrap 中所有的 CSS 變數進行修改,依據自己的需求裁剪程式碼。
|
||||
|
||||

|
||||

|
||||
|
||||
圖 14.1 bootstrap 站點
|
||||
|
||||
@@ -44,7 +44,7 @@ Bootstrap 是 Twitter 推出的一個開源的用於前端開發的工具套件
|
||||
|
||||
1. 首先把下載的 bootstrap 目錄放到我們的專案目錄,取名為 static,如下截圖所示
|
||||
|
||||

|
||||

|
||||
|
||||
圖 14.2 專案中靜態檔案目錄結構
|
||||
|
||||
@@ -68,7 +68,7 @@ StaticDir["/static"] = "static"
|
||||
```
|
||||
上面可以實現把 bootstrap 整合到 beego 中來,如下展示的圖就是整合進來之後的展現效果圖:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 14.3 建構的基於 bootstrap 的站點介面
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ if SessionOn {
|
||||
```
|
||||
這樣只要 SessionOn 設定為 true,那麼就會預設開啟 session 功能,獨立開一個 goroutine 來處理 session。
|
||||
|
||||
為了方便我們在自訂 Controller 中快速使用 session,作者在`beego.Controller`中提供瞭如下方法:
|
||||
為了方便我們在自訂 Controller 中快速使用 session,作者在`beego.Controller`中提供了如下方法:
|
||||
```Go
|
||||
|
||||
func (c *Controller) StartSession() (sess session.Session) {
|
||||
@@ -86,7 +86,7 @@ func (this *MainController) Get() {
|
||||
this.TplNames = "index.tpl"
|
||||
}
|
||||
```
|
||||
上面的程式碼展示瞭如何在控制邏輯中使用 session,主要分兩個步驟:
|
||||
上面的程式碼展示了如何在控制邏輯中使用 session,主要分兩個步驟:
|
||||
|
||||
1. 取得 session 物件
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
- 開啟一個網頁顯示出表單。
|
||||
- 使用者填寫並提交了表單。
|
||||
- 如果使用者提交了一些無效的資訊,或者可能漏掉了一個必填項,表單將會連同使用者的資料和錯誤問題的描述資訊返回。
|
||||
- 如果使用者提交了一些無效的資訊,或者可能漏掉了一個必填項,表單將會連同使用者的資料和錯誤問題的描述資訊回傳。
|
||||
- 使用者再次填寫,繼續上一步過程,直到提交了一個有效的表單。
|
||||
|
||||
在接收端,指令碼必須:
|
||||
|
||||
- 檢查使用者遞交的表單資料。
|
||||
- 檢查使用者提交的表單資料。
|
||||
- 驗證資料是否為正確的型別,合適的標準。例如,如果一個使用者名稱被提交,它必須被驗證是否只包含了允許的字元。它必須有一個最小長度,不能超過最大長度。使用者名稱不能與已存在的他人使用者名稱重複,甚至是一個保留字等。
|
||||
- 過濾資料並清理不安全字元,保證邏輯處理中接收的資料是安全的。
|
||||
- 如果需要,預格式化資料(資料需要清除空白或者經過 HTML 編碼等等。)
|
||||
@@ -66,7 +66,7 @@ func (this *AddController) Post() {
|
||||
<table cellpadding="0" cellspacing="1" border="0" style="width:100%" class="tableborder">
|
||||
<tbody><tr>
|
||||
<th>名稱</th>
|
||||
<th>引數</th>
|
||||
<th>參數</th>
|
||||
<th>功能描述</th>
|
||||
</tr>
|
||||
|
||||
@@ -132,7 +132,7 @@ func (this *AddController) Post() {
|
||||
<table cellpadding="0" cellspacing="1" border="0" style="width:100%" class="tableborder">
|
||||
<tbody><tr>
|
||||
<th>規則</th>
|
||||
<th>引數</th>
|
||||
<th>參數</th>
|
||||
<th>描述</th>
|
||||
<th>舉例</th>
|
||||
</tr>
|
||||
@@ -140,140 +140,140 @@ func (this *AddController) Post() {
|
||||
<tr>
|
||||
<td class="td"><strong>required</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果元素為空,則返回 FALSE</td>
|
||||
<td class="td">如果元素為空,則回傳 FALSE</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>matches</strong></td>
|
||||
<td class="td">Yes</td>
|
||||
<td class="td">如果表單元素的值與引數中對應的表單欄位的值不相等,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素的值與參數中對應的表單欄位的值不相等,則回傳 FALSE</td>
|
||||
<td class="td">matches[form_item]</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>is_unique</strong></td>
|
||||
<td class="td">Yes</td>
|
||||
<td class="td">如果表單元素的值與指定資料表欄位有重複,則返回 False(譯者注:比如 is_unique[User.Email],那麼驗證類別會去查詢 User 表中 Email 欄位有沒有與表單元素一樣的值,如存重複,則返回 false,這樣開發者就不必另寫 Callback 驗證程式碼。)</td>
|
||||
<td class="td">如果表單元素的值與指定資料表欄位有重複,則回傳 False(譯者注:比如 is_unique[User.Email],那麼驗證類別會去查詢 User 表中 Email 欄位有沒有與表單元素一樣的值,如存重複,則回傳 false,這樣開發者就不必另寫 Callback 驗證程式碼。)</td>
|
||||
<td class="td">is_unique[table.field]</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>min_length</strong></td>
|
||||
<td class="td">Yes</td>
|
||||
<td class="td">如果表單元素值的字元長度少於引數中定義的數字,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值的字元長度少於參數中定義的數字,則回傳 FALSE</td>
|
||||
<td class="td">min_length[6]</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>max_length</strong></td>
|
||||
<td class="td">Yes</td>
|
||||
<td class="td">如果表單元素值的字元長度大於引數中定義的數字,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值的字元長度大於參數中定義的數字,則回傳 FALSE</td>
|
||||
<td class="td">max_length[12]</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>exact_length</strong></td>
|
||||
<td class="td">Yes</td>
|
||||
<td class="td">如果表單元素值的字元長度與引數中定義的數字不符,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值的字元長度與參數中定義的數字不符,則回傳 FALSE</td>
|
||||
<td class="td">exact_length[8]</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>greater_than</strong></td>
|
||||
<td class="td">Yes</td>
|
||||
<td class="td">如果表單元素值是非數字型別,或小於引數定義的值,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值是非數字型別,或小於參數定義的值,則回傳 FALSE</td>
|
||||
<td class="td">greater_than[8]</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>less_than</strong></td>
|
||||
<td class="td">Yes</td>
|
||||
<td class="td">如果表單元素值是非數字型別,或大於引數定義的值,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值是非數字型別,或大於參數定義的值,則回傳 FALSE</td>
|
||||
<td class="td">less_than[8]</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>alpha</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素值中包含除字母以外的其他字元,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值中包含除字母以外的其他字元,則回傳 FALSE</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>alpha_numeric</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素值中包含除字母和數字以外的其他字元,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值中包含除字母和數字以外的其他字元,則回傳 FALSE</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>alpha_dash</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素值中包含除字母/數字/下劃線/破折號以外的其他字元,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值中包含除字母/數字/下劃線/破折號以外的其他字元,則回傳 FALSE</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>numeric</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素值中包含除數字以外的字元,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值中包含除數字以外的字元,則回傳 FALSE</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>integer</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素中包含除整數以外的字元,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素中包含除整數以外的字元,則回傳 FALSE</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>decimal</strong></td>
|
||||
<td class="td">Yes</td>
|
||||
<td class="td">如果表單元素中輸入(非小數)不完整的值,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素中輸入(非小數)不完整的值,則回傳 FALSE</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>is_natural</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素值中包含了非自然數的其他數值 (其他數值不包括零),則返回 FALSE。自然數形如:0,1,2,3....等等。</td>
|
||||
<td class="td">如果表單元素值中包含了非自然數的其他數值 (其他數值不包括零),則回傳 FALSE。自然數形如:0,1,2,3....等等。</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>is_natural_no_zero</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素值包含了非自然數的其他數值 (其他數值包括零),則返回 FALSE。非零的自然數:1,2,3.....等等。</td>
|
||||
<td class="td">如果表單元素值包含了非自然數的其他數值 (其他數值包括零),則回傳 FALSE。非零的自然數:1,2,3.....等等。</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>valid_email</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素值包含不合法的 email 地址,則返回 FALSE</td>
|
||||
<td class="td">如果表單元素值包含不合法的 email 地址,則回傳 FALSE</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>valid_emails</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素值中任何一個值包含不合法的 email 地址(地址之間用英文逗號分割),則返回 FALSE。</td>
|
||||
<td class="td">如果表單元素值中任何一個值包含不合法的 email 地址(地址之間用英文逗號分割),則回傳 FALSE。</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>valid_ip</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素的值不是一個合法的 IP 地址,則返回 FALSE。</td>
|
||||
<td class="td">如果表單元素的值不是一個合法的 IP 地址,則回傳 FALSE。</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="td"><strong>valid_base64</strong></td>
|
||||
<td class="td">No</td>
|
||||
<td class="td">如果表單元素的值包含除了 base64 編碼字元之外的其他字元,則返回 FALSE。</td>
|
||||
<td class="td">如果表單元素的值包含除了 base64 編碼字元之外的其他字元,則回傳 FALSE。</td>
|
||||
<td class="td"> </td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ beego 目前沒有針對這三種方式進行任何形式的整合,但是可
|
||||
|
||||
github.com/abbot/go-http-auth
|
||||
```
|
||||
下面程式碼示範瞭如何把這個函式庫引入 beego 中從而實現認證:
|
||||
下面程式碼示範了如何把這個函式庫引入 beego 中從而實現認證:
|
||||
```Go
|
||||
|
||||
package controllers
|
||||
@@ -56,7 +56,7 @@ oauth 和 oauth2 是目前比較流行的兩種認證方式,還好第三方有
|
||||
|
||||
github.com/bradrydzewski/go.auth
|
||||
```
|
||||
下面程式碼示範瞭如何把該函式庫引入 beego 中從而實現 oauth 的認證,這裡以 github 為例示範:
|
||||
下面程式碼示範了如何把該函式庫引入 beego 中從而實現 oauth 的認證,這裡以 github 為例示範:
|
||||
|
||||
1. 新增兩條路由
|
||||
```Go
|
||||
@@ -134,19 +134,19 @@ func (this *PageController) Get() {
|
||||
```
|
||||
整個的流程如下,首先開啟瀏覽器輸入地址:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 14.4 顯示帶有登入按鈕的首頁
|
||||
|
||||
然後點選連結出現如下介面:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 14.5 點選登入按鈕後顯示 github 的授權頁
|
||||
|
||||
然後點選 Authorize app 就出現如下介面:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 14.6 授權登入之後顯示的取得到的 github 資訊頁
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ func InitLang(){
|
||||
beego.Translation.SetLocale(beego.Lang)
|
||||
}
|
||||
```
|
||||
為了方便在範本中直接呼叫多語言套件,我們設計了三個函式來處理響應的多語言:
|
||||
為了方便在範本中直接呼叫多語言套件,我們設計了三個函式來處理回應的多語言:
|
||||
```Go
|
||||
|
||||
beegoTplFuncMap["Trans"] = i18n.I18nT
|
||||
@@ -97,7 +97,7 @@ beego.InitLang()
|
||||
3. 使用語言套件
|
||||
|
||||
|
||||
我們可以在 controller 中呼叫翻譯取得響應的翻譯語言,如下所示:
|
||||
我們可以在 controller 中呼叫翻譯取得回應的翻譯語言,如下所示:
|
||||
```Go
|
||||
|
||||
func (this *MainController) Get() {
|
||||
@@ -105,7 +105,7 @@ func (this *MainController) Get() {
|
||||
this.TplNames = "index.tpl"
|
||||
}
|
||||
```
|
||||
我們也可以在範本中直接呼叫響應的翻譯函式:
|
||||
我們也可以在範本中直接呼叫回應的翻譯函式:
|
||||
```Go
|
||||
|
||||
//直接文字翻譯
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 14.6 pprof 支援
|
||||
Go 語言有一個非常棒的設計就是標準函式庫裡面帶有程式碼的效能監控工具,在兩個地方有套件:
|
||||
Go 語言有一個非常棒的設計就是標準函式庫裡面帶有程式碼的效能監聽工具,在兩個地方有套件:
|
||||
```Go
|
||||
|
||||
net/http/pprof
|
||||
@@ -60,13 +60,13 @@ func (this *ProfController) Get() {
|
||||
beego.PprofOn = true
|
||||
```
|
||||
然後你就可以在瀏覽器中開啟如下 URL 就看到如下介面:
|
||||

|
||||

|
||||
|
||||
圖 14.7 系統當前 goroutine、heap、thread 資訊
|
||||
|
||||
點選 goroutine 我們可以看到很多詳細的資訊:
|
||||
|
||||

|
||||

|
||||
|
||||
圖 14.8 顯示當前 goroutine 的詳細資訊
|
||||
|
||||
@@ -103,7 +103,7 @@ go tool pprof http://localhost:8080/debug/pprof/profile
|
||||
|
||||
(pprof)web
|
||||
|
||||

|
||||

|
||||
|
||||
圖 14.9 展示的執行流程資訊
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 14.7 小結
|
||||
這一章主要闡述瞭如何基於 beego 框架進行擴充套件,這包括靜態檔案的支援,靜態檔案主要講述瞭如何利用 beego 進行快速的網站開發,利用 bootstrap 建立漂亮的站點;第二小結講解了如何在 beego 中整合 sessionManager,方便使用者在利用 beego 的時候快速的使用 session;第三小結介紹了表單和驗證,基於 Go 語言的 struct 的定義使得我們在開發 Web 的過程中從重複的工作中解放出來,而且加入了驗證之後可以儘量做到資料安全,第四小結介紹了使用者認證,使用者認證主要有三方面的需求,http basic 和 http digest 認證,第三方認證,自訂認證,透過程式碼示範瞭如何利用現有的第三方套件整合到 beego 應用中來實現這些認證;第五小節介紹了多語言的支援,beego 中集成了 go-i18n 這個多語言套件,使用者可以很方便的利用該函式庫開發多語言的 Web 應用;第六小節介紹瞭如何整合 Go 的 pprof 套件,pprof 套件是用於效能除錯的工具,透過對 beego 的改造之後集成了 pprof 套件,使得使用者可以利用 pprof 測試基於 beego 開發的應用,透過這六個小節的介紹我們擴展出來了一個比較強壯的 beego 框架,這個框架足以應付目前大多數的 Web 應用,使用者可以繼續發揮自己的想象力去擴充套件,我這裡只是簡單的介紹了我能想的到的幾個比較重要的擴充套件。
|
||||
這一章主要闡述了如何基於 beego 框架進行擴充套件,這包括靜態檔案的支援,靜態檔案主要講述了如何利用 beego 進行快速的網站開發,利用 bootstrap 建立漂亮的站點;第二小結講解了如何在 beego 中整合 sessionManager,方便使用者在利用 beego 的時候快速的使用 session;第三小結介紹了表單和驗證,基於 Go 語言的 struct 的定義使得我們在開發 Web 的過程中從重複的工作中解放出來,而且加入了驗證之後可以儘量做到資料安全,第四小結介紹了使用者認證,使用者認證主要有三方面的需求,http basic 和 http digest 認證,第三方認證,自訂認證,透過程式碼示範了如何利用現有的第三方套件整合到 beego 應用中來實現這些認證;第五小節介紹了多語言的支援,beego 中集成了 go-i18n 這個多語言套件,使用者可以很方便的利用該函式庫開發多語言的 Web 應用;第六小節介紹了如何整合 Go 的 pprof 套件,pprof 套件是用於效能除錯的工具,透過對 beego 的改造之後集成了 pprof 套件,使得使用者可以利用 pprof 測試基於 beego 開發的應用,透過這六個小節的介紹我們擴展出來了一個比較強壯的 beego 框架,這個框架足以應付目前大多數的 Web 應用,使用者可以繼續發揮自己的想象力去擴充套件,我這裡只是簡單的介紹了我能想的到的幾個比較重要的擴充套件。
|
||||
|
||||
## links
|
||||
* [目錄](<preface.md>)
|
||||
|
||||
@@ -27,6 +27,7 @@ This book is licensed under the [CC BY-SA 3.0 License](http://creativecommons.or
|
||||
|
||||
## 正體中文翻譯
|
||||
|
||||
* [Will 保哥](https://www.facebook.com/will.fans)
|
||||
* [Blog](https://blog.miniasp.com/)
|
||||
* [Twitter](https://twitter.com/Will_Huang)
|
||||
* **Will 保哥**
|
||||
* [Blog (The Will Will Web)](https://blog.miniasp.com/)
|
||||
* [Twitter (Will Huang)](https://twitter.com/Will_Huang)
|
||||
* [Facebook (Will 保哥的技術交流中心)](https://www.facebook.com/will.fans/)
|
||||
@@ -23,7 +23,7 @@
|
||||
* [處理表單的輸入](04.1.md)
|
||||
* [驗證表單的輸入](04.2.md)
|
||||
* [預防跨站指令碼](04.3.md)
|
||||
* [防止多次遞交表單](04.4.md)
|
||||
* [防止多次提交表單](04.4.md)
|
||||
* [處理檔案上傳](04.5.md)
|
||||
* [小結](04.6.md)
|
||||
* [訪問資料庫](05.0.md)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
- 4.1 [處理表單的輸入](04.1.md)
|
||||
- 4.2 [驗證表單的輸入](04.2.md)
|
||||
- 4.3 [預防跨站指令碼](04.3.md)
|
||||
- 4.4 [防止多次遞交表單](04.4.md)
|
||||
- 4.4 [防止多次提交表單](04.4.md)
|
||||
- 4.5 [處理檔案上傳](04.5.md)
|
||||
- 4.6 [小結](04.6.md)
|
||||
* 5.[訪問資料庫](05.0.md)
|
||||
|
||||
Reference in New Issue
Block a user