1.
1).GOPATH設置
先設置自己的GOPATH,可以在本機中運行$PATH進行查看:
userdeMacBook-Pro:~ user$ $GOPATH -bash: /Users/user/go: is a directory
在這可見我的GOPATH是/Users/user/go,並在該目錄下生成如下作用的三個子目錄:
- src:存放源代碼(比如.go .c .h .s等)
- pkg:編譯后生成的文件(比如.a)
- bin:編譯后生成的可執行文件(為了方便可將此目錄加入到$PATH中,本機已添加)
2).應用目錄結構
然后之后如果想要自己新建應用或者一個代碼包都是在src目錄下新建一個文件夾,文件夾一般是代碼包名稱,比如$GOPATH/src/mymath/sqrt.go,在這里,包名就是mymath,然后其代碼中的包名寫成package mymath,比如:
package mymath func Sqrt(x float64) float64{ z := 0.0 for i := 0; i < 1000; i++ { z -= ( z * z - x ) / ( 2 * x ) } return z }
當然也允許多級目錄,例如在src下面新建了目錄$GOPATH/src/github.com/astaxie/beedb,在這里包路徑就是github.com/astaxie/beedb,包名稱為最后一個目錄beedb
3).編譯應用
假設上面我們建好了自己的mymath應用包,之后的編譯安裝方法有兩種:
- 一是進入對應的應用包目錄,即mymath目錄,然后運行go install
- 二是在任意目錄下執行go install mymath
編譯安裝好后,我們就能夠到$GOPATH/pkg/${GOOS}_${GOARCH}目錄下看見mymath.a這個應用包
${GOOS}_${GOARCH}是平台名,如mac系統是darwin_amd64,linux是linux_amd64
userdeMacBook-Pro:src user$ cd mymath/ userdeMacBook-Pro:mymath user$ ls sqrt.go userdeMacBook-Pro:mymath user$ go install userdeMacBook-Pro:mymath user$ cd .. userdeMacBook-Pro:src user$ cd .. userdeMacBook-Pro:go user$ cd pkg userdeMacBook-Pro:pkg user$ cd darwin_amd64/ userdeMacBook-Pro:darwin_amd64 user$ ls golang.org mymath.a userdeMacBook-Pro:darwin_amd64 user$
4).調用應用
然后就是對該應用進行調用
比如我們再新建一個應用包mathapp,創建一個main.go源碼:
package main import( "mymath" "fmt" ) func main() { fmt.Printf("Hello, world. Sqrt(2) = %v \n", mymath.Sqrt(2)) }
然后進入該應用目錄,運行go build來編譯程序:
userdeMacBook-Pro:src user$ cd mathapp/ userdeMacBook-Pro:mathapp user$ ls main.go userdeMacBook-Pro:mathapp user$ go build userdeMacBook-Pro:mathapp user$ ls main.go mathapp userdeMacBook-Pro:mathapp user$
然后運行該可執行文件,./mathapp,得到返回結果為:
userdeMacBook-Pro:mathapp user$ ./mathapp Hello, world. Sqrt(2) = 1.414213562373095
⚠️
package <pkgName> :用於指明當前文件屬於哪個包
package main : 說明該文件是一個可獨立執行的文件,它在編譯后會產生可執行文件
除了main包外,其他包都會生成*.a文件(也就是包文件),並放在$GOPATH/pkg/${GOOS}_${GOARCH}目錄下
每一個可獨立執行的go程序中,必定都包含一個package main,在這個main包中必定包含一個入口函數main(),該函數即沒有參數,也沒有返回值
5).獲取遠程包
如果你想要獲取的是一個遠程包,可以使用go get獲取,其支持多數的開源社區(如github、googlecode、bitbucket、Launchpad),運行語句為:
go get github.com/astaxie/beedb
go get -u參數可以自動更新包,並且在使用go get時會自動獲取該包依賴的其他第三方包
userdeMBP:~ user$ go get github.com/astaxie/beedb userdeMBP:~ user$ cd go/src userdeMBP:src user$ ls mymath golang.org mathapp github.com userdeMBP:src user$ cd github.com/ userdeMBP:github.com user$ ls WeMeetAgain astaxie btcsuite conformal userdeMBP:github.com user$ cd astaxie/ userdeMBP:astaxie user$ ls beedb userdeMBP:astaxie user$ cd ../../.. userdeMBP:go user$ cd pkg userdeMBP:pkg user$ ls darwin_amd64 userdeMBP:pkg user$ cd darwin_amd64/ userdeMBP:darwin_amd64 user$ ls github.com golang.org mymath.a userdeMBP:darwin_amd64 user$ cd github.com/astaxie/ userdeMBP:astaxie user$ ls beedb.a
通過這個命令可以獲取相應的源碼,對應的開源平台采用不同的源碼控制工具,如github采用git,googlecode采用hg。因此想要使用哪個平台的代碼就要對應安裝相應的源碼控制工具
上面的代碼在本地的代碼結構為:
go get 本質上可以分成兩步:
- 通過源碼工具clone代碼到src下面
- 然后自動執行go install
使用方法就是:
import github.com/astaxie/beedb
2.相關http內容可見go標准庫的學習-net/http
3.表單學習——form
1)如何處理表單的輸入
舉例:
package main import( "fmt" "net/http" "log" ) func index(w http.ResponseWriter, r *http.Request){ r.ParseForm() //解析URL傳遞的參數,對於POST則解析響應包的主體(request body),如果不調用它則無法獲取表單的數據 fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只會返回同名參數slice中的第一個,不存在則返回空字符串),則可以不用調用上面的ParseForm()方法 for k, v := range r.Form{ fmt.Println("key :", k) fmt.Println("value :", v) } html := `<html> <head> <title></title> </head> <body> <form action="http://localhost:9090/login" method="post"> username: <input type="text" name="username"> password: <input type="text" name="password"> <input type="submit" value="login"> </form> </body> </html>` fmt.Fprintf(w, html) //將html寫到w中,w中的內容將會輸出到客戶端中 } func login(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //獲得請求的方法 r.ParseForm() fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) if r.Method == "POST"{ fmt.Println("username : ", r.Form["username"]) fmt.Println("password : ", r.Form["password"]) } } func main() { http.HandleFunc("/", index) //設置訪問的路由 http.HandleFunc("/login", login) //設置訪問的路由 err := http.ListenAndServe(":9090", nil) //設置監聽的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
調用http://localhost:9090/后,瀏覽器返回:
終端返回:
userdeMBP:go-learning user$ go run test.go map[] map[] path / scheme []
瀏覽器訪問http://localhost:9090/login后終端變成:
userdeMBP:go-learning user$ go run test.go map[] map[] path / scheme [] method POST map[username:[hello] password:[world]] map[username:[hello] password:[world]] path /login scheme [] username : [hello] password : [world]
r.Form里面包含所有請求的參數,比如URL中query-string、POST的數據、PUT的數據
當你URL的query-string字段和POST的字段沖突時,該值會被保存成一個slice存儲在一起
比如把index函數中html值中的action改成http://localhost:9090/login?username=allen,如下:
<form action="http://localhost:9090/login?username=allen" method="post">
此時的終端為:
method POST map[password:[world] username:[hello allen]] map[password:[world] username:[hello]] path /login scheme [] username : [hello allen] password : [world]
可見r.PostForm中不會存放URL中query-string的數據
2)對表單的輸入進行驗證
因為不能夠信任任何用戶的輸入,因此我們需要對用戶的輸入進行有效性驗證
主要有兩方面的數據驗證:
- 頁面端的js驗證(使用插件庫,比如ValidationJS插件)
- 服務器端的驗證,這里講的就是這種
1》必填字段
確保從表單元素中能夠得到一個值,如上面例子中的username字段,使用len()獲取字符串長度:
if len(r.Form["username"][0]) == 0{ //如果為0則說明該表單元素中沒有值,即為空時要做出什么處理 }
- 當r.Form中表單元素的類型是空文本框、空文本區域以及文件上傳,表單元素為空值
- 如果類型是未選中的復選框和單選按鈕,那么就不會在r.Form中產生相應的條目,用這種方法來驗證會報錯。所以需要使用r.Form.Get()來獲取這類表單元素的值,這樣當該字段不存在時會返回。但是這種方法只能獲取單個值,如果是map的值,還是要使用上面的方法
2》數字
確保從表單獲取的是數字,比如年齡
getInt, err := strconv.Atoi(r.Form.Get("age")) if err != nil { //這就說明數字轉化出錯了,即輸入的可能不是數字,這里進行錯誤的操作 } //如果確定是數字則繼續進行下面的操作 if getInt > 100{ //判斷年齡的大小范圍的問題 }
還有另一種方法就是使用正則表達式:
if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m{ //如果沒有匹配項,則!m為true,說明輸入的不是數字 return false }
3》中文
保證從表單中獲取的是中文,使用正則表達式
if m, _ := regexp.MatchString("^[\\x{4e00}-\\x{9fa5}]+$", r.Form.Get("realname")); !m{ //如果沒有匹配項,則!m為true,說明輸入的不是中文 return false }
4》英文
保證從表單中獲取的是英文,使用正則表達式
if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("englishname")); !m{ //如果沒有匹配項,則!m為true,說明輸入的不是英文 return false }
5》電子郵件
查看用戶輸入的電子郵件是否正確
if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m{ //如果沒有匹配項,則!m為true,說明輸入郵箱格式不對 return false }
6》手機號碼
if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m{ //如果沒有匹配項,則!m為true,說明輸入電話號碼格式不對 return false }
7》下拉菜單
判斷表單的<select>元素生成的下拉菜單中被選中項目是否正確,因為有時黑客會偽造下拉菜單中不存在的值發送給你,比如下拉菜單為:
<select name"fruit"> <option value="apple">apple</option> <option value="pear">pear</option> <option value="banana">banana</option> </select>
驗證方法為:
slice := []string{"apple", "pear", "banana"} for _, v := range slice{ if v == r.Form.Get("fruit"){ return true } } return false
8》單選按鈕
單選按鈕<radio>中只有男=1,女=2兩個選項,如何防止傳入的值為3等錯誤值,單選按鈕為:
<input type="radio" name="gender" value="1">男 <input type="radio" name="gender" value="2">女
驗證方法:
slice := []int {1,2} for _, v := range slice{ if v == r.Form.Get("gender"){ return true } } return false
9》復選框
選定用戶選中的都是你提供的值,不同之處在於接受到的數據是一個slice
<input type="checkbox" name="interest" value="football">足球 <input type="checkbox" name="interest" value="basketball">籃球 <input type="checkbox" name="interest" value="tennis">網球
驗證:
slice := []string{"football", "basketball", "tennis"} a := Slice_diff(r.Form["interest"], slice) if a == nil{//說明接收到的數據中的值都來自slice return true } return false
10》時間和日期
使用time處理包
11》身份證號
//驗證15位身份證,15位都是數字 if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m{ //如果沒有匹配項,則!m為true,說明輸入身份證格式不對 return false } //驗證18位身份證,前17位都是數字,最后一位是校驗碼,可能是數字和X if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m{ //如果沒有匹配項,則!m為true,說明輸入身份證格式不對 return false }
3)預防跨站腳本
因為現在的網站含有大量動態內容以提高用戶體驗,動態站點會受到名為“跨站腳本攻擊”(即XSS)的威脅,靜態站點則不受影響
攻擊者會在有漏洞的程序中插入攻擊的JavaScript、VBScript、ActiveX或Flash來欺騙用戶在這上面進行操作來盜取用戶的賬戶信息、修改用戶設置、盜取/污染cookie和植入惡意廣告等。
兩種防護方法:
- 驗證所有輸入數據,即上面2)進行的操作
- 對所有輸出數據進行適當的處理,一防止任何已經注入的腳本在瀏覽器端運行,這里講的是這種
該適當的處理使用的是html/template中的函數進行轉義:
func HTMLEscape
func HTMLEscape(w io.Writer, b []byte)
函數向w中寫入b的HTML轉義等價表示。
func HTMLEscapeString
func HTMLEscapeString(s string) string
返回s的HTML轉義等價表示字符串。
func HTMLEscaper
func HTMLEscaper(args ...interface{}) string
函數返回其所有參數文本表示的HTML轉義等價表示字符串。
Template類型是text/template包的Template類型的特化版本,用於生成安全的HTML文本片段。
func New
func New(name string) *Template
創建一個名為name的模板。
func (*Template) Parse
func (t *Template) Parse(src string) (*Template, error)
Parse方法將字符串text解析為模板。嵌套定義的模板會關聯到最頂層的t。Parse可以多次調用,但只有第一次調用可以包含空格、注釋和模板定義之外的文本。如果后面的調用在解析后仍剩余文本會引發錯誤、返回nil且丟棄剩余文本;如果解析得到的模板已有相關聯的同名模板,會覆蓋掉原模板。
func (*Template) ExecuteTemplate
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
ExecuteTemplate方法類似Execute,但是使用名為name的t關聯的模板產生輸出。
因為HTTP是一種無狀態的協議,那么要如何判別是否為同一個用戶。一般是使用cookie(cookie是存儲在客戶端的信息,能夠每次通過header和服務器進行交互)
更詳細的內容可見go標准庫的學習-text/template
舉例:
package main import( "fmt" "net/http" "log" "html/template" ) func index(w http.ResponseWriter, r *http.Request){ r.ParseForm() //解析URL傳遞的參數,對於POST則解析響應包的主體(request body),如果不調用它則無法獲取表單的數據 fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只會返回同名參數slice中的第一個,不存在則返回空字符串),則可以不用調用上面的ParseForm()方法 for k, v := range r.Form{ fmt.Println("key :", k) fmt.Println("value :", v) } fmt.Fprintf(w, "hello world") //將html寫到w中,w中的內容將會輸出到客戶端中 } func login(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //獲得請求的方法 r.ParseForm() if r.Method == "GET"{ // html := `<html> <head> <title></title> </head> <body> <form action="http://localhost:9090/login" method="post"> username: <input type="text" name="username"> password: <input type="text" name="password"> <input type="submit" value="login"> </form> </body> </html>` t := template.Must(template.New("test").Parse(html)) t.Execute(w, nil) }else{ fmt.Println("username : ", template.HTMLEscapeString(r.Form.Get("username")))//在終端即客戶端輸出 fmt.Println("password : ", template.HTMLEscapeString(r.Form.Get("password")))//把r.Form.Get("password")轉義之后返回字符串 template.HTMLEscape(w, []byte(r.Form.Get("username"))) //在客戶端輸出,把r.Form.Get("username")轉義后寫到w } } func main() { http.HandleFunc("/", index) //設置訪問的路由 http.HandleFunc("/login", login) //設置訪問的路由 err := http.ListenAndServe(":9090", nil) //設置監聽的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
訪問http://localhost:9090/
訪問http://localhost:9090/login
如果僅傳入字符串:
服務端返回:
method POST username : hello password : allen map[] map[] path /favicon.ico scheme []
客戶端:
當時如果username輸入的是<script>alert()</script>
客戶端返回:
可見html/template包默認幫你過濾了html標簽
如果你想要內容不被轉義,方法有:
1》使用text/template
import ( "text/template" "os" ) ... t, err := template.New("test").Parse(`{{define "T"}} Hello, {{.}}!{{end}}`) err := template.ExecuteTemplate(os.Stdout, "T", "<script>alert('you have benn pwned')</script>")
2》使用html/template,和template.HTML
import ( "html/template" "os" ) ... t, err := template.New("test").Parse(`{{define "T"}} Hello, {{.}}!{{end}}`) err := template.ExecuteTemplate(os.Stdout, "T", template.HTML("<script>alert('you have benn pwned')</script>"))
4)防止多次遞交表單
解決辦法是在表單中添加一個帶有唯一值的隱藏字段。在驗證表單時,先檢查帶有該唯一值的表單是否已經提交過,如果是,則拒絕再次提交;如果不是,則處理表單進行邏輯處理。
如果使用的是Ajax模式遞交表單的話,當表單遞交后,通過javascript來禁用表單的遞交按鈕
比如我們能夠使用MD5(時間戳)來獲取唯一值,如time.Now().Unix()
舉例:
package main import( "fmt" "net/http" "log" "text/template" "crypto/md5" "time" "io" "strconv" ) func index(w http.ResponseWriter, r *http.Request){ r.ParseForm() //解析URL傳遞的參數,對於POST則解析響應包的主體(request body),如果不調用它則無法獲取表單的數據 fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只會返回同名參數slice中的第一個,不存在則返回空字符串),則可以不用調用上面的ParseForm()方法 for k, v := range r.Form{ fmt.Println("key :", k) fmt.Println("value :", v) } fmt.Fprintf(w, "hello world") //將html寫到w中,w中的內容將會輸出到客戶端中 } func login(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //獲得請求的方法 if r.Method == "GET"{ // html := `<html> <head> <title></title> </head> <body> <form action="http://localhost:9090/login" method="post"> username: <input type="text" name="username"> password: <input type="text" name="password"> <input type="hidden" name="token" value="{{.}}"> <input type="submit" value="login"> </form> </body> </html>` crutime := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(crutime, 10)) token := fmt.Sprintf("%x", h.Sum(nil)) t := template.Must(template.New("test").Parse(html)) t.Execute(w, token) }else{ r.ParseForm() token := r.Form.Get("token") if token != ""{ //驗證token的合法性 }else{ //如果不存在token,則報錯 log.Fatal("not token") } fmt.Println("username : ", template.HTMLEscapeString(r.Form.Get("username")))//在終端即客戶端輸出 fmt.Println("password : ", template.HTMLEscapeString(r.Form.Get("password"))) template.HTMLEscape(w, []byte(r.Form.Get("username"))) //在客戶端輸出 } } func main() { http.HandleFunc("/", index) //設置訪問的路由 http.HandleFunc("/login", login) //設置訪問的路由 err := http.ListenAndServe(":9090", nil) //設置監聽的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
瀏覽器中訪問http://localhost:9090/login
可見得到的token時間戳為:"7cf962884609e3810259654d1e766754"
該方案可以防止非惡意的攻擊,並能使惡意用戶暫時不知所措。但是它不能夠排除所有的欺騙性的動機,對此類情況還需要更加復雜的工作
5)處理文件上傳——大文件
要使得表單能夠上傳文件,首先就是要添加form的encrype屬性,該屬性有三種情況:
- application/x-www-form-urlencoded : 表示在發送前編碼所有字符(默認)
- multipart/form-data :不對字符編碼。在使用包含文件上傳控件的表單時,必須使用該值,所以這里設置為它
- text/plain:空格轉換為"+"加號,但不對特殊字符編碼
舉例:
通過表單上傳文件,在服務器端處理文件
package main import( "fmt" "net/http" "log" "text/template" "crypto/md5" "time" "io" "strconv" "os" ) func index(w http.ResponseWriter, r *http.Request){ r.ParseForm() //解析URL傳遞的參數,對於POST則解析響應包的主體(request body),如果不調用它則無法獲取表單的數據 fmt.Println(r.Form) fmt.Println(r.PostForm) fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) //如果使用的是方法FormValue()方法(它只會返回同名參數slice中的第一個,不存在則返回空字符串),則可以不用調用上面的ParseForm()方法 for k, v := range r.Form{ fmt.Println("key :", k) fmt.Println("value :", v) } fmt.Fprintf(w, "hello world") //將html寫到w中,w中的內容將會輸出到客戶端中 } func upload(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //獲得請求的方法 if r.Method == "GET"{ // html := `<html> <head> <title>上傳文件</title> </head> <body> <form enctype="multipart/form-data" action="http://localhost:9090/upload" method="post"> <input type="file" name="uploadfile" /> <input type="hidden" name="token" value="{{.}}" /> <input type="submit" value="upload" /> </form> </body> </html>` crutime := time.Now().Unix() h := md5.New() io.WriteString(h, strconv.FormatInt(crutime, 10)) token := fmt.Sprintf("%x", h.Sum(nil)) t := template.Must(template.New("test").Parse(html)) t.Execute(w, token) }else{ r.ParseMultipartForm(32 << 20) //表示maxMemory,調用ParseMultipart后,上傳的文件存儲在maxMemory大小的內存中,如果大小超過maxMemory,剩下部分存儲在系統的臨時文件中 file, handler, err := r.FormFile("uploadfile") //根據input中的name="uploadfile"來獲得上傳的文件句柄 if err != nil{ fmt.Println(err) return } defer file.Close() fmt.Fprintf(w, "%v", handler.Header) f, err := os.OpenFile("./test/" + handler.Filename, os.O_WRONLY| os.O_CREATE, 0666) if err != nil{ fmt.Println(err) return } defer f.Close() io.Copy(f, file) } } func main() { http.HandleFunc("/", index) //設置訪問的路由 http.HandleFunc("/upload", upload) //設置訪問的路由 err := http.ListenAndServe(":9090", nil) //設置監聽的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
獲取其他非文件字段信息的時候就不需要調用r.ParseForm,因為在需要的時候Go自動會去調用。而且ParseMultipartForm調用一次之后,后面再調用不會再有效果
瀏覽器中返回handler.Header:
test文件夾中也生成了該傳入test.txt的副本:
⚠️如果上面的表單form沒有設置enctype="multipart/form-data"就會報錯:
Content-Type isn't multipart/form-data
上傳文件主要三步處理:
- 表單中增加enctype="multipart/form-data"
- 服務器調用r.ParseMultipartForm,把上傳的文件存儲在內存和臨時文件中
- 使用r.FormFile獲取文件句柄,然后對文件進行存儲等處理
客戶端上傳文件
舉例:
package main import( "fmt" "net/http" "io/ioutil" "bytes" "mime/multipart" "os" "io" ) func postFile(filename string, targetUrl string) error { bodyBuf := &bytes.Buffer{} bodyWriter := multipart.NewWriter(bodyBuf)//把文件的文本流寫入一個緩存中,然后調用http.Post方法把緩存傳入服務器 //關鍵操作 fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename) //使用給出的屬性名和文件名創建一個新的form-data頭。 fmt.Println(fileWriter) //&{0xc00008acc0 false <nil>} if err != nil{ fmt.Println("error writing to buffer") return err } //打開文件句柄操作 fh, err := os.Open(filename) if err != nil{ fmt.Println("error open file") return err } //復制 _, err = io.Copy(fileWriter, fh) if err != nil{ return err } contentType := bodyWriter.FormDataContentType()//返回bodyWriter對應的HTTP multipart請求的Content-Type的值,多以multipart/form-data起始。 fmt.Println(contentType) //multipart/form-data; boundary=b7c3357b23c6a6697af5810d1c0dc0184912ae24c5f5074db8aae0fe5198 bodyWriter.Close() resp, err := http.Post(targetUrl, contentType, bodyBuf) if err != nil { return err } defer resp.Body.Close() resp_body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } fmt.Println(resp.Status) //200 OK fmt.Println(string(resp_body)) //map[Content-Disposition:[form-data; name="uploadfile"; filename="./testFmt.txt"] Content-Type:[application/octet-stream]] return nil } func main() { targetUrl := "http://localhost:9090/upload" filename := "./testFmt.txt" postFile(filename, targetUrl) }
運行之前服務端的同時調用該客戶端,返回如上數據,然后可見相應的test文件夾中生成了testFmt.txt:
4.訪問數據庫
1)database/sql接口
更詳細的內容可看go標准庫的學習-database/sql/driver和go標准庫的學習-database/sql
go和PHP不同的地方是Go沒有官方提供數據庫驅動,而是為開發者開發數據庫驅動定義了一些標准接口,開發者可以根據定義的接口來開發相應的數據庫驅動。
這樣的好處是只要按照標准接口開發的代碼,以后需要遷移數據庫時,不需要任何更改。
1》sql.Register - 在database/sql中
該函數用來注冊數據庫驅動。當第三方開發者開發數據庫驅動時,都會實現init函數,在init里面調用這個Register(name string, driver driver.Driver)完成本驅動的注冊,比如
1>sqlite3的驅動:
//http://github.com/mattn/go-sqlite3驅動 func init(){ sql.Register("sqlite3", &SQLiteDriver{}) }
2>mysql的驅動
//http://github.com/mikespook/mymysql驅動 var d = Driver{proto : "tcp", raddr : "127.0.0.1:3306"} func init(){ Register("SET NAMES utf8") sql.Register("mymysql", &d) }
由上可見第三方數據庫驅動都是通過這個函數來注冊自己的數據庫驅動名稱及相應的driver實現。
上面的例子實現的都是注冊一個驅動,該函數還能夠實現同時注冊多個數據庫驅動,只要這些驅動不重復,通過一個map來存儲用戶定義的相應驅動
var drivers = make(map[string]driver.Driver) drivers[name] = driver
在使用database/sql接口和第三方庫時經常看見如下:
import( "database/sql" _ "github.com/mattn/go-sqlite3" //上面定義的sqlite3驅動包 )
里面的_的作用就是說明引入了"github.com/mattn/go-sqlite3"該包,但是不直接使用包里面的函數或變量,其中init函數也不自動調用。因此我們之后需要自己手動去調用init函數。
2》driver.Driver - 在database/sql/driver中
Driver是一個數據庫驅動的接口,其定義了一個Open(name string)方法,該方法返回一個數據庫的Conn接口:
type Driver interface { // Open返回一個新的與數據庫的連接,參數name的格式是驅動特定的。 // // Open可能返回一個緩存的連接(之前關閉的連接),但這么做是不必要的; // sql包會維護閑置連接池以便有效的重用連接。 // // 返回的連接同一時間只會被一個go程使用。 Open(name string) (Conn, error) }
因為返回的連接同一時間只會被一個go程使用,所以返回的Conn只能用來進行一次goroutine操作,即不能把這個Conn應用於Go的多個goroutine中,否則會出現錯誤,如:
go goroutineA(Conn) //執行查詢操作 go goroutineB(Conn) //執行插入操作
這樣的代碼會使Go不知某個操作到底是由哪個goroutine發起的從而導致數據混亂。即可能會講goroutineA里面執行的查詢操作的結果返回給goroutineB,從而讓goroutineB將此結果當成自己執行的插入數據
3》driver.Conn - 在database/sql/driver中
Conn是一個數據連接的接口定義。這個Conn只能應用在一個goroutine中,如上所說。
type Conn interface { // Prepare返回一個准備好的、綁定到該連接的狀態。 Prepare(query string) (Stmt, error) // Close作廢並停止任何現在准備好的狀態和事務,將該連接標注為不再使用。 // // 因為sql包維護着一個連接池,只有當閑置連接過剩時才會調用Close方法, // 驅動的實現中不需要添加自己的連接緩存池。 Close() error // Begin開始並返回一個新的事務。 Begin() (Tx, error) }
Prepare函數返回與當前連接相關的SQL語句的准備狀態,可以進行查詢、刪除等操作
Close函數關閉當前的連接,執行釋放連接擁有的資源等清理工作。因為驅動實現了database/sql中建議的conn pool,所以不用再去實現緩存conn之類的,這樣會更容易引起問題
Begin函數返回一個代表事務處理的Tx,通過它你可以進行查詢、更新等操作,或者對事務進行回滾、遞交
4》driver.Stmt - 在database/sql/driver中
Stmt是一種准備好的狀態,綁定到一個Conn中,並只能應用在一個goroutine中。
type Stmt interface { // Close關閉Stmt。 // // 和Go1.1一樣,如果Stmt被任何查詢使用中的話,將不會被關閉。 Close() error // NumInput返回占位參數的個數。 // // 如果NumInput返回值 >= 0,sql包會提前檢查調用者提供的參數個數, // 並且會在調用Exec或Query方法前返回數目不對的錯誤。 // // NumInput可以返回-1,如果驅動占位參數的數量不知時。 // 此時sql包不會提前檢查參數個數。 NumInput() int // Exec執行查詢,而不會返回結果,如insert或update。 Exec(args []Value) (Result, error) // Query執行查詢並返回結果,如select。 Query(args []Value) (Rows, error) }
Close函數關閉當前的連接狀態,但是如果當前正在執行query,query還是會有效地返回rows數據
Exec函數執行Conn的Prepare准備好的sql,傳入參數執行update/insert等操作,返回Result數據
Query函數執行Conn的Prepare准備好的sql,傳入需要的參數執行select操作,返回Rows結果集
5》driver.Tx - 在database/sql/driver中
事務處理一般就兩個過程,遞交或回滾,即下面的兩個函數:
type Tx interface { Commit() error Rollback() error }
6》driver.Execer - 在database/sql/driver中
這是一個Conn可選擇實現的接口
type Execer interface { Exec(query string, args []Value) (Result, error) }
如果一個Conn未實現Execer接口,sql包的DB.Exec會首先准備一個查詢(即調用Prepare返回Stmt),執行狀態(即執行Stmt的Exec函數),然后關閉狀態(即關閉Stmt)。Exec可能會返回ErrSkip。
7》driver.Result
這是是執行Update/insert等操作返回的結果接口定義
type Result interface { // LastInsertId返回insert等命令后數據庫自動生成的ID LastInsertId() (int64, error) // RowsAffected返回被查詢影響的行數 RowsAffected() (int64, error) }
8》driver.Rows
Rows是執行查詢返回的結果集接口定義
type Rows interface { // Columns返回各列的名稱,列的數量可以從切片長度確定。 // 如果某個列的名稱未知,對應的條目應為空字符串。 Columns() []string // Close關閉Rows。 Close() error // 調用Next方法以將下一行數據填充進提供的切片中,即返回下一條數據,並把數據返回給dest。 // 提供的切片必須和Columns返回的切片長度相同。 // // 切片dest可能被填充同一種驅動Value類型,但字符串除外;即dest里面的元素必須是driver.Vlaue的值,除了string。 // 所有string值都必須轉換為[]byte。 // // 當沒有更多行時,Next應返回io.EOF。 Next(dest []Value) error }
Columns函數返回查詢數據庫表的字段信息,返回的slice和sql查詢的字段一一對應,而不是返回整個表的所有字段
9》driver.RowsAffected
type RowsAffected int64
RowsAffected其實就是int64的別名,但是它實現了Result接口,用來底層實現Result的表示方式
RowsAffected實現了Result接口,用於insert或update操作,這些操作會修改零到多行數據。
10》driver.Value
type Value interface{}
Value其實就是一個空接口,它可以容納任何數據
driver.Value是驅動必須能夠操作的Value,所以Value要么是nil,要么是下面的任意一種:
int64 float64 bool []byte string [*] Rows.Next不會返回該類型值 time.Time
11》driver.ValueConverter
ValueConverter接口定義了一個如何把一個普通值轉化成driver.Value的接口
type ValueConverter interface { // ConvertValue將一個值轉換為驅動支持的Value類型 ConvertValue(v interface{}) (Value, error) }
ValueConverter接口提供了ConvertValue方法。
driver包提供了各種ValueConverter接口的實現,以保證不同驅動之間的實現和轉換的一致性。ValueConverter接口有如下用途:
- 轉換sql包提供的driver.Value類型值到數據庫指定列的類型,並保證它的匹配,例如保證某個int64值滿足一個表的uint16列。
- 轉換數據庫提供的值(即數據庫查詢結果)成driver.Value類型。
- 在Scan函數中被sql包用於將driver.Value類型轉換為用戶定義的類型。
12》driver.Valuer
type Valuer interface { // Value返回一個驅動支持的Value類型值 Value() (Value, error) }
Valuer接口定義了一個返回driver.Value的方法
很多類型都實現了這個Value方法,用來實現自身與driver.Value的轉換
一個驅動driver只要實現了上面的這些接口就能夠完成增刪改查等基本操作,剩下的就是與相應的數據庫進行數據交互等細節問題了
2)使用MySQL數據庫
1.MySQL驅動
Go中支持MySQL的驅動很多,有些支持database/sql標准,有些采用的是自己的實現接口。常用的有下面的幾種:
- https://github.com/go-sql-driver/mysql,支持database/sql,全部采用go寫
- https://github.com/ziutek/mymysql,支持database/sql,也支持自定義接口,全部采用go寫
- https://github.com/Philio/GoMySQL,不支持database/sql,自定義接口,全部采用go寫
在這里我們使用的是第一個驅動
首先可見該驅動源碼中mysql/driver.go為:
import ( "database/sql" "database/sql/driver" "net" "sync" ) type MySQLDriver struct{} func init() { sql.Register("mysql", &MySQLDriver{}) } func (d MySQLDriver) Open(dsn string) (driver.Conn, error) { ... } ...
當第三方開發者開發數據庫驅動時,都會實現init函數來完成本驅動的注冊,這樣才能在Open時使用"mysql"作為其參數driverName的值,說明打開的是上面注冊的mysql驅動
首先先在mysql中創建數據庫test,並生成兩個表,一個是用戶表userinfo,一個是關聯用戶信息表userdetail。使用workbench進行創建,首先創建數據庫test:
CREATE SCHEMA `test` DEFAULT CHARACTER SET utf8 ;
然后創建表:
use test; create table `userinfo` ( `uid` int(10) not null auto_increment, `username` varchar(64) null default null, `department` varchar(64) null default null, `created` date null default null, primary key (`uid`) ); create table `userdetail`( `uid` int(10) not null default '0', `intro` text null, `profile` text null, primary key (`uid`) );
接下來就示范怎么使用database/sql接口對數據庫進行增刪改查操作:
當然運行前首先需要下載驅動:
go get -u github.com/go-sql-driver/mysql
舉例;
package main import( "fmt" "database/sql" _ "github.com/go-sql-driver/mysql" ) func checkErr(err error){ if err != nil{ panic(err) } } func main() { db, err := sql.Open("mysql", "root:user78@/test") //后面格式為"user:password@/dbname" defer db.Close() checkErr(err) //插入數據 stmt, err := db.Prepare("insert userinfo set username = ?,department=?,created=?") checkErr(err) //執行准備好的Stmt res, err := stmt.Exec("user1", "computing", "2019-02-20") checkErr(err) //獲取上一個,即上面insert操作的ID id, err := res.LastInsertId() checkErr(err) fmt.Println(id) //1 //更新數據 stmt, err =db.Prepare("update userinfo set username=? where uid=?") checkErr(err) res, err = stmt.Exec("user1update", id) checkErr(err) affect, err := res.RowsAffected() checkErr(err) fmt.Println(affect) //1 //查詢數據 rows, err := db.Query("select * from userinfo") checkErr(err) for rows.Next() { var uid int var username, department, created string err = rows.Scan(&uid, &username, &department, &created) //1 user1update computing 2019-02-20 checkErr(err) fmt.Println(uid, username, department, created) } defer rows.Close() //關閉結果集,釋放鏈接 //刪除數據 stmt, err = db.Prepare("delete from userinfo where uid=?") checkErr(err) res, err = stmt.Exec(id) checkErr(err) affect, err = res.RowsAffected() checkErr(err) fmt.Println(affect) //1 }
返回:
userdeMBP:go-learning user$ go run test.go 1 1 1 user1update computing 2019-02-20 1
可以知道該操作成功了,但是從workbench中查看userinfo表中沒有變化,因為插入的數據又被刪除了,因此上面的三步如果一步步來我們就能夠看見如下的輸出:
1)先插入數據
package main import( "fmt" "database/sql" _ "github.com/go-sql-driver/mysql" ) func checkErr(err error){ if err != nil{ panic(err) } } func main() { db, err := sql.Open("mysql", "root:user78@/test") //后面格式為"user:password@/dbname" defer db.Close() checkErr(err) //插入數據 stmt, err := db.Prepare("insert userinfo set username = ?,department=?,created=?") checkErr(err) //執行准備好的Stmt res, err := stmt.Exec("user1", "computing", "2019-02-20") checkErr(err) //獲取上一個,即上面insert操作的ID id, err := res.LastInsertId() checkErr(err) fmt.Println(id) //2,因為上面進行過一次操作了,這次操作id會自增1,所以為2 affect, err := res.RowsAffected() checkErr(err) fmt.Println(affect)//1 }
workbench中可見表中數據為:
2)更改數據
package main import( "fmt" "database/sql" _ "github.com/go-sql-driver/mysql" ) func checkErr(err error){ if err != nil{ panic(err) } } func main() { id := 2 db, err := sql.Open("mysql", "root:user78@/test") //后面格式為"user:password@/dbname" defer db.Close() checkErr(err) //更新數據 stmt, err :=db.Prepare("update userinfo set username=? where uid=?") checkErr(err) res, err := stmt.Exec("user1update", id) checkErr(err) affect, err := res.RowsAffected() checkErr(err) fmt.Println(affect) //1 //查詢數據 rows, err := db.Query("select * from userinfo") checkErr(err) for rows.Next() { var uid int var username, department, created string err = rows.Scan(&uid, &username, &department, &created) checkErr(err) fmt.Println(uid, username, department, created) //2 user1update computing 2019-02-20 } defer rows.Close() //關閉結果集,釋放鏈接 }
workbench中可見表中數據變為:
3)刪除數據
package main import( "fmt" "database/sql" _ "github.com/go-sql-driver/mysql" ) func checkErr(err error){ if err != nil{ panic(err) } } func main() { id := 2 db, err := sql.Open("mysql", "root:user78@/test") //后面格式為"user:password@/dbname" defer db.Close() checkErr(err) //刪除數據 stmt, err := db.Prepare("delete from userinfo where uid=?") checkErr(err) res, err := stmt.Exec(id) checkErr(err) affect, err := res.RowsAffected() checkErr(err) fmt.Println(affect) //1 }
workbench表中數據就被清空了
上面代碼使用的函數的作用分別是:
1.sql.Open()函數用來打開一個注冊過的數據庫驅動,go-sql-driver/mysql中注冊了mysql這個數據庫驅動,第二個參數是DNS(Data Source Name),它是go-sql-driver/mysql定義的一些數據庫連接和配置信息,其支持下面的幾種格式:
user@unix(/path/to/socket)/dbname?charset=utf8 user:password@tcp(localhost:5555)/dbname?charset=utf8 user:password@/dbname user:password@tcp([de:ad:be::ca:fe]:80)/dbname
2.db.Prepare()函數用來返回准備要執行的sql操作,然后返回准備完畢的執行狀態
3.db.Query()函數用來直接執行Sql並返回Rows結果
4.stmt.Exec()函數用來執行stmt准備好的SQL語句
⚠️sql中傳入的參數都是=?對應的數據,這樣做可以在一定程度上防止SQL注入
還有調用其他驅動的例子,如sqlite3\Mongodb等,過程大同小異,這里省略
5.session和數據存儲
因為HTTP協議是無狀態的,所以每次請求都是無狀態的,因此為了解決在web操作中連接與用戶無法關聯的問題,提出了cookie和session的解決方案。
cookie機制是一種客戶端機制,把用戶數據保存在客戶端,客戶可更改;session機制是一種服務端機制,服務器使用一種類似散列表的結構來保存信息,每一個網站訪客都會被分配給一個唯一的標志符,即session ID,該ID的存放方式有兩種:一是經過URL,即GET方式傳遞給服務器,二是保存在客戶端的cookies中,通過cookie來獲取。當然你也可以將session保存到數據庫(如memcache或redis)中,更安全,但是效率會下降。
1)session和cookie
1》cookie
cookie簡而言之就是在本地計算機中保存一些用戶操作的歷史信息(包括登錄信息,是一小段文本信息),並且在用戶再次訪問該站點時瀏覽器通過HTTP協議將本地cookie內容發送給服務器,從而完成驗證
因此cookie存在一定的安全隱患。例如本地cookie中保存的用戶名密碼被破譯或cookie被其他網站收集
cookie是有時間限制的,根據生命周期的不同分成:
- 會話cookie:如果不設置過期時間,則默認cookie的生命周期為從創建到關閉瀏覽器為止。只要關閉了瀏覽器,cookie則消失。一般不保存在硬盤中,而是保存在內存中
- 持久cookie:設置了過期時間,該cookie會被保存在硬盤上,關閉后再次打開瀏覽器,這些cookie仍然有效直至超過設定的過期時間。
存儲在硬盤上的cookie能夠在不同的瀏覽器進程之間共享,比如兩個IE窗口。而對於保存在內存的cookie,不同的瀏覽器有不同的處理方式
Go中使用net/http中的SetCookie()函數來設置cookie:
func SetCookie
func SetCookie(w ResponseWriter, cookie *Cookie)
SetCookie在w的頭域中添加Set-Cookie頭,該HTTP頭的值為cookie。
然后使用request的Cookies()、Cookie(name string)函數和response的Cookies()函數來獲取設置的cookie信息
func (*Request) Cookies
func (r *Request) Cookies() []*Cookie
Cookies解析並返回該請求的Cookie頭設置的cookie。
func (*Request) Cookie
func (r *Request) Cookie(name string) (*Cookie, error)
Cookie返回請求中名為name的cookie,如果未找到該cookie會返回nil, ErrNoCookie。
func (*Response) Cookies
func (r *Response) Cookies() []*Cookie
Cookies解析並返回該回復中的Set-Cookie頭設置的cookie。
詳情可見本博客的go標准庫的學習-net/http
2》session
session就是在服務器上保存用戶操作的歷史信息。服務器使用session ID來標識session,session ID有服務器生成,保證其隨機性和唯一性,相當於一個隨機密鑰,避免在握手或者傳輸中暴露用戶真實密碼。該session ID會保存在cookie中
使用session過程:
- 當程序需要為某個客戶端的請求創建一個session時,服務器首先檢查這個客戶端的請求里面是否包含了一個session的表示標識,即session ID
- 如果包含session ID,則說明該用戶之前已經登錄過該服務器,創建過session,服務器就會根據該session ID把session搜索出來
- 如果檢索不到該session ID對應的session,則會新建一個,出現這種情況的原因是可能服務器已經刪除了該用戶對應的session對象,但用戶認為地在請求的URL后面附上一個JSESSION的參數來傳遞一個session ID
- 如果客戶的請求中不包含session ID,則為此客戶創建一個session並同時生成一個與該session相關聯的session ID,該session ID會在本次response響應中返回給客戶端保存
2)Go如何使用session
目前Go標准包中沒有為session提供支持,因此這里使用的是作者寫的session包,可見:https://github.com/astaxie/session,這個是舊版本,更新版本的可見https://github.com/astaxie/beego/tree/master/session
這里使用的是舊版本,來說明下要如何自定義session包
1》session的創建
- 生成全局唯一標識符session ID
- 開辟數據存儲空間。一般會在內存中創建相應的數據結構,但這種情況下,系統掉電將會丟失所有會話數據。因此最好是將會話數據寫到文件里或存儲在數據庫中,雖然會增加I/O開銷,但這樣可以實現某種程度的session持久化,也更有利於session的共享
- 將該session ID發送給客戶端
最關鍵就在於如何發送session ID,有兩種常用方法:cookie和URL重寫
- cookie:cookie服務端通過設置Set-cookie頭來將session標識符傳送到客戶端。這樣客戶端之后的每一次請求就都會帶着這個標識符。這種帶有session的cookie一般都會將失效時間設置為0,即該cookie為會話cookie
- URL重寫:在返回給用戶的頁面里的所有URL后面追加session標識符,這樣用戶在收到響應之后,無論是點擊響應頁面的哪個鏈接或者是提交表單,都會自動帶上這個session ID,從而實現了會話的保持。這種做法比較麻煩,但是在客戶端禁用cookie時,這種方案將是首選
2》Go實現session管理
- 全局session管理器
- 保證session的全局唯一性
- 為每一個顧客關聯一個session
- session的存儲(可以存儲到內存,文件,數據庫等)
- session過期處理
下面是實現session管理器Manager的代碼,該代碼主要實現了一下的功能:
- session ID的生成—sessionId() string:根據隨機數生成session ID
- session的創建—SessionStart(w http.ResponseWriter, r *http.Request) (session Session):判斷當前的請求request中是否存在有效的session,有則返回,否則創建
- session的銷毀—SessionDestroy(w http.ResponseWriter, r *http.Request) :銷毀session同時刪除sessionid
- session的垃圾回收GC—GC:將到期的session移除
代碼為:
package session import ( "crypto/rand" "encoding/base64" "fmt" "io" "net/http" "net/url" "sync" "time" ) type Session interface { Set(key, value interface{}) error //set session value Get(key interface{}) interface{} //get session value Delete(key interface{}) error //delete session value SessionID() string //back current sessionID } type Provider interface { SessionInit(sid string) (Session, error) SessionRead(sid string) (Session, error) SessionDestroy(sid string) error SessionGC(maxlifetime int64) } var provides = make(map[string]Provider) //根據provider的名字字符串來存儲對應的Provider // Register通過提供的名字來使得session的provider了用 // 如果Register被同樣的名字調用兩次或者驅動driver為nil,則會報錯Panic // 該函數由實現了Provider接口的結構體調用 func Register(name string, provide Provider) { if provide == nil { panic("session: Register provide is nil") } if _, dup := provides[name]; dup { //因為如果之前該name沒有調用過Register則不會在slice——provides中找到對應的值的 panic("session: Register called twice for provide " + name) } provides[name] = provide } //定義一個全局的session管理器 type Manager struct { cookieName string //private cookiename lock sync.Mutex //互斥鎖,用來保護session provider Provider //存儲session方式 maxlifetime int64 //cookie有效期 } //實例化一個session管理器 func NewManager(provideName, cookieName string, maxlifetime int64) (*Manager, error) { provider, ok := provides[provideName] if !ok { //說明該provider還沒有調用Register函數進行注冊 return nil, fmt.Errorf("session: unknown provide %q (forgotten import?)", provideName) } //否則就能夠使用該provider來生成session管理器 return &Manager{provider: provider, cookieName: cookieName, maxlifetime: maxlifetime}, nil } //判斷當前的請求request中是否存在有效的session,有則返回,否則創建 func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) { manager.lock.Lock() defer manager.lock.Unlock() cookie, err := r.Cookie(manager.cookieName) //從請求中獲取cookie值 if err != nil || cookie.Value == "" { //如果沒能得到cookie,則創建一個 sid := manager.sessionId() //首先新創建一個session ID session, _ = manager.provider.SessionInit(sid) //在Provider中根據提供的session ID初始化一個session //然后將session的唯一標識符sid寫到cookie中,這樣之后就能夠使用它去查看對應的session cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)} http.SetCookie(w, &cookie) //然后設置cookie } else {//如果有 sid, _ := url.QueryUnescape(cookie.Value) //QueryUnescape函數用於將QueryEscape轉碼的字符串還原,QueryEscape函數對string進行轉碼使之可以安全的用在URL查詢里 session, _ = manager.provider.SessionRead(sid) //從Provider中根據給定的session ID讀取session } return } //銷毀session同時刪除sessionid func (manager *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(manager.cookieName) if err != nil || cookie.Value == "" {//如果沒能得到cookie,則返回,沒得可刪的session return } else {//如果有 manager.lock.Lock() defer manager.lock.Unlock() manager.provider.SessionDestroy(cookie.Value) //消除Provider中的session expiration := time.Now() //並將相應cookie對應的session的失效信息返回 cookie := http.Cookie{Name: manager.cookieName, Path: "/", HttpOnly: true, Expires: expiration, MaxAge: -1} http.SetCookie(w, &cookie) } } //將到期的session移除 func (manager *Manager) GC() { manager.lock.Lock() defer manager.lock.Unlock() manager.provider.SessionGC(manager.maxlifetime) //session垃圾回收,即將到期的session移除 time.AfterFunc(time.Duration(manager.maxlifetime)*time.Second, func() { manager.GC() }) //即隔一段時間進行一次垃圾回收 } //生成session ID func (manager *Manager) sessionId() string { b := make([]byte, 32) if _, err := io.ReadFull(rand.Reader, b); err != nil { //ReadFull從rand.Reader精確地讀取len(b)字節數據填充進b。rand.Reader是一個全局、共享的密碼用強隨機數生成器 return "" } //舉例,如當b為[219 189 5 108 128 157 17 143 220 18 92 213 44 54 28 9 128 244 53 239 6 5 67 43 121 235 190 133 241 73 246 10] //下面返回的session ID為270FbICdEY_cElzVLDYcCYD0Ne8GBUMreeu-hfFJ9go= return base64.URLEncoding.EncodeToString(b)//將生成的隨機數b編碼后返回字符串 }
3)session存儲——存儲在內存中
下面的代碼實現的是將session存儲在內存中,如果你想要學習存儲在其他地方的實現,可見https://github.com/astaxie/beego/tree/master/session
實現代碼為memory.go:
package memory import ( "container/list" //list包實現了雙向鏈表 "github.com/astaxie/session" "sync" "time" ) //該SessionStore實現了session.go中的Session接口 type SessionStore struct { sid string //session id唯一標示 timeAccessed time.Time //最后訪問時間 value map[interface{}]interface{} //session里面存儲的值 } //設置 func (st *SessionStore) Set(key, value interface{}) error { st.value[key] = value //將該鍵值對存儲到SessionStore的value切片中 pder.SessionUpdate(st.sid) //更新st.sid對應的session的最后訪問時間並將其移到Provider的GC list的第一個位置 return nil } func (st *SessionStore) Get(key interface{}) interface{} { pder.SessionUpdate(st.sid) if v, ok := st.value[key]; ok {//得到session中key對應的value return v } else { return nil } return nil } func (st *SessionStore) Delete(key interface{}) error { delete(st.value, key) //按照指定的鍵key將元素從映射st.value中刪除 pder.SessionUpdate(st.sid) return nil } //得到session ID值 func (st *SessionStore) SessionID() string { return st.sid } //該Provider實現了session.go中的Provider接口,實現session是存儲在內存的 type Provider struct { lock sync.Mutex //用來鎖 sessions map[string]*list.Element //用來存儲session對應的內容到內存 list *list.List //用來做gc } //在Provider中根據提供的session ID初始化一個session func (pder *Provider) SessionInit(sid string) (session.Session, error) { pder.lock.Lock() defer pder.lock.Unlock() v := make(map[interface{}]interface{}, 0) //根據session生成一個SessionStore對象,timeAccessed為現在生成的時間,value為空列表 newsess := &SessionStore{sid: sid, timeAccessed: time.Now(), value: v} element := pder.list.PushBack(newsess) //然后將這個新SessionStore存儲到pder.list雙向列表的尾部,然后會返回一個生成的新元素,該元素的element.Value值就是這個新生成的SessionStore對象指針 pder.sessions[sid] = element //然后將這個session ID對應的生成的element對應存儲在Provider的session列表中 return newsess, nil //然后返回的是這個新生成的SessionStore對象指針 } //從Provider中根據給定的session ID讀取session func (pder *Provider) SessionRead(sid string) (session.Session, error) { if element, ok := pder.sessions[sid]; ok { //element.Value中存儲的就是該session ID 對應的&SessionStore對象指針 return element.Value.(*SessionStore), nil } else { //如果沒有,則重新初始化一個session sess, err := pder.SessionInit(sid) return sess, err } return nil, nil } //消除Provider中的session func (pder *Provider) SessionDestroy(sid string) error { if element, ok := pder.sessions[sid]; ok { delete(pder.sessions, sid) //從pder.sessions列表中刪除sid對應的session pder.list.Remove(element) //同時也將該元素從list列表中刪除 return nil } return nil } //session垃圾回收,即將到期的session移除 func (pder *Provider) SessionGC(maxlifetime int64) { pder.lock.Lock() defer pder.lock.Unlock() for { element := pder.list.Back() //Back返回鏈表最后一個元素 if element == nil { //直到為nil則說明鏈表中的element已經獲取完了,可以結束循環了 break } //查看session的有效時間是否已經到期,將到期的session移除 if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() { pder.list.Remove(element) delete(pder.sessions, element.Value.(*SessionStore).sid) } else { break } } } //更新某個session的最后訪問時間,並將該session移到Provider的GC list的第一個位置 func (pder *Provider) SessionUpdate(sid string) error { pder.lock.Lock() defer pder.lock.Unlock() if element, ok := pder.sessions[sid]; ok { //獲得某個session ID的存儲內容 element.Value.(*SessionStore).timeAccessed = time.Now() //將雙向鏈表的一個元素的值轉成*SessionStore類型然后設置其最后訪問時間 pder.list.MoveToFront(element) //MoveToFront將元素element移動到鏈表的第一個位置,說明這是最新操作的值 return nil } return nil } var pder = &Provider{list: list.New()} //New創建一個鏈表 func init() { pder.sessions = make(map[string]*list.Element, 0) session.Register("memory", pder) //調用session.go中的Register函數,將名字為"memory"的Provider-pder注冊
調用舉例說明如何使用:
package main import( "fmt" "net/http" "log" "html/template" "github.com/astaxie/session" _ "github.com/astaxie/session/providers/memory" ) var globalSessions *session.Manager var err error func init(){ globalSessions, err = session.NewManager("memory","gosessionid",60) //參數分別表示(provideName, cookieName string, maxlifetime int64) if err != nil{ log.Fatal(err) } go globalSessions.GC() } func index(w http.ResponseWriter, r *http.Request){ fmt.Fprintf(w, "hello world") //將html寫到w中,w中的內容將會輸出到客戶端中 } func login(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //獲得請求的方法 cookie, _ := r.Cookie("gosessionid") fmt.Printf("start before : Name = %s, Value = %s\n", cookie.Name, cookie.Value) sess := globalSessions.SessionStart(w, r) fmt.Printf("after start: Name = %s, Value = %s\n", cookie.Name, cookie.Value) fmt.Printf("Before - session name: %v\n", sess.Get("username")) r.ParseForm() if r.Method == "GET"{ html := `<html> <head> <title></title> </head> <body> <form action="http://localhost:9090/login" method="post"> username: <input type="text" name="username"> password: <input type="text" name="password"> <input type="submit" value="login"> {{.}} </form> </body> </html>` t := template.Must(template.New("test").Parse(html)) w.Header().Set("Content-Type", "text/html") t.Execute(w, nil) }else{ sess.Set("username", r.Form["username"]) fmt.Printf("After - session name: %v\n", sess.Get("username")) fmt.Println("username : ", template.HTMLEscapeString(r.Form.Get("username")))//在終端即客戶端輸出 fmt.Println("password : ", template.HTMLEscapeString(r.Form.Get("password")))//把r.Form.Get("password")轉義之后返回字符串 http.Redirect(w, r, "/", 302) //讓其上交完表單后就會自動重定向到"/"網址上 // template.HTMLEscape(w, []byte(r.Form.Get("username"))) //在客戶端輸出,把r.Form.Get("username")轉義后寫到w } } func main() { http.HandleFunc("/", index) //設置訪問的路由 http.HandleFunc("/login", login) //設置訪問的路由 err = http.ListenAndServe(":9090", nil) //設置監聽的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
返回:
userdeMBP:go-learning user$ go run test.go method GET start before : Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D //這里有值的原因是因為之前運行過一次,有效時間設置為了3600,按照道理如果是第一次調用,應該為空 after start: Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D Before - session name: <nil> method POST start before : Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D after start: Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D Before - session name: <nil> After - session name: [hello] //設置session后我們能夠看見能夠得到username的值 username : hello password : world
4)預防session劫持
在session技術中,客戶端和服務端通過session的標識符來維護會話,但這個標識符很容易就能夠被嗅探到,從而被其他人利用,這是中間人攻擊的一種類型
1>session的劫持
比如之前我們在chrome瀏覽器中生成了的name = gosessionid, value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D的
先不打開服務器,僅僅是在Firefox中訪問http://localhost:9090/login,當然會報頁面連接失敗的,然后我們使用Firefox的cookie editor插件來該網址上插入chrome中的cookie值
返回為:
{ "name": "gosessionid", "value": "qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D", "domain": "localhost", "hostOnly": true, "path": "/", "secure": false, "httpOnly": false, "sameSite": "no_restriction", "session": true, "firstPartyDomain": "", "storeId": null },
然后再將服務器打開,然后這時候再在瀏覽器中訪問http://localhost:9090/login時就能夠看見返回的結果也是:
userdeMBP:go-learning user$ go run test.go method GET start before : Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D after start: Name = gosessionid, Value = qNTgkr6ur7qncfxJDMuM5CvZeVYecK-EiyseILjE9oY%3D Before - session name: <nil>
按照道理來說不應該是相同的,這樣其實就說明我們成功實現了session的劫持
2>session劫持的預防
有三種解決方案:
- 一是session ID的值只允許通過cookie設置,而不是通過URL重置方式設置。同時設置cookie的httponly為true,這個屬性是設置是否可通過客戶端腳本訪問這個設置的cookie。這樣子設置的好處一是防止這個cookie被XSS讀取從而引起session劫持;二是cookie設置不會像URL重置方式那么容易獲取session ID
- 二是在每一個請求里面添加token,就是之前講的防止form重復遞交類似的功能。在每一個請求中加上一個隱藏的token,然后每次驗證這個token,從而保證用戶請求的唯一性
h := md5.New() salt := "some%^7&8888" io.WriteString(h, salt + time.Now().String()) token := fmt.Sprintf("%x", h.Sum(nil)) if r.Form["token"] != token { //提示登錄 } sess.Set("token", token)
- 三是給session額外設置一個創建時間的值,一旦過了這個時間,就把這個session ID銷毀,重新生成新的session,這樣可以在一定程度上防止session劫持的問題
createtime := sess.Get("createtime") if createtime == nil { sess.Get("createtime", time.Now().Unix()) }else if(createtime.(int64) + 60) < (time.Now().Unix()){ sess = globalSessions.SessionStart(w, r) }
6. 文本處理
1)XML處理
XML 被設計用來傳輸和存儲數據,HTML 被設計用來顯示數據。HTML 旨在顯示信息,而 XML 旨在傳輸信息。
XML 能把數據從 HTML 分離:
如果你需要在 HTML 文檔中顯示動態數據,那么每當數據改變時將花費大量的時間來編輯 HTML。
通過 XML,數據能夠存儲在獨立的 XML 文件中。這樣你就可以專注於使用 HTML 進行布局和顯示,並確保修改底層數據不再需要對 HTML 進行任何的改變。
通過使用幾行 JavaScript,你就可以讀取一個外部 XML 文件,然后更新 HTML 中的數據內容。
屬性和元素的區別:
請盡量使用元素來描述數據。而僅僅使用屬性來提供與數據無關的信息。元數據(有關數據的數據)應當存儲為屬性,而數據本身應當存儲為元素。
1》解析XML—— xml.Unmarshal()
為管理的服務器生成下面內容的XML配置文件:
<?xml version="1.0" encoding="utf-8"?> <servers version="1"> <server> <serverName>Shanghai_VPN</serverName> <serverIP>127.0.0.1</serverIP> </server> <server> <serverName>Beijing_VPN</serverName> <serverIP>127.0.0.2</serverIP> </server> </servers>
舉例:
xml文件為:
<?xml version="1.0" encoding="utf-8"?> <servers version="1"> <server> <serverName>Shanghai_VPN</serverName> <serverIP>127.0.0.1</serverIP> </server> <server> <serverName>Beijing_VPN</serverName> <serverIP>127.0.0.2</serverIP> </server> </servers>
舉例:
package main
import(
"fmt" "encoding/xml" "io/ioutil" "os" "log" ) type Recurlyservers struct {//后面的內容是struct tag,標簽,是用來輔助反射的 XMLName xml.Name `xml:"servers"` //將元素名寫入該字段 Version string `xml:"version,attr"` //將version該屬性的值寫入該字段 Svs []server `xml:"server"` Description string `xml:",innerxml"` //Unmarshal函數直接將對應原始XML文本寫入該字段 } type server struct{ XMLName xml.Name `xml:"server"` ServerName string `xml:"serverName"` ServerIP string `xml:"serverIP"` } func main() { file, err := os.Open("servers.xml") if err != nil { log.Fatal(err) } defer file.Close() data, err := ioutil.ReadAll(file) if err != nil { log.Fatal(err) } v := Recurlyservers{} err = xml.Unmarshal(data, &v) if err != nil { log.Fatal(err) } fmt.Println(v) fmt.Printf("XMLName: %#v\n", v.XMLName) fmt.Printf("Version: %q\n", v.Version) fmt.Printf("Server: %v\n", v.Svs) for i, svs := range v.Svs{ fmt.Println(i) fmt.Printf("Server XMLName: %#v\n", svs.XMLName) fmt.Printf("Server ServerName: %q\n", svs.ServerName) fmt.Printf("Server ServerIP: %q\n", svs.ServerIP) } fmt.Printf("Description: %q\n", v.Description) }
返回:
userdeMBP:go-learning user$ go run test.go
{{ servers} 1 [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}] <server> <serverName>Shanghai_VPN</serverName> <serverIP>127.0.0.1</serverIP> </server> <server> <serverName>Beijing_VPN</serverName> <serverIP>127.0.0.2</serverIP> </server> } XMLName: xml.Name{Space:"", Local:"servers"} Version: "1" Server: [{{ server} Shanghai_VPN 127.0.0.1} {{ server} Beijing_VPN 127.0.0.2}] 0 Server XMLName: xml.Name{Space:"", Local:"server"} Server ServerName: "Shanghai_VPN" Server ServerIP: "127.0.0.1" 1 Server XMLName: xml.Name{Space:"", Local:"server"} Server ServerName: "Beijing_VPN" Server ServerIP: "127.0.0.2" Description: "\n <server>\n <serverName>Shanghai_VPN</serverName>\n <serverIP>127.0.0.1</serverIP>\n </server>\n <server>\n <serverName>Beijing_VPN</serverName>\n <serverIP>127.0.0.2</serverIP>\n </server>\n"
xml解析到struct的與標簽tag相關的規則詳情可見go標准庫的學習-encoding/xml的Unmarshal()函數部分
只要設置對了tag,XML的解析就會變得十分簡單,tag和XML的element是一一對應的關系
為了正確解析,go語言的xml包要求struct定義中的所有字段都必須是可導出的,即首字母為大寫
2》輸出XML
如果想要生成上面所示的XML文件,需要使用到xml包中的下面兩個函數:
- func Marshal(v interface{}) ([]byte, error)
- func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
兩個函數第一個參數是用來生成XML的結構定義類型數據,都是返回生成的XML數據流
舉例生成上面解析的XML文件:
package main import( "fmt" "encoding/xml" "os" ) type Servers struct {//后面的內容是struct tag,標簽,是用來輔助反射的 XMLName xml.Name `xml:"servers"` //將元素名寫入該字段 Version string `xml:"version,attr"` //將version該屬性的值寫入該字段 Svs []server `xml:"server"` } type server struct{ ServerName string `xml:"serverName"` ServerIP string `xml:"serverIP"` } func main() { v := &Servers{Version : "1"} v.Svs = append(v.Svs, server{"Shanghai_VPN", "127.0.0.1"}) v.Svs = append(v.Svs, server{"Beijing_VPN", "127.0.0.2"}) //每個XML元素會另起一行並縮進,每行以prefix(這里為兩個空格)起始,后跟一或多個indent(這里為四個空格)的拷貝(根據嵌套層數) //即第一層嵌套只遞進四個空格,第二層嵌套則遞進八個空格 output, err := xml.MarshalIndent(v," ", " ") if err != nil{ fmt.Printf("error : %v\n", err) } os.Stdout.Write([]byte(xml.Header)) //輸出預定義的xml頭 <?xml version="1.0" encoding="UTF-8"?> os.Stdout.Write(output) }
返回:
userdeMBP:go-learning user$ go run test.go <?xml version="1.0" encoding="UTF-8"?> <servers version="1"> <server> <serverName>Shanghai_VPN</serverName> <serverIP>127.0.0.1</serverIP> </server> <server> <serverName>Beijing_VPN</serverName> <serverIP>127.0.0.2</serverIP> </server> </servers>
需要os.Stdout.Write([]byte(xml.Header))這句代碼是因為上面的兩個函數輸出的信息都是不帶XML頭的,為了生成正確的xml文件,需要使用xml包預定義的Header變量
另一個例子:
package main import( "fmt" "encoding/xml" "os" ) type Address struct { City, State string } type Person struct { XMLName xml.Name `xml:"person"` //該XML文件的根元素為person Id int `xml:"id,attr"` //該值會作為person元素的屬性 FirstName string `xml:"name>first"` //first為name的子元素 LastName string `xml:"name>last"` //last Age int `xml:"age"` Height float32 `xml:"height,omitempty"` //含omitempty選項的字段如果為空值會省略 Married bool //默認為false Address //匿名字段(其標簽無效)會被處理為其字段是外層結構體的字段,所以沒有Address這個元素,而是直接顯示City, State這兩個元素 Comment string `xml:",comment"` //注釋 } func main() { v := &Person{Id: 13, FirstName: "John", LastName: "Doe", Age: 42} v.Comment = " Need more details. " v.Address = Address{"Hanga Roa", "Easter Island"} output, err := xml.MarshalIndent(v, " ", " ") if err != nil { fmt.Printf("error: %v\n", err) } os.Stdout.Write(output) }
返回:
userdeMBP:go-learning user$ go run test.go <person id="13"> <name> <first>John</first> <last>Doe</last> </name> <age>42</age> <Married>false</Married> <City>Hanga Roa</City> <State>Easter Island</State> <!-- Need more details. --> </person>
如果是用的是xml.Marshal(v),返回為:
userdeMBP:go-learning user$ go run test.go <person id="13"><name><first>John</first><last>Doe</last></name><age>42</age><Married>false</Married><City>Hanga Roa</City><State>Easter Island</State><!-- Need more details. --></person>
可讀性就會變得差很多
2)JSON處理
1》解析json文件:
1>解析到結構體
使用的JSON包的方法為:
func Unmarshal
func Unmarshal(data []byte, v interface{}) error
舉例:
package main import( "fmt" "encoding/json" ) type Server struct { ServerName string ServerIP string } type Serverslice struct { Servers []Server } func main() { var s Serverslice //json文件的內容 str := ` { "servers" : [ { "serverName" : "Shanghai_VPN", "serverIP" : "127.0.0.1" }, { "serverName" : "Beijing_VPN", "serverIP" : "127.0.0.2" } ] }` json.Unmarshal([]byte(str), &s) fmt.Println(s) for i, svs := range s.Servers { fmt.Println(i) fmt.Printf("ServerName is %s\n", svs.ServerName) fmt.Printf("ServerIP is %s\n", svs.ServerIP) } }
返回:
userdeMBP:go-learning user$ go run test.go {[{Shanghai_VPN 127.0.0.1} {Beijing_VPN 127.0.0.2}]} 0 ServerName is Shanghai_VPN ServerIP is 127.0.0.1 1 ServerName is Beijing_VPN ServerIP is 127.0.0.2
⚠️:
能夠被賦值的字段必須是可導出字段,即首字母大寫
同時JSON解析只會解析能夠找得到的字段,如果找不到則忽略。這樣的好處是當你接收到一個很大的JSON數據結構並只想要獲取其中的部分數據的時候,你只需要將你想要的數據對應的字段名大寫,就可以輕動解決這個問題
就比如上面的例子,如果我將結構體中的serverName首字母小寫變成:
type Server struct { serverName string ServerIP string }
則得到的s將會是:
{[{ 127.0.0.1} { 127.0.0.2}]}
可見不會得到serverName的值
2>解析到interface
上面的解析方式是在我們知道被解析的json數據的結構的前提下使用的方法,當不能夠確定結構時應該使用interface
JSON包中使用map[string]interface{} 和 []interface{} 結構來存儲任意的JSON對象和數組
Go類型和JSON類型的對應關系是:
- bool代表JSON booleans
- float64 代表 JSON numbers
- string 代表 JSON strings
- nil 代表 JSON null
1.通過interface{} 與 type assert的配合,這樣就能夠解析未知結構的JSON數了
舉例:
package main import( "fmt" "encoding/json" ) func main() { var s interface{} str := ` { "name" : "testJSON", "servers" : [ { "serverName" : "Shanghai_VPN", "serverIP" : "127.0.0.1" }, { "serverName" : "Beijing_VPN", "serverIP" : "127.0.0.2" } ], "status" : false }` json.Unmarshal([]byte(str), &s) fmt.Println(s) //然后需要通過斷言來將其從interface{}類型轉成map[string]interface{}類型 m := s.(map[string]interface{}) for key, value := range m { switch v := value.(type){//得到s的類型 case string : fmt.Println(key , " is string ", v) case int : fmt.Println(key, "is int", v) case []interface{}: fmt.Println(key, "is an array") for i, vv := range v{ fmt.Println(i, vv) } default: fmt.Println(key, "is of a type i don't know how to handle") } } }
返回:
userdeMBP:go-learning user$ go run test.go map[name:testJSON servers:[map[serverName:Shanghai_VPN serverIP:127.0.0.1] map[serverName:Beijing_VPN serverIP:127.0.0.2]] status:false] name is string testJSON servers is an array 0 map[serverName:Shanghai_VPN serverIP:127.0.0.1] 1 map[serverIP:127.0.0.2 serverName:Beijing_VPN] status is of a type i don't know how to handle
2.go-simplejson包, https://github.com/bitly/go-simplejson
使用上面的類型斷言的方法操作起來其實並不是十分方便,我們可以使用一個名為go-simplejson的包,用來處理未知結構體的JSON
詳細內容可見go-simplejson文檔學習
舉例:
package main import( "fmt" "github.com/bitly/go-simplejson" "bytes" "log" ) func main() { buf := bytes.NewBuffer([]byte(`{ "test": { "array": [1, "2", 3], "arraywithsubs": [ {"subkeyone": 1}, {"subkeytwo": 2, "subkeythree": 3} ], "bignum": 8000000000 } }`)) js, err := simplejson.NewFromReader(buf) if err != nil || js == nil{ log.Fatal("something wrong when call NewFromReader") } fmt.Println(js) //&{map[test:map[array:[1 2 3] arraywithsubs:[map[subkeyone:1] map[subkeytwo:2 subkeythree:3]] bignum:8000000000]]} arr, err := js.Get("test").Get("array").Array() if err != nil || arr == nil{ log.Fatal("something wrong when call Get and Array") } fmt.Println(arr) //[1 2 3] //使用下面的Must類方法就不用判斷而err了 fmt.Println(js.Get("test").Get("array").MustArray()) //[1 2 3] fmt.Println(js.Get("test").Get("arraywithsubs").GetIndex(0).MustMap()) //map[subkeyone:1] fmt.Println(js.Get("test").Get("bignum").MustInt64()) //8000000000 }
2》生成JSON
使用兩個函數:
- func Marshal(v interface{}) ([]byte, error)
- func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
詳細內容可見go標准庫的學習-encoding/json
舉例:
package main
import(
"fmt" "encoding/json" "os" ) type Address struct { City, State string } type Person struct { Id int `json:"id"` //id作為該JSON字段的key值 FirstName string //不設置標簽則默認使用FirstName為字段key值 LastName string `json:"-"` //字段被本包忽略,即使有值也不輸出 Age int `json:",omitempty"` //含omitempty選項的字段如果為空值會省略,如果存在Age作為該JSON字段的key值 Height float32 `json:"height,omitempty"` //含omitempty選項的字段如果為空值會省略,如果存在height作為該JSON字段的key值 Address //匿名字段(其標簽無效)會被處理為其字段是外層結構體的字段,所以沒有Address這個元素,而是直接顯示City, State這兩個元素 } func main() { v := &Person{Id: 13, FirstName: "John", LastName: "Doe", Age: 42} v.Address = Address{"Hanga Roa", "Easter Island"} output, err := json.MarshalIndent(v, "&", " ")//每行以&作為前綴,並縮進四個空格 if err != nil { fmt.Printf("error: %v\n", err) } os.Stdout.Write(output) }
返回:
userdeMBP:go-learning user$ go run test.go
{
& "id": 13, & "FirstName": "John", & "Age": 42, & "City": "Hanga Roa", & "State": "Easter Island" &}
如果使用的是Marshal,則返回:
userdeMBP:go-learning user$ go run test.go
{"id":13,"FirstName":"John","Age":42,"City":"Hanga Roa","State":"Easter Island"}
Marshal只有在轉換成功的時候才會返回數據,因此在轉換過程中我們需要注意:
- Json對象只支持string作為key,所以要編碼一個map,必須是map[string]T這種類型(T是Go語言中任意類型)
- Channel,complex和function是不能夠被編碼成JSON的
- 嵌套的數據是不能編碼的,不然會讓JSON編碼進入死循環
- 指針在編碼的時候會輸出指針指向的內容,而空指針會輸出null
3)正則處理
雖然正則表達式比純粹的文本匹配效率低,但是它卻更靈活。可以使用官方提供的regexp標准包
⚠️:所有的字符都是UTF-8編碼的
1》通過正則判斷是否匹配
使用regexp包中的三個函數來實現,匹配則返回true,否則返回false:
- func Match(pattern string, b []byte) (matched bool, err error)
- func MatchString(pattern string, s string) (matched bool, err error)
- func MatchReader(pattern string, r io.RuneReader) (matched bool, err error)
上面的三個函數都實現了同一個功能,就是判斷pattern是否和輸入源匹配,匹配就返回true,如果解析正則出錯則返回error。三者的區別在於輸入源不同
比如驗證一個輸入是不是IP地址:
package main import( "fmt" "regexp" ) func IsIP(ip string) (b bool){ if m, _ := regexp.MatchString("^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$", ip); !m{ return false } return true } func main() { fmt.Println(IsIP("127.0.0.1")) //true fmt.Println(IsIP("127.0.0.1.1")) //false }
2)通過正則獲取內容
Match模式只能用來對字符串的判斷,如果想要截取部分字符串、過濾字符串或者提取出符合條件的一批字符串,就應該使用更復雜的方法
1》解析正則表達式的方法有:
- func Compile(expr string) (*Regexp, error)
- func CompilePOSIX(expr string) (*Regexp, error)
- func MustCompile(str string) *Regexp
- func MustCompilePOSIX(str string) *Regexp
上面的函數用於解析正則表達式是否合法,如果正確,則會返回一個Regexp,然后就能夠利用該對象在任意字符串上執行需要的操作
帶POSIX后綴的不同點在於其使用POSIX語法,該語法使用最長最左方式搜索,而不帶該后綴的方法是采用最左方式搜索(如[a-z]{2,4}這樣的正則表達式,應用於"aa09aaa88aaaa"這個文本串時,帶POSIX后綴的將返回aaaa,不帶后綴的則返回aa)。
前綴有Must的函數表示在解析正則表達式時,如果匹配模式串不滿足正確的語法則直接panic,而不加Must前綴的將只是返回錯誤
2》解析完正則表達式后能夠進行的操作有
1〉查找操作——即前綴帶有Find的函數
詳情可見go標准庫的學習-regexp
2)替換操作-即前綴帶有Replace的函數
詳情可見go標准庫的學習-regexp
舉例:
服務端為:
package main
import(
"fmt" "net/http" "log" "html/template" ) func index(w http.ResponseWriter, r *http.Request){ fmt.Fprintf(w, "hello world") //將html寫到w中,w中的內容將會輸出到客戶端中 } func login(w http.ResponseWriter, r *http.Request){ fmt.Println("method", r.Method) //獲得請求的方法 r.ParseForm() if r.Method == "GET"{ html := `<html> <HEAD> <title></title> </HEAD> <body> <form action="http://localhost:9090/login" method="post"> username: <input type="text" name="username"> password: <input type="text" name="password"> <input type="submit" value="login"> {{.}} </form> </body> </html>` t := template.Must(template.New("test").Parse(html)) w.Header().Set("Content-Type", "text/html") t.Execute(w, nil) } } func main() { http.HandleFunc("/", index) //設置訪問的路由 http.HandleFunc("/login", login) //設置訪問的路由 err := http.ListenAndServe(":9090", nil) //設置監聽的端口 if err != nil{ log.Fatal("ListenAndServe : ", err) } }
客戶端
package main
import(
"fmt" "regexp" "net/http" "strings" "io/ioutil" ) func main() { resp, err := http.Get("http://localhost:9090/login") if err != nil{ fmt.Println("http get err") } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil{ fmt.Println("http read err") return } src := string(body) fmt.Println("--------begin--------") fmt.Println(src) //將HTML標簽都轉成小寫 re, _ := regexp.Compile("\\<[\\S\\s]+?\\>") src = re.ReplaceAllStringFunc(src, strings.ToLower) fmt.Println("--------one--------") fmt.Println(src) //去除head re, _ = regexp.Compile("\\<head[\\S\\s]+?\\</head\\>") src = re.ReplaceAllString(src, "") fmt.Println("--------two--------") fmt.Println(src) //去除所有尖括號內的HTML代碼,並換成換行符 re, _ = regexp.Compile("\\<[\\S\\s]+?\\>") src = re.ReplaceAllString(src, "\n") fmt.Println("--------three--------") fmt.Println(src) //去除連續的換行符 re, _ = regexp.Compile("\\s{2,}") src = re.ReplaceAllString(src, "\n") fmt.Println("--------four--------") fmt.Println(src) //去掉空行 fmt.Println("--------five--------") fmt.Println(strings.TrimSpace(src)) // }
返回:
userdeMacBook-Pro:go-learning user$ go run test.go
--------begin--------
<html>
<HEAD>
<title></title>
</HEAD>
<body>
<form action="http://localhost:9090/login" method="post"> username: <input type="text" name="username"> password: <input type="text" name="password"> <input type="submit" value="login"> </form> </body> </html> --------one-------- <html> <head> <title></title> </head> <body> <form action="http://localhost:9090/login" method="post"> username: <input type="text" name="username"> password: <input type="text" name="password"> <input type="submit" value="login"> </form> </body> </html> --------two-------- <html> <body> <form action="http://localhost:9090/login" method="post"> username: <input type="text" name="username"> password: <input type="text" name="password"> <input type="submit" value="login"> </form> </body> </html> --------three-------- username: password: --------four-------- username: password: --------five-------- username: password:
3)前綴為Expand的函數
舉例:
package main
import(
"fmt" "regexp" ) func main() { src := []byte(` call hello alice hello bob call hello eve `) pat := regexp.MustCompile(`(?m)(call)\s+(?P<cmd>\w+)\s+(?P<arg>.+)\s*$`) res := []byte{} for _, s := range pat.FindAllSubmatchIndex(src, -1){ res = pat.Expand(res, []byte("$cmd('$arg')\n"), src, s) } fmt.Println(string(res)) }
返回:
userdeMacBook-Pro:go-learning user$ go run test.go
hello('alice') // hello('eve') userdeMacBook-Pro:go-learning user$
上面例子使用到的正則表達式語法為
分組:
(re) 編號的捕獲分組
(?P<name>re) 命名並編號的捕獲分組 (?:re) 不捕獲的分組 (?flags) 設置當前所在分組的標志,不捕獲也不匹配 (?flags:re) 設置re段的標志,不捕獲的分組
flags的語法為xyz(設置)、-xyz(清楚)、xy-z(設置xy,清楚z),標志如下:
I 大小寫敏感(默認關閉)
m ^和$在匹配文本開始和結尾之外,還可以匹配行首和行尾(默認開啟) s 讓.可以匹配\n(默認關閉) U 非貪婪的:交換x*和x*?、x+和x+?……的含義(默認關閉)
因此
- (?m)表示行首
- (?P<cmd>\w+)表示將符合\w+(\w== [0-9A-Za-z_])正則表達式的值命名為name,之后再使用函數時可以使用$name來表達
4)模版處理—text/template包
MVC設計模式:Model處理數據,View展示結果,Controller控制用戶的請求。
在View層的處理都是通過在靜態HTML中插入動態語言生成的數據來實現的,模版就是用來獲取數據然后渲染數據的。
5)字符串處理
1》字符串操作——常用函數——strings包
func Contains
func Contains(s, substr string) bool
判斷字符串s是否包含子串substr。
舉例:
package main
import(
"fmt" "strings" ) func main() { fmt.Println(strings.Contains("seafood", "foo")) //true fmt.Println(strings.Contains("seafood", "bar")) //false fmt.Println(strings.Contains("seafood", "")) //true fmt.Println(strings.Contains("", "")) //true }
func Index
func Index(s, sep string) int
子串sep在字符串s中第一次出現的位置,不存在則返回-1。
舉例:
package main
import(
"fmt" "strings" ) func main() { fmt.Println(strings.Index("chicken", "ken")) //4 fmt.Println(strings.Index("chicken", "dmr")) //-1 }
func Join
func Join(a []string, sep string) string
將一系列字符串連接為一個字符串,之間用sep來分隔。
舉例:
package main
import(
"fmt" "strings" ) func main() { s := []string{"foo", "bar", "baz"} fmt.Println(strings.Join(s, ", "))//foo, bar, baz }
func Repeat
func Repeat(s string, count int) string
返回count個s串聯的字符串。
舉例:
package main
import(
"fmt" "strings" ) func main() { fmt.Println("ba" + strings.Repeat("na", 2)) //banana }
func Replace
func Replace(s, old, new string, n int) string
返回將s中前n個不重疊old子串都替換為new的新字符串,如果n<0會替換所有old子串。
舉例:
package main
import(
"fmt" "strings" ) func main() { fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2)) //oinky oinky oink fmt.Println(strings.Replace("oink oink oink", "oink", "moo", -1)) //moo moo moo }
func Split
func Split(s, sep string) []string
用去掉s中出現的sep的方式進行分割,會分割到結尾,並返回生成的所有片段組成的切片(每一個sep都會進行一次切割,即使兩個sep相鄰,也會進行兩次切割)。如果sep為空字符,Split會將s切分成每一個unicode碼值一個字符串。
舉例:
package main
import(
"fmt" "strings" ) func main() { fmt.Printf("%q\n", strings.Split("a,b,c", ",")) //["a" "b" "c"] fmt.Printf("%q\n", strings.Split("a man a plan a canal panama", "a ")) //["" "man " "plan " "canal panama"] fmt.Printf("%q\n", strings.Split(" xyz ", "")) //[" " "x" "y" "z" " "] fmt.Printf("%q\n", strings.Split("", "Bernardo O'Higgins")) //[""] }
func Trim
func Trim(s string, cutset string) string
返回將s前后端所有cutset包含的utf-8碼值都去掉的字符串。
舉例:
package main
import(
"fmt" "strings" ) func main() { fmt.Printf("[%q]\n", strings.Trim(" !!! Achtung! Achtung! !!! ", "! ")) //["Achtung! Achtung"] }
func Fields
func Fields(s string) []string
返回將字符串按照空白(unicode.IsSpace確定,可以是一到多個連續的空白字符)分割的多個字符串。如果字符串全部是空白或者是空字符串的話,會返回空切片。
舉例:
package main
import(
"fmt" "strings" ) func main() { fmt.Printf("Fields are: %q\n", strings.Fields(" foo bar baz ")) //Fields are: ["foo" "bar" "baz"] }
更多詳情可見go標准庫的學習-strings-字符串操作
2)字符串轉換——strconv包
strconv包實現了基本數據類型和其字符串表示的相互轉換。
1)append系列
將值添加到現有的字節數組中
func AppendBool
func AppendBool(dst []byte, b bool) []byte
等價於append(dst, FormatBool(b)...)
func AppendInt
func AppendInt(dst []byte, i int64, base int) []byte
等價於append(dst, FormatInt(I, base)...)
func AppendUint
func AppendUint(dst []byte, i uint64, base int) []byte
等價於append(dst, FormatUint(I, base)...)
func AppendFloat
func AppendFloat(dst []byte, f float64, fmt byte, prec int, bitSize int) []byte
等價於append(dst, FormatFloat(f, fmt, prec, bitSize)...)
func AppendQuote
func AppendQuote(dst []byte, s string) []byte
等價於append(dst, Quote(s)...)
func AppendQuoteToASCII
func AppendQuoteToASCII(dst []byte, s string) []byte
等價於append(dst, QuoteToASCII(s)...)
func AppendQuoteRune
func AppendQuoteRune(dst []byte, r rune) []byte
等價於append(dst, QuoteRune(r)...)
func AppendQuoteRuneToASCII
func AppendQuoteRuneToASCII(dst []byte, r rune) []byte
等價於append(dst, QuoteRuneToASCII(r)...)
package main
import(
"fmt" "strconv" ) func main() { str := make([]byte, 0, 100) str = strconv.AppendInt(str, 4567, 10) str = strconv.AppendBool(str, false) str = strconv.AppendQuote(str, "abcdefg") str = strconv.AppendQuoteRune(str, '單') fmt.Println(string(str)) //4567false"abcdefg"'單' }
2)Format系列
將其他類型值轉換為字符串func FormatBool
func FormatBool(b bool) string
根據b的值返回"true"或"false"。
func FormatInt
func FormatInt(i int64, base int) string
返回i的base進制的字符串表示。base 必須在2到36之間,結果中會使用小寫字母'a'到'z'表示大於10的數字。
func FormatUint
func FormatUint(i uint64, base int) string
是FormatInt的無符號整數版本。
func FormatFloat
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
函數將浮點數表示為字符串並返回。
bitSize表示f的來源類型(32:float32、64:float64),會據此進行舍入。
fmt表示格式:'f'(-ddd.dddd)、'b'(-ddddp±ddd,指數為二進制)、'e'(-d.dddde±dd,十進制指數)、'E'(-d.ddddE±dd,十進制指數)、'g'(指數很大時用'e'格式,否則'f'格式)、'G'(指數很大時用'E'格式,否則'f'格式)。
prec控制精度(排除指數部分):對'f'、'e'、'E',它表示小數點后的數字個數;對'g'、'G',它控制總的數字個數。如果prec 為-1,則代表使用最少數量的、但又必需的數字來表示f。
package main
import(
"fmt" "strconv" ) func main() { a := strconv.FormatBool(false) //'g'表示格式(指數很大時用'e'格式-d.dddde±dd,否則'f'格式-ddd.dddd) //對於'g',后面的12表示控制總的數字個數,控制精度 //64表示來源類型為float64 b := strconv.FormatFloat(123.23, 'g', 12, 64) e := strconv.FormatFloat(123.23, 'g', 4, 64) c := strconv.FormatInt(1234, 10) //10表示10進制 d := strconv.FormatUint(12345, 10) fmt.Println(a, b, e, c, d) //false 123.23 123.2 1234 12345 }
3)Parse系列
把字符串轉換為其他類型
func ParseBool
func ParseBool(str string) (value bool, err error)
返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否則返回錯誤。
func ParseInt
func ParseInt(s string, base int, bitSize int) (i int64, err error)
返回字符串表示的整數值,接受正負號。
base指定進制(2到36),如果base為0,則會從字符串前置判斷,"0x"是16進制,"0"是8進制,否則是10進制;
bitSize指定結果必須能無溢出賦值的整數類型,0、8、16、32、64 分別代表 int、int8、int16、int32、int64;返回的err是*NumErr類型的,如果語法有誤,err.Error = ErrSyntax;如果結果超出類型范圍err.Error = ErrRange。
func ParseUint
func ParseUint(s string, base int, bitSize int) (n uint64, err error)
ParseUint類似ParseInt但不接受正負號,用於無符號整型。
func ParseFloat
func ParseFloat(s string, bitSize int) (f float64, err error)
解析一個表示浮點數的字符串並返回其值。
如果s合乎語法規則,函數會返回最為接近s表示值的一個浮點數(使用IEEE754規范舍入)。bitSize指定了期望的接收類型,32是float32(返回值可以不改變精確值的賦值給float32),64是float64;返回值err是*NumErr類型的,語法有誤的,err.Error=ErrSyntax;結果超出表示范圍的,返回值f為±Inf,err.Error= ErrRange。
package main
import(
"fmt" "strconv" ) func main() { a, err := strconv.ParseBool("false") if err != nil{ fmt.Println(err) } //64表示返回類型為float64 b, err := strconv.ParseFloat("123.23", 64) if err != nil{ fmt.Println(err) } //10表示10進制,64表示返回int64 c, err := strconv.ParseInt("1234", 10, 64) if err != nil{ fmt.Println(err) } d, err := strconv.ParseUint("12345", 10, 64) if err != nil{ fmt.Println(err) } fmt.Println(a, b, c, d) //false 123.23 1234 12345 }
7.web服務
web服務可以讓你在HTTP協議的基礎上通過XML或者JSON來交換信息
web服務背后的關鍵在於平台的無關性,你可以運行你的服務在Linux系統上,可以與其他Window的asp.net程序交互。同樣的,也可以通過同一個接口和運行在FreeBSD上面的JSP無障礙地通信
目前有以下幾種主流的web服務:REST(更簡潔),SOAP,XML-RPC
什么是REST?可見https://blog.csdn.net/qq_21383435/article/details/80032375
1)Socket編程
常見socket(套接字)分為兩種:
- 流式socket(SOCK STREAM)
- 數據報Socket(SOCK DGRAM)
流式是一種面向連接的socket,針對面向連接的TCP服務應用;數據報式socket是一種無連接的socket,對應於無連接的UDP服務應用
進程間通過socket進行通信需要解決如何唯一標識一個進程的問題:
在本地可以通過進程PID來唯一標識一個進程,但是這在網絡中是行不通的。TCP/IP協議族已經解決了這個問題
通過網絡層的IP地址可以為宜標識網絡中的主機,而傳輸層的"協議TCP/UDP+端口"能夠唯一標識主機中的進程。這樣講兩者結合起來,利用三元組(IP地址,協議,端口)就可以唯一表示網絡上的進程了,這樣進程間就能夠進行通信了
Go的net包中定義了很多類型、函數和方法用來網絡編程
1》其中IP的定義為:
type IP
type IP []byte
常用方法有:
func ParseIP
func ParseIP(s string) IP
ParseIP將s解析為IP地址,並返回該地址。如果s不是合法的IP地址文本表示,ParseIP會返回nil。
字符串可以是小數點分隔的IPv4格式(如"74.125.19.99")或IPv6格式(如"2001:4860:0:2001::68")格式。
舉例:
package main import( "fmt" "os" "net" ) func main() { if len(os.Args) != 2{ fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0]) os.Exit(1) } name := os.Args[1] addr := net.ParseIP(name) if addr == nil { fmt.Println("Invalid address") }else{ fmt.Println("the address is", addr.String()) } os.Exit(0) }
返回:
userdeMacBook-Pro:go-learning user$ go run test.go Usage: /var/folders/2_/g5wrlg3x75zbzyqvsd5f093r0000gn/T/go-build258331112/b001/exe/test ip-addr exit status 1 userdeMacBook-Pro:go-learning user$ go run test.go 127.0.0.1 the address is 127.0.0.1
2》TCP Socket —— net包中的TCPConn
當我們知道如何通過網絡端口訪問一個服務后:
- 作為客戶端,我們可以通過向遠端某台機器的某個網絡端口發送一個請求,然后在機器的此端口上監聽服務反饋的信息
- 作為服務端,需要把服務綁定到某個指定端口,並且在此端口上監聽,當有客戶端來訪問時能夠讀取信息並且寫入反饋信息
Go的net包中有一個類型TCPConn,這個類型能夠用來作為客戶端和服務器交互的通道,里面主要使用的是下面的兩個函數:
- func (c *TCPConn) Read(b []byte) (int, error)
- func (c *TCPConn) Write(b []byte) (int, error)
還需要知道TCPAddr類型:
type TCPAddr
type TCPAddr struct { IP IP Port int Zone string // IPv6范圍尋址域 }
TCPAddr代表一個TCP終端地址。
使用的函數有:
- func ResolveTCPAddr(net, addr string) (*TCPAddr, error)
參數addr表示域名或IP地址,如“www.baidu.com:80”或“127.0.0.1:22”。格式為"host:port"或"[ipv6-host%zone]:port",解析得到網絡名和端口名;
net參數必須是"tcp"、"tcp4"或"tcp6",表示(IPv4\IPv6任意一個)、(IPv4-only)或者(IPv6-only)。
1>TCP客戶端
首先使用下面的函數來建立一個TCP連接,並返回一個TCPConn類型的對象,當連接建立時服務器端也會創建一個同類型的對象,此時客戶端和服務端通過各自擁有的TCPConn對象來進行數據交換:
- func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)
參數net必須是"tcp"、"tcp4"、"tcp6",表示(IPv4\IPv6任意一個)、(IPv4-only)或者(IPv6-only);
laddr表示本機地址,一般為nil.如果laddr不是nil,將使用它作為本地地址,即客戶端,否則自動選擇一個本地地址;
raddr表示遠程的服務地址,即服務端
一般來說,客戶端通過TCPConn對象將請求信息發送到服務端,然后等待讀取服務端響應的信息。服務端讀取並解析來自客戶端的請求,並返回應答信息。這個連接只有當任一端關閉后連接才會失效,不然連接可以一直使用
舉例,模擬一個基於HTTP協議的客戶端請求去連接一個web服務端
首先是實現一個客戶端,代碼如下:
package main import( "fmt" "net" "io/ioutil" "os" ) func checkError(index int, err error){ if err != nil{ fmt.Fprintf(os.Stderr, "index : %v,Fatal error : %s", index, err.Error()) os.Exit(1) } } func main() { if len(os.Args) != 2 { fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0]) os.Exit(1) } service := os.Args[1] tcpAddr, err := net.ResolveTCPAddr("tcp4", service) checkError(1, err) //創建一個TCP連接conn conn, err := net.DialTCP("tcp", nil, tcpAddr) checkError(2, err) //通過conn來發送請求信息 _, err = conn.Write([]byte("HEAD / HTTP/1.0 \r\n\r\n")) checkError(3, err) //從conn中讀取服務端返回的全部的文本 result, err := ioutil.ReadAll(conn) checkError(4, err) fmt.Println(string(result)) os.Exit(0) }
2>TCP服務端
然后是編寫服務端,實現簡單的時間同步服務,監聽7777端口:
在服務端需要綁定服務到指定的非激活端口,並監聽此端口,當有客戶端請求到達的時候可以接收到來自客戶端連接的請求,需要使用的函數有:
- func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)
- func (l *TCPListener) Accept() (Conn, error)
詳情可見go標准庫的學習-net
package main import( "fmt" "net" "os" "time" ) func checkError(err error){ if err != nil{ fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error()) os.Exit(1) } } func main() { service := ":7777" tcpAddr, err := net.ResolveTCPAddr("tcp4", service) checkError(err) listener, err := net.ListenTCP("tcp", tcpAddr) checkError(err) for i := 0; i < 4; i++{ conn, err := listener.Accept() if err != nil{ continue } daytime := time.Now().String() conn.Write([]byte(daytime)) conn.Close() } }
客戶端訪問服務器后會出錯:
userdeMBP:go-learning user$ go run test.go 127.0.0.1:7777 Fatal error : read tcp 127.0.0.1:51349->127.0.0.1:7777: read: connection reset by peerexit status 1
這個錯誤是因為使用了ioutil.ReadAll(conn),原因是什么不清楚,推薦還是使用conn.Read()方法,即客戶端改成:
package main import( "fmt" "net" "os" ) func checkError(index int, err error){ if err != nil{ fmt.Fprintf(os.Stderr, "index : %v,Fatal error : %s", index, err.Error()) os.Exit(1) } } func main() { if len(os.Args) != 2 { fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0]) os.Exit(1) } service := os.Args[1] tcpAddr, err := net.ResolveTCPAddr("tcp4", service) checkError(1, err) //創建一個TCP連接conn conn, err := net.DialTCP("tcp", nil, tcpAddr) checkError(2, err) //通過conn來發送請求信息 _, err = conn.Write([]byte("HEAD / HTTP/1.0 \r\n\r\n")) checkError(3, err) //從conn中讀取服務端返回的全部的文本 rsp := make([]byte, 64) n, err := conn.Read(rsp) checkError(4, err) fmt.Printf("receive %d bytes in response : %q\n", n, rsp[:n]) os.Exit(0) }
上面的服務器跑起來后,它會一直在那里等待,直到有新的客戶端請求到達並同意接收Accept()該請求,這時候服務端就會反饋給客戶端當前的時間信息
在該代碼的for循環中當有錯誤發生時,直接continue而不是退出,是因為在服務端跑代碼的時候,當有錯誤發生的情況下最好是由服務器記錄錯誤,然后當前連接的客戶端直接報錯而退出, 從而不會影響到當前服務端運行的整個服務
上面的代碼的缺點在於執行的時候是單任務的,不能同時接收多個請求。為了支持並發使用了goroutine機制,因此服務端改成:
package main import( "fmt" "net" "os" "time" ) func checkError(err error){ if err != nil{ fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error()) os.Exit(1) } } func main() { service := ":1200" tcpAddr, err := net.ResolveTCPAddr("tcp4", service) checkError(err) listener, err := net.ListenTCP("tcp", tcpAddr) checkError(err) for{ conn, err := listener.Accept() if err != nil{ continue } go handleerClient(conn) } } func handleerClient(conn net.Conn){ defer conn.Close() daytime := time.Now().String() conn.Write([]byte(daytime)) }
上面通過將業務處理分離到函數handleerClient,這樣就能夠實現多並發執行了
這樣運行起服務端監聽等待着客戶端的請求,然后再運行客戶端能夠返回:
userdeMBP:go-learning user$ go run test.go 127.0.0.1:1200 receive 51 bytes in response : "2019-02-27 15:25:02.113373 +0800 CST m=+5.168778770"
3>控制TCP連接函數有:
- func (c *TCPConn) SetKeepAlive(keepalive bool) error
- func (c *TCPConn) SetTimeOut(nsec int64) error
SetTimeOut函數用來設置連接的超時時間,客戶端和服務端都適用,當超過設置的時間該連接就會失效
SetKeepAlive函數用來設置客戶端是否和服務端一直保持着連接,即使沒有任何的數據發送
詳情可見go標准庫的學習-net
3》UDP Socket —— net包中的UDPConn
和上面的TCPConn的操作是相似的,不同之處是服務端處理多個客戶端請求數據包的方式不同,UDP缺少了對客戶端連接請求的Accept函數,使用下面的幾個函數:
- func ResolveUDPAddr(net, addr string) (*UDPAddr, error)
- func DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error)
- func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error)
- func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err error)
- func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error)
- func (c *UDPConn) Read(b []byte) (int, error)
- func (c *UDPConn) Write(b []byte) (int, error)
客戶端:
package main
import(
"fmt" "net" "os" ) func checkError(index int, err error){ if err != nil{ fmt.Fprintf(os.Stderr, "index : %v,Fatal error : %s", index, err.Error()) os.Exit(1) } } func main() { if len(os.Args) != 2 { fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0]) os.Exit(1) } service := os.Args[1] udpAddr, err := net.ResolveUDPAddr("udp", service) checkError(1, err) //創建一個TCP連接conn conn, err := net.DialUDP("udp", nil, udpAddr) defer conn.Close() checkError(2, err) //通過conn來發送請求信息 _, err = conn.Write([]byte("anything")) checkError(3, err) //從conn中讀取服務端返回的全部的文本 var rsp [512]byte n, err := conn.Read(rsp[0:]) checkError(4, err) fmt.Printf("receive %d bytes in response : %q\n", n, rsp[:n]) os.Exit(0) }
服務端
package main
import(
"fmt" "net" "os" "time" ) func checkError(err error){ if err != nil{ fmt.Fprintf(os.Stderr, "Fatal error : %s", err.Error()) os.Exit(1) } } func main() { service := ":11200" udpAddr, err := net.ResolveUDPAddr("udp", service) checkError(err) conn, err := net.ListenUDP("udp", udpAddr) defer conn.Close() checkError(err) for{ go handlerClient(conn) } } func handlerClient(conn *net.UDPConn){ var rsp [512]byte _, addr, err := conn.ReadFromUDP(rsp[0:]) if err != nil{ return } daytime := time.Now().String() conn.WriteToUDP([]byte(daytime), addr) }
客戶端返回:
userdeMBP:go-learning user$ go run test.go 127.0.0.1:11200 receive 51 bytes in response : "2019-02-27 16:25:46.905443 +0800 CST m=+2.197257345"
2)WebSocket——實現即時更新數據,瀏覽器與服務器能進行全雙工通信
參考http://www.runoob.com/html/html5-websocket.html
socket和websocket的區別():
Socket 其實並不是一個協議,是應用層與 TCP/IP 協議族通信的中間軟件抽象層,它是一組接口。當兩台主機通信時,讓 Socket 去組織數據,以符合指定的協議。TCP 連接則更依靠於底層的 IP 協議,IP 協議的連接則依賴於鏈路層等更低層次。
WebSocket 則是一個典型的應用層協議。
總的來說:Socket 是傳輸控制層協議,WebSocket 是應用層協議。
Websocket 使用 ws 或 wss 的統一資源標志符,類似於 HTTPS,其中 wss 表示在 TLS 之上的 Websocket。如:
ws://example.com/wsapi wss://secure.example.com/
Websocket 使用和 HTTP 相同的 TCP 端口,可以繞過大多數防火牆的限制。默認情況下,Websocket 協議使用 80 端口;運行在 TLS 之上時,默認使用 443 端口。
一個典型的Websocket握手請求如下:
客戶端請求
GET / HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: example.com Origin: http://example.com Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ== Sec-WebSocket-Version: 13
服務器回應
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s= Sec-WebSocket-Location: ws://example.com/
- Connection 必須設置 Upgrade,表示客戶端希望連接升級。
- Upgrade 字段必須設置 Websocket,表示希望升級到 Websocket 協議。
- Sec-WebSocket-Key 是隨機的字符串,服務器端會用這些數據來構造出一個 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一個特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計算 SHA-1 摘要,之后進行 BASE-64 編碼,將結果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端。如此操作,可以盡量避免普通 HTTP 請求被誤認為 Websocket 協議。
- Sec-WebSocket-Version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均應當棄用。
- Origin 字段是可選的,通常用來表示在瀏覽器中發起此 Websocket 連接所在的頁面,類似於 Referer。但是,與 Referer 不同的是,Origin 只包含了協議和主機名稱。
- 其他一些定義在 HTTP 協議中的字段,如 Cookie 等,也可以在 Websocket 中使用。
websocket協議的交互流程:
客戶端首先發起一個Http請求到服務端,請求的特殊之處,在於在請求里面帶了一個upgrade的字段,告訴服務端,我想生成一個websocket的協議,服務端收到請求后,會給客戶端一個握手的確認,返回一個switching, 意思允許客戶端向websocket協議轉換,完成這個協商之后,客戶端與服務端之間的底層TCP協議是沒有中斷的,接下來,客戶端可以向服務端發起一個基於websocket協議的消息,服務端也可以主動向客戶端發起websocket協議的消息,websocket協議里面通訊的單位就叫message。
Go語言標准包中沒有提供對WebSocket的支持,但是在官方維護的go.net子包中有對這個的支持,安裝該包:
go get github.com/gorilla/websocket
客戶端通過WebSocket函數建立了一個與服務器的連接sock。當握手成功后,會觸發WebSocket對象的onopen事件,告訴客戶端連接已經建立成功。在客戶端中一共綁定了四個事件:
- onopen:建立連接后會觸發該事件
- onmessage:收到消息后會觸發該事件
- onerror:發生錯誤時會觸發該事件
- onclose:關閉連接時會觸發該事件
服務端代碼:
package main import ( "flag" "html/template" "log" "net/http" "github.com/gorilla/websocket" ) var addr = flag.String("addr", "localhost:8080", "http service address") var upgrader = websocket.Upgrader{} // use default options func echo(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Print("upgrade:", err) return } defer c.Close() for { mt, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) break } log.Printf("recv: %s", message) err = c.WriteMessage(mt, message) if err != nil { log.Println("write:", err) break } } } func home(w http.ResponseWriter, r *http.Request) { homeTemplate.Execute(w, "ws://"+r.Host+"/echo") } func main() { flag.Parse() log.SetFlags(0) http.HandleFunc("/echo", echo) http.HandleFunc("/", home) log.Fatal(http.ListenAndServe(*addr, nil)) } var homeTemplate = template.Must(template.New("").Parse(` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script> window.addEventListener("load", function(evt) { var output = document.getElementById("output"); var input = document.getElementById("input"); var ws; var print = function(message) { var d = document.createElement("div"); d.innerHTML = message; output.appendChild(d); }; document.getElementById("open").onclick = function(evt) { if (ws) { return false; } ws = new WebSocket("{{.}}"); ws.onopen = function(evt) { print("OPEN"); } ws.onclose = function(evt) { print("CLOSE"); ws = null; } ws.onmessage = function(evt) { print("RESPONSE: " + evt.data); } ws.onerror = function(evt) { print("ERROR: " + evt.data); } return false; }; document.getElementById("send").onclick = function(evt) { if (!ws) { return false; } print("SEND: " + input.value); ws.send(input.value); return false; }; document.getElementById("close").onclick = function(evt) { if (!ws) { return false; } ws.close(); return false; }; }); </script> </head> <body> <table> <tr><td valign="top" width="50%"> <p>Click "Open" to create a connection to the server, "Send" to send a message to the server and "Close" to close the connection. You can change the message and send multiple times. <p> <form> <button id="open">Open</button> <button id="close">Close</button> <p><input id="input" type="text" value="Hello world!"> <button id="send">Send</button> </form> </td><td valign="top" width="50%"> <div id="output"></div> </td></tr></table> </body> </html> `))
然后在瀏覽器中調用127.0.0.1:8080:
點擊open將創建連接,send將發送數據,close將關閉連接。然后就會相應地在網站的右邊返回輸出:
服務端的輸出為:
recv: Hello world! read: websocket: close 1005 (no status)
當然,也可以編寫一個客戶端來與服務端互相發送時間信息:
package main import ( "flag" "log" "net/url" "os" "os/signal" "time" "github.com/gorilla/websocket" ) var addr = flag.String("addr", "localhost:8080", "http service address") func main() { flag.Parse() log.SetFlags(0) interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo"} log.Printf("connecting to %s", u.String()) c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { log.Fatal("dial:", err) } defer c.Close() done := make(chan struct{}) go func() { defer close(done) for { _, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) return } log.Printf("recv: %s", message) } }() ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case <-done: return case t := <-ticker.C: err := c.WriteMessage(websocket.TextMessage, []byte(t.String())) if err != nil { log.Println("write:", err) return } case <-interrupt: log.Println("interrupt") // Cleanly close the connection by sending a close message and then // waiting (with timeout) for the server to close the connection. err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { log.Println("write close:", err) return } select { case <-done: case <-time.After(time.Second): } return } } }
客戶端返回:
userdeMBP:go-learning user$ go run test.go connecting to ws://localhost:8080/echo recv: 2019-02-27 17:29:48.742574 +0800 CST m=+1.004759822 recv: 2019-02-27 17:29:49.740974 +0800 CST m=+2.003161426 recv: 2019-02-27 17:29:50.744928 +0800 CST m=+3.007116994
服務端返回:
userdeMacBook-Pro:go-learning user$ go run test1.go recv: 2019-02-27 17:29:48.742574 +0800 CST m=+1.004759822 recv: 2019-02-27 17:29:49.740974 +0800 CST m=+2.003161426 recv: 2019-02-27 17:29:50.744928 +0800 CST m=+3.007116994 recv: 2019-02-27 17:29:51.745137 +0800 CST m=+4.007328841 ...
這個代碼之后有時間並且弄完go websocket包——gorilla/websocket學習-沒弄-最好學學后再仔細看看!!!!!!
3)REST
RESTful是一種互聯網軟件架構:
- 每一個URI代表一種資源
- 客戶端和服務器之間,傳遞這種資源的某種表現層
- 客戶端通過四個HTTP動詞,對服務器資源進行操作,實現“表現層狀態轉化”。GET用來獲取資源,POST用來新建資源(也可以用來更新資源),PUT用來更新資源,DELETE用來刪除資源
web應用要滿足REST的原則:
最重要的是客戶端和服務器之間的交互在請求之間是無狀態的,即從客戶端到服務器的每個請求都必須包含理解請求所必需的信息。如果服務器在請求之間的任何時間點重啟,客戶端不會得到通知。此外此請求可以由任何可用服務器回答,這十分適合雲計算之類的環境。因為是無狀態的,所以客戶端可以緩存數據以改進性能
另一個是系統分層,即組件無法了解除了與它直接交互的層次之外的組建,用於限制整個系統的復雜性,從而促進底層的獨立性
下面的代碼使用了庫:
go get github.com/drone/routes
舉例:
服務端:
package main import ( "fmt" "net/http" "github.com/drone/routes" ) func getUser(w http.ResponseWriter, r *http.Request){ params := r.URL.Query() uid := params.Get(":uid") fmt.Fprintf(w, "you are get user %s\n", uid) } func modifyUser(w http.ResponseWriter, r *http.Request){ params := r.URL.Query() uid := params.Get(":uid") fmt.Fprintf(w, "you are modify user %s\n", uid) } func deleteUser(w http.ResponseWriter, r *http.Request){ params := r.URL.Query() uid := params.Get(":uid") fmt.Fprintf(w, "you are delete user %s\n", uid) } func addUser(w http.ResponseWriter, r *http.Request){ params := r.URL.Query() uid := params.Get(":uid") fmt.Fprintf(w, "you are add user %s\n", uid) } func main() { mux := routes.New() //同樣的URI,當時使用的是不同的方法進行訪問則調用的是不同的函數 mux.Get("/user/:uid", getUser) mux.Post("/user/:uid", modifyUser) mux.Del("/user/:uid", deleteUser) mux.Put("/user/", addUser) http.Handle("/", mux) http.ListenAndServe(":8088", nil) }
使用瀏覽器訪問默認使用的是GET方法:
然后是使用代碼訪問,客戶端:
package main import ( "fmt" "net/http" "io/ioutil" ) func main() { req, err := http.NewRequest("GET","http://127.0.0.1:8088/user/:uid", nil) if err != nil { fmt.Println(err) } resp, err := http.DefaultClient.Do(req) if err != nil { fmt.Println(err) } defer resp.Body.Close() b, _ := ioutil.ReadAll(resp.Body) fmt.Printf("info : %q\n", b) }
首先使用的是GET方法,然后使用POST、DELETE,返回:
userdeMBP:go-learning user$ go run test.go info : "you are get user :uid\n" userdeMBP:go-learning user$ go run test.go info : "you are modify user :uid\n" userdeMBP:go-learning user$ go run test.go info : "you are delete user :uid\n"
然后調用PUT方式是:
req, err := http.NewRequest("PUT","http://127.0.0.1:8088/user/", nil)
返回:
userdeMBP:go-learning user$ go run test.go info : "you are add user \n"
根據上面的代碼可知,其實REST就是根據不同的method訪問同一個資源的時候實現不同的邏輯處理!!!
4)RPC
上面的socket和HTTP采用的是類似“信息交換”模式,即客戶端發送一條信息到服務端,然后服務器端都會返回一定的信息以表示響應。客戶端和服務端之間約定了交互信息的格式,以便雙方都能夠解析交互所產生的信息。但是很多獨立的應用並沒有采用這種模式,而是采用常規的函數調用的方式來完成想要的功能。
RPC(Remote Procedure Call Protocol)就是想實現函數調用模式的網絡化,它是一種通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議。
客戶端就像調用本地函數一樣,然后客戶端把這些參數打包之后通過網絡傳給服務端,服務端解包到處理過程中執行,然后執行結果返回給客戶端
運行時一次客戶機對服務器的RPC調用步驟有:
- 調用客戶端句柄,執行傳送參數
- 調用本地系統內核發送網絡信息
- 消息傳送到遠程主機
- 服務器句柄得到消息並取得參數
- 執行遠程過程
- 執行的過程將結果返回服務器句柄
- 服務器句柄返回結果,調用遠程系統內核
- 消息傳回本地主機
- 客戶句柄由內核接收消息
- 客戶接收句柄返回的數據
Go標准包中已經提供了對RPC的支持,支持三個級別的RPC:TCP、HTTP、JSONRPC,下面將一一說明
Go的RPC包與傳統的RPC系統不同,他只支持Go開發的服務器與客戶端之間的交互,因為在內部,它們采用了Gob來編碼
Go RPC的函數要滿足下面的條件才能夠被遠程調用,不然會被忽略:
- 函數必須是導出的,即首字母為大寫
- 必須有兩個導出類型的參數
- 第一個參數是接收的參數,第二個參數是返回給客戶端的參數,第二個參數必須是指針類型的
- 函數還要有一個error類型返回值。方法的返回值,如果非nil,將被作為字符串回傳,在客戶端看來就和errors.New創建的一樣。如果返回了錯誤,回復的參數將不會被發送給客戶端。
舉個例子,正確的RPC函數格式為:
func (t *T) MethidName(argType T1, replyType *T2) error
T、T1和T2類型都必須能被encoding/gob包編解碼
任何RPC都需要通過網絡來傳遞數據,Go RPC可以利用HTTP和TCP來傳遞數據
1》HTTP RPC
利用HTTP的好處是可以直接復用net/http中的一些函數,下面舉例說明:
服務端:
package main import ( "fmt" "net/http" "net/rpc" "errors" ) type Args struct{ A, B int } type Quotient struct{ Quo, Rem int } type Arith int func (t *Arith) Multiply(args *Args, reply *int) error{ *reply = args.A * args.B return nil } func (t *Arith) Divide(args *Args, quo *Quotient) error{ if args.B == 0{ return errors.New("divide by zero") } quo.Quo = args.A / args.B quo.Rem = args.A % args.B return nil } func main() { arith := new(Arith) rpc.Register(arith) rpc.HandleHTTP() err := http.ListenAndServe(":1234", nil) if err != nil{ fmt.Println(err.Error()) } }
客戶端:
package main import ( "fmt" "net/rpc" "log" "os" ) type Args struct{ A, B int } type Quotient struct{ Quo, Rem int } func main() { if len(os.Args) != 2{ fmt.Println("Usage: ", os.Args[0], "server") os.Exit(1) } serverAddress := os.Args[1] client, err := rpc.DialHTTP("tcp", serverAddress + ":1234") if err != nil{ log.Fatal("dialing : ", err) } //Synchronous call args := Args{17, 8} var reply int err = client.Call("Arith.Multiply", args, &reply) if err != nil{ log.Fatal("arith error : ", err) } fmt.Printf("Arith: %d*%d = %d \n", args.A, args.B, reply) var quot Quotient err = client.Call("Arith.Divide", args, ") if err != nil{ log.Fatal("arith error : ", err) } fmt.Printf("Arith: %d/%d = %d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) }
客戶端返回:
userdeMBP:go-learning user$ go run test.go Usage: /var/folders/2_/g5wrlg3x75zbzyqvsd5f093r0000gn/T/go-build438875911/b001/exe/test server exit status 1 userdeMBP:go-learning user$ go run test.go 127.0.0.1 Arith: 17*8 = 136 Arith: 17/8 = 2 remainder 1
2》TCP RPC
服務端:
package main import ( "fmt" "net" "net/rpc" "errors" "os" ) type Args struct{ A, B int } type Quotient struct{ Quo, Rem int } type Arith int func (t *Arith) Multiply(args *Args, reply *int) error{ *reply = args.A * args.B return nil } func (t *Arith) Divide(args *Args, quo *Quotient) error{ if args.B == 0{ return errors.New("divide by zero") } quo.Quo = args.A / args.B quo.Rem = args.A % args.B return nil } func main() { arith := new(Arith) rpc.Register(arith) tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234") if err != nil{ fmt.Println(err.Error()) os.Exit(1) } listener, err := net.ListenTCP("tcp", tcpAddr) if err != nil{ fmt.Println(err.Error()) os.Exit(1) } for{ conn, err := listener.Accept() if err != nil{ continue } rpc.ServeConn(conn) } }
客戶端:
package main import ( "fmt" "net/rpc" "log" "os" ) type Args struct{ A, B int } type Quotient struct{ Quo, Rem int } func main() { if len(os.Args) != 2{ fmt.Println("Usage: ", os.Args[0], "server:port") os.Exit(1) } service := os.Args[1] client, err := rpc.Dial("tcp", service) if err != nil{ log.Fatal("dialing : ", err) } //Synchronous call args := Args{17, 8} var reply int err = client.Call("Arith.Multiply", args, &reply) if err != nil{ log.Fatal("arith error : ", err) } fmt.Printf("Arith: %d*%d = %d \n", args.A, args.B, reply) var quot Quotient err = client.Call("Arith.Divide", args, ") if err != nil{ log.Fatal("arith error : ", err) } fmt.Printf("Arith: %d/%d = %d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) }
客戶端返回:
userdeMBP:go-learning user$ go run test.go 127.0.0.1 2019/02/28 11:16:37 dialing : dial tcp: address 127.0.0.1: missing port in address exit status 1 userdeMBP:go-learning user$ go run test.go 127.0.0.1:1234 Arith: 17*8 = 136 Arith: 17/8 = 2 remainder 1
其代碼與HTTP的代碼的區別就是一個是DialHTTP,一個是Dial(tcp)
3》JSON RPC
服務端:
package main import ( "fmt" "net" "net/rpc" "net/rpc/jsonrpc" "errors" "os" ) type Args struct{ A, B int } type Quotient struct{ Quo, Rem int } type Arith int func (t *Arith) Multiply(args *Args, reply *int) error{ *reply = args.A * args.B return nil } func (t *Arith) Divide(args *Args, quo *Quotient) error{ if args.B == 0{ return errors.New("divide by zero") } quo.Quo = args.A / args.B quo.Rem = args.A % args.B return nil } func main() { arith := new(Arith) rpc.Register(arith) tcpAddr, err := net.ResolveTCPAddr("tcp", ":1234")//jsonrpc是基於TCP協議的,現在他還不支持http協議 if err != nil{ fmt.Println(err.Error()) os.Exit(1) } listener, err := net.ListenTCP("tcp", tcpAddr) if err != nil{ fmt.Println(err.Error()) os.Exit(1) } for{ conn, err := listener.Accept() if err != nil{ continue } jsonrpc.ServeConn(conn) } }
客戶端:
package main import ( "fmt" "net/rpc/jsonrpc" "log" "os" ) type Args struct{ A, B int } type Quotient struct{ Quo, Rem int } func main() { if len(os.Args) != 2{ fmt.Println("Usage: ", os.Args[0], "server:port") os.Exit(1) } service := os.Args[1] client, err := jsonrpc.Dial("tcp", service) if err != nil{ log.Fatal("dialing : ", err) } //Synchronous call args := Args{17, 8} var reply int err = client.Call("Arith.Multiply", args, &reply) if err != nil{ log.Fatal("arith error : ", err) } fmt.Printf("Arith: %d*%d = %d \n", args.A, args.B, reply) var quot Quotient err = client.Call("Arith.Divide", args, ") if err != nil{ log.Fatal("arith error : ", err) } fmt.Printf("Arith: %d/%d = %d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem) }
客戶端返回:
userdeMBP:go-learning user$ go run test.go 127.0.0.1:1234 Arith: 17*8 = 136 Arith: 17/8 = 2 remainder 1
這一節介紹了四種主要的網絡應用開發方式:socket編程(雲計算方向)、WebSocket(html5,服務器可以主動地push消息,以簡便以前Ajax輪詢的模式)、REST(開發網絡應用API)、RPC
8.安全與加密
1)預防CSRF攻擊
CSRF(跨站請求偽造),攻擊者通過在授權用戶訪問的頁面中包含鏈接或者腳本的方式工作。
要完成一次CSRF攻擊,受害者必需滿足下面餓兩個條件:
- 登錄受信任網站,並在本地生成cookie
- 在不退出A的情況下,訪問危險網站B
比如當用戶登錄網絡銀行去查看其存款余額並沒有退出時,這時候他的QQ好友發來了一個鏈接,這個鏈接可能是攻擊者編寫了一個在網絡銀行進行取款的form提交的鏈接,這樣當用戶點擊該鏈接后,攻擊者就能夠得到其網絡銀行的cookie和該form提交一起實現將用戶賬戶中的資金轉移到攻擊者的賬戶中
因此預防的思想主要從兩方面入手:
- 正確使用GET、POST和Cookie
- 在非GET請求中增加偽隨機數
其實主要是在第二步,就像我們之前表單小節“如何防止表單多次遞交”中說到的添加偽隨機數的操作
因為攻擊者編寫的虛假表單生成的偽隨機數是不合法的,這樣即使他通過XSS獲得了用戶的cookie,他發送的表單也不會被接受
2)確保輸入過濾
這部分與表單小節中的“對表單的輸入進行驗證”內容是很類似的
過濾數據分成三步:
- 識別數據,搞清楚需要過濾的數據來自哪里
- 過濾數據,弄明白我們需要什么樣的數據。可以使用strconv、string、regexp包來過濾數據
- 區分已過濾數據及被污染數據,如果存在攻擊數據,就保證過濾后可以讓我們使用更安全的數據
3)XSS攻擊
- 防御方式:
過濾特殊字符。可以使用HTML的過濾函數,如text/template包下面的HTMLEscapeString、JSEscapeString等函數
- 使用HTTP頭指定類型
w.Header().Set("Content-Type", "text/javascript")
這樣就可以讓瀏覽器解析javascript代碼,而不會是html輸出
4)避免SQL注入
防治SQL注入方法:
- 嚴格限制Web應用的數據庫的操作權限,給此用戶提供僅僅能夠滿足其工作的最低權限,從而最大限度減少注入攻擊對數據庫的危害
- 檢查輸入的數據是否具有所期望的數據格式,嚴格限制變量的類型,例如使用regexp包進行一些匹配處理,或者使用strconv包對字符串轉化成其他基本類型的數據進行判斷
- 對進入數據庫的特殊字符(‘“\<&*;等)進行轉義處理,或編碼轉換。Go的text/template包里面的HTMLEscapeString函數可以對字符串進行轉義處理
- 所有的查詢語句建議使用數據庫提供的參數化查詢接口,參數化的語句使用參數而不是將用戶輸入變量嵌入到SQL語句中,即不要直接拼接SQL語句。例如使用database/sql里面的查詢函數Prepare和Query,或者Exec(query string,args ...interface{})
- 在應用開發之前建議使用專業的SQL注入檢測工具進行檢測,以及時修補被發現的SQL注入漏洞。如sqlmap、SQLninja等
- 避免網站打印出SQL錯誤信息,比如類型錯誤,字段不匹配等,把代碼里的SQL語句暴露出來,以防止攻擊者利用這些錯誤信息進行SQL注入
5)存儲密碼
1》普通方案
現在用得最多的密碼存儲方案是將明文密碼做單向哈希后再存儲。常用的有SHA-256、SHA-1、MD5
例子可見本博客
2》進階方案
因為攻擊者能夠用rainbow table來解析哈希后的密碼,因此進行“加鹽”——salt來進階
即先將用戶輸入的密碼進行一次MD5等算法加密;然后將得到的MD5值前后加上一些只有管理員自己知道的隨機串(salt),在進行一次MD5機密
這個隨機串(salt)可以包含某些固定的串,也可以包含用戶名
3》專家方案——scrypt方案
因為並行計算能力的提升,使用多個rainbow table來攻擊變得可行
新方案即:通過故意增加密碼計算所需耗費的資源和時間,使得任何人都不可獲得足夠的資源建立所需的rainbow table
該方案的特點為:算法中有個因子,用於指明計算密碼摘要所需要的資源和時間,也就是計算強度。計算強度越大,攻擊者建立rainbow table越困難,以至於不可繼續
scrypt方案
http://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt
dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)
通過上面的方法可以獲得唯一的相應的密碼值,這是目前最難破解的
可見本博客的keystore密鑰文件使用的算法-PBKDF2WithHmacSHA1 和Scrypt
6)加密和解密數據
1》base64加解密
舉例:
package main import( "fmt" "encoding/base64" ) func main() { msg := "Hello,world" //11個字符應該裝成15個base64編碼的字符 encoded := base64.StdEncoding.EncodeToString([]byte(msg)) fmt.Println(encoded) //SGVsbG8sd29ybGQ=,后面的=是作填充用的 decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { fmt.Println("decode error:", err) return } fmt.Println(string(decoded))//Hello,world }
2》高級加解密
使用的是下面的兩個標准包:
- crypto/aes包:又稱Rijndael加密法
- crypto/des包:對稱加密算法(老了,不建議使用)
詳情可見:
7) 國際化和本地化
國際化相關的包https://github.com/astaxie/go-i18n
8)錯誤處理,調試和測試
1》錯誤處理
Go中定義了一個error的類型,來顯式表達錯誤。在使用時,通過把返回的error變量與nil的比較,來判定操作是否成功。如果出現錯誤,就會使用log.Fatal(err)來輸出錯誤信息
if err != nil { log.Fatal(err) }
error類型
error類型是一個內置接口類型,定義為:
type error struct{ Error() string }
我們在很多內部包中使用到的error是errors包下面的私有結構errorString:
//errors包中實現了error接口的結構體errorString type errorString struct{ s string } func (e *errorString) Error() string{ return e.s } //當調用errors.New()函數是其實就是返回了一個實現了error接口的errorString對象
//在這里及時返回的是自定義的error類型,返回值也應該設置為error類型 func New(text string) error{ return &errorString{text} }
舉例:
package main import( "errors" "fmt" ) func main() { const name, id = "coco", 17 err := errors.New(fmt.Sprintf("user %q (id %d )not found", name, id)) if err != nil{ fmt.Println(err) } }
詳情可見go標准庫的學習-errors
自定義Error
舉例:
package main import ( "fmt" ) //當除數為0時報錯 type divdError struct{ err string //錯誤描述 dividend float64 //被除數的值 } // 雖然這里只返回了err string的信息,但是可以使用類型斷言來獲得divdError的dividend func (e *divdError) Error() string{ return fmt.Sprintf("divdError is %s", e.err) } //要注意,函數返回自定義錯誤時,返回值也應該設置為error,而非自定義類型,也不應該預聲明自定義錯誤類型的變量 //如果返回值設置為*divdError,則可能導致上層調用者err != nil的判斷永遠為true //預聲明即var err *divdError func divide(dividend, divisor float64) (float64, error) { if divisor == 0{ return 0, &divdError{"divisor is 0 ", dividend} } return dividend/divisor, nil } func main() { divisor := 0.0 result, err := divide(25.0, divisor) if err != nil{ if err, ok := err.(*divdError);ok{ //使用類型斷言來獲得divdError的dividend fmt.Printf("dividend is %0.2f, divdError is %s\n", err.dividend, err.err) return } fmt.Println(err) } fmt.Printf("divide result is %0.2f\n", result) }
錯誤處理:
1.錯誤代碼的冗余——通過檢測函數來減少類似的代碼
func init(){ http.HandleFunc("/view", viewRecord) } func viewRecord(w http.ResponseWriter, r *http.Request){ ... if err := datastore.Get(c, key, record); err != nil{ //獲取數據 http.Error(w, err.Error(), 500) } if err := viewTemplate.Execute(w, record); err != nil{//模版展示 http.Error(w, err.Error(), 500) } }
上面的代碼在獲取數據和模版展示調用時都有檢測錯誤,調用了統一的處理函數http.Error,返回給客戶端500錯誤碼,並顯示相應的錯誤信息。但是當越來越多的HandleFunc加入后,這樣的錯誤處理邏輯代碼就會開始變得冗余
更改后代碼,通過自定義路由器來縮減代碼:
func init(){ http.HandleFunc("/view", appHandler(viewRecord)) //將viewRecord類型轉換為appHandler } type appHandler func(w http.ResponseWriter, r *http.Request) error //當訪問/view路徑時,就會自動調用appHandler的ServeHTTP函數,這樣就會對返回的error進行錯誤處理了 func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){ if err := fn(w, r); err != nil{ http.Error(w, err.Error(), 500) } } func viewRecord(w http.ResponseWriter, r *http.Request) error{ ... if err := datastore.Get(c, key, record); err != nil{ return err } return viewTemplate.Execute(w, record) }
2》使用GDB調試
Go只要修改不需要編譯就可以直接輸出,可以動態在運行環境下打印數據
Go可以通過Println之類的打印數據來調試,但是每次都需要重新編譯,這是一件十分麻煩的事情,因此在這里使用GDB來調試程序
GDB是FSF(自由軟件基金會)發布的一個強大的類Unix系統下的調試工具,可以做如下事情:
- 啟動程序,可以按照開發者自定義的自定義要求運行程序
- 可讓被調試的程序在開發者設定的調置的斷點處停住(斷點可以是條件表達式)
- 當程序被停住時,可以檢查此時程序中所發生的事
- 動態的改變當前程序的執行環境
編譯Go程序時需要注意如下幾點:
- 傳遞參數-ldflags "-s",忽略debug的打印信息
- 傳遞-gcflags "-N -l"參數,這樣可以忽略Go內部做的一些優化,聚合變量和函數等優化,這樣對GDB調試來說非常困難,所以在編譯的時候加入這兩個參數避免這些優化
下面的內容就省略了,官方文檔可見https://golang.org/doc/gdb
現在更好的調試工具是Delve,詳情可見:Go調試工具—— Delve
3》Go怎么寫測試用例
Go自帶一個輕量級的測試框架testing和自帶的go test 命令來實現單元測試和性能測試
新建一個文件夾gotest,然后創建兩個文件gotest.go和gotest_test.go:
1.gotest.go
package gotest import ( "errors" ) func Division(a, b float64) (float64, error) { if b == 0{ return 0, errors.New("divisor can not be 0") } return a / b, nil }
2.gotest_test.go
單元測試文件的要求有:
- 文件名必須是`_test.go`結尾的,這樣在執行`go test`的時候才會執行到相應的代碼
- 你必須 import `testing`這個包
- 所有的測試用例函數必須是`Test`開頭
- 測試用例會按照源代碼中寫的順序依次執行
- 測試函數`TestXxx()`的參數是`testing.T`,我們可以使用該類型來記錄錯誤或者是測試狀態
- 測試格式:`func TestXxx (t *testing.T)`,`Xxx`部分可以為任意的字母數字的組合,但是首字母不能是小寫字母[a-z],例如`Testintdiv`是錯誤的函數名。
- 函數中通過調用`testing.T`的`Error`, `Errorf`, `FailNow`, `Fatal`, `FatalIf`方法,說明測試不通過,調用`Log`方法用來記錄測試的信息。詳情可見
package gotest import ( "testing" ) func Test_Division_1(t *testing.T) { if i, e := Division(6, 2); i != 3 || e != nil { t.Error("除法函數測試沒通過") } else { t.Log("第一個測試通過") } } func Test_Division_2(t *testing.T) { t.Error("就是不通過") }
然后進入gotest文件夾,運行go test :
userdeMBP:gotest user$ go test --- FAIL: Test_Division_2 (0.00s) gotest_test.go:16: 就是不通過 FAIL exit status 1 FAIL _/Users/user/go-learning/gotest 0.006s
顯示測試沒有通過,這是因為在第二個測試函數中我們寫死了測試不通過的代碼t.Error
如果要看見詳細的測試結果,使用標簽-v:
userdeMBP:gotest user$ go test -v === RUN Test_Division_1 --- PASS: Test_Division_1 (0.00s) gotest_test.go:11: 第一個測試通過 === RUN Test_Division_2 --- FAIL: Test_Division_2 (0.00s) gotest_test.go:16: 就是不通過 FAIL exit status 1 FAIL _/Users/user/go-learning/gotest 0.005s
3.壓力測試
文件原則:
- 壓力測試用例必須遵循如下格式,其中 XXX 可以是任意字母數字的組合,但是首字母不能是小寫字母,func BenchmarkXXX(b *testing.B) { ... }
- go test 不會默認執行壓力測試的函數,如果要執行壓力測試需要帶上參數-test.bench,語法:-test.bench="test_name_regex",例如 go test -test.bench=".*"表示測試全部的壓力測試函數
- 在壓力測試用例中,請記得在循環體內使用 testing.B.N,以使測試可以正常的運行
- 文件名也必須以_test.go 結尾
package gotest import ( "testing" ) func Benchmark_Division(b *testing.B) { for i := 0; i < b.N; i++ { //為了循環使用b.N Division(4, 5) } } func Benchmark_TimeConsumingFunction(b *testing.B) { b.StopTimer() //調用該函數停止壓力測試的時間計數 //做一些初始化的工作,例如讀取文件數據,數據庫連接之類的 //這樣這些時間不影響我們測試函數本身的性能 b.StartTimer() //重新開始時間 for i := 0; i < b.N; i++ { Division(4, 5) } }
出錯:
userdeMBP:gotest user$ go test -v -bench=. -benchtime=5s webbench_test.go can't load package: package main: read /Users/user/go-learning/gotest/webbench_test.go: unexpected NUL in inpu
是編碼格式問題,使用sublime將其改成UTF-8編碼即可,在工具欄file——save with Encoding——UTF-8即可
出錯:
userdeMBP:gotest user$ go test -bench="." webbench_test.go # command-line-arguments [command-line-arguments.test] ./webbench_test.go:9:9: undefined: Division ./webbench_test.go:20:9: undefined: Division FAIL command-line-arguments [build failed]
這是因為webbench_test.go 調用了gotest.go中的Division函數,所以你要把有依賴的函數文件都放一起編譯執行才行:
userdeMBP:gotest user$ go test -v ./webbench_test.go ./gotest.go -bench=".*" goos: darwin goarch: amd64 Benchmark_Division-8 2000000000 0.31 ns/op Benchmark_TimeConsumingFunction-8 2000000000 0.61 ns/op PASS ok command-line-arguments 1.939s
上面的結果可以顯示只執行了壓力測試函數,第一條顯示Benchmark_Division執行了2000000000次,每次執行的平均時間是0.31納秒;第二條顯示了Benchmark_TimeConsumingFunction執行了2000000000次,每次執行的平均時間是0.61納秒;最后一條顯示總共的執行時間
9)部署和維護
1》應用日志
Go中提供了一個簡易的log包,使用它可以方便地實現日志記錄的功能。這些日志都是基於fmt包的打印再結合Panic之類的函數來進行一般的打印、拋出錯誤處理
Go目前的標准包只實現了簡單的功能,如果想要把應用日志保存到文件,然后又能夠結合日志實現很多復雜的功能,可以使用第三方開發的一個日志系統:http://github.com/cihub/seelog,其實現了很強大的日志功能。
seelog是用Go語言實現的一個日志系統,它提供了一些簡單的函數來實現復雜的日志分配、過濾和格式化。主要有如下特性:
- XML的動態配置,可以不用重新編譯程序而動態的加載配置信息
- 支持熱更新,能夠動態改變配置而不需要重啟應用
- 支持多輸出流,能夠同時把日志輸出到多種流中,如文件流、網絡流等
- 支持不同的日志輸出
- 命令行輸出
- 文件輸出
- 緩存輸出
- 支持log rotate
- SMTP郵件
更多詳情可見:
go第三方日志系統-seelog-Basic sections
當日志寫完后,我們想要查找日志中某個特定的日志信息,我們可以使用Linux中的grep等語句,如:
cat /data/logs/roll.log | grep "failed login"
這樣就能夠查看到日志中與"failed login"內容相關的日志信息了
通過這種方式我們能夠很方便地查找相應的信息,這樣有利於我們針對應用日志做一些統計和分析。
另外還需要考慮的就是日志的大小,對於一個高流量的Web應用來說,日志的增長是十分可怕的,所以在seelog的配置文件中設置了logrorate,即<rollingfile>,這樣就能夠保證日志文件不會因為不斷變大而導致我們的磁盤空間不夠引起問題
2》網站錯誤處理
web應用中可能出現的錯誤you:
-
數據庫錯誤:指與訪問數據庫服務器或數據相關的錯誤。例如,以下可能出現的一些數據庫錯誤。
- 連接錯誤:這一類錯誤可能是數據庫服務器網絡斷開、用戶名密碼不正確、或者數據庫不存在。
- 查詢錯誤:使用的SQL非法導致錯誤,這樣子SQL錯誤如果程序經過嚴格的測試應該可以避免。
- 數據錯誤:數據庫中的約束沖突,例如一個唯一字段中插入一條重復主鍵的值就會報錯,但是如果你的應用程序在上線之前經過了嚴格的測試也是可以避免這類問題。
-
應用運行時錯誤:這類錯誤范圍很廣,涵蓋了代碼中出現的幾乎所有錯誤。可能的應用錯誤的情況如下:
- 文件系統和權限:應用讀取不存在的文件,或者讀取沒有權限的文件、或者寫入一個不允許寫入的文件,這些都會導致一個錯誤。應用讀取的文件如果格式不正確也會報錯,例如配置文件應該是ini的配置格式,而設置成了json格式就會報錯。
- 第三方應用:如果我們的應用程序耦合了其他第三方接口程序,例如應用程序發表文章之后自動調用接發微博的接口,所以這個接口必須正常運行才能完成我們發表一篇文章的功能。
-
HTTP錯誤:這些錯誤是根據用戶的請求出現的錯誤,最常見的就是404錯誤。雖然可能會出現很多不同的錯誤,但其中比較常見的錯誤還有401未授權錯誤(需要認證才能訪問的資源)、403禁止錯誤(不允許用戶訪問的資源)和503錯誤(程序內部出錯)。
-
操作系統出錯:這類錯誤都是由於應用程序上的操作系統出現錯誤引起的,主要有操作系統的資源被分配完了,導致死機,還有操作系統的磁盤滿了,導致無法寫入,這樣就會引起很多錯誤。
-
網絡出錯:指兩方面的錯誤,一方面是用戶請求應用程序的時候出現網絡斷開,這樣就導致連接中斷,這種錯誤不會造成應用程序的崩潰,但是會影響用戶訪問的效果;另一方面是應用程序讀取其他網絡上的數據,其他網絡斷開會導致讀取失敗,這種需要對應用程序做有效的測試,能夠避免這類問題出現的情況下程序崩潰。
錯誤處理的目標
在實現錯誤處理之前,我們必須明確錯誤處理想要達到的目標是什么,錯誤處理系統應該完成以下工作:
- 通知訪問用戶出現錯誤了:不論出現的是一個系統錯誤還是用戶錯誤,用戶都應當知道Web應用出了問題,用戶的這次請求無法正確的完成了。例如,對於用戶的錯誤請求,我們顯示一個統一的錯誤頁面(404.html)。出現系統錯誤時,我們通過自定義的錯誤頁面顯示系統暫時不可用之類的錯誤頁面(error.html)。
- 記錄錯誤:系統出現錯誤,一般就是我們調用函數的時候返回err不為nil的情況,可以使用前面小節介紹的日志系統記錄到日志文件。如果是一些致命錯誤,則通過郵件通知系統管理員。一般404之類的錯誤不需要發送郵件,只需要記錄到日志系統。
- 回滾當前的請求操作:如果一個用戶請求過程中出現了一個服務器錯誤,那么已完成的操作需要回滾。下面來看一個例子:一個系統將用戶遞交的表單保存到數據庫,並將這個數據遞交到一個第三方服務器,但是第三方服務器掛了,這就導致一個錯誤,那么先前存儲到數據庫的表單數據應該刪除(應告知無效),而且應該通知用戶系統出現錯誤了。
- 保證現有程序可運行可服務:我們知道沒有人能保證程序一定能夠一直正常的運行着,萬一哪一天程序崩潰了,那么我們就需要記錄錯誤,然后立刻讓程序重新運行起來,讓程序繼續提供服務,然后再通知系統管理員,通過日志等找出問題。
如何處理異常
我們知道在很多其他語言中有try...catch關鍵詞,用來捕獲異常情況,但是其實很多錯誤都是可以預期發生的,而不需要異常處理,應該當做錯誤來處理,這也是為什么Go語言采用了函數返回錯誤的設計,這些函數不會panic,例如如果一個文件找不到,os.Open返回一個錯誤,它不會panic;如果你向一個中斷的網絡連接寫數據,net.Conn系列類型的Write函數返回一個錯誤,它們不會panic。這些狀態在這樣的程序里都是可以預期的。你知道這些操作可能會失敗,因為設計者已經用返回錯誤清楚地表明了這一點。這就是上面所講的可以預期發生的錯誤。
但是還有一種情況,有一些操作幾乎不可能失敗,而且在一些特定的情況下也沒有辦法返回錯誤,也無法繼續執行,這樣情況就應該panic。舉個例子:如果一個程序計算x[j],但是j越界了,這部分代碼就會導致panic,像這樣的一個不可預期嚴重錯誤就會引起panic,在默認情況下它會殺掉進程,它允許一個正在運行這部分代碼的goroutine從發生錯誤的panic中恢復運行,發生panic之后,這部分代碼后面的函數和代碼都不會繼續執行,這是Go特意這樣設計的,因為要區別於錯誤和異常,panic其實就是異常處理。如下代碼,我們期望通過uid來獲取User中的username信息,但是如果uid越界了就會拋出異常,這個時候如果我們沒有recover機制,進程就會被殺死,從而導致程序不可服務。因此為了程序的健壯性,在一些地方需要建立recover機制。
func GetUser(uid int) (username string) { defer func() { if x := recover(); x != nil { username = "" } }() username = User[uid] return }
上面介紹了錯誤和異常的區別,那么我們在開發程序的時候如何來設計呢?規則很簡單:如果你定義的函數有可能失敗,它就應該返回一個錯誤。當我調用其他package的函數時,如果這個函數實現的很好,我不需要擔心它會panic,除非有真正的異常情況發生,即使那樣也不應該是我去處理它。而panic和recover是針對自己開發package里面實現的邏輯,針對一些特殊情況來設計。
3》應用部署——使用daemon守護進程實現程序后台持續運行
建議使用Supervisord
Supervisord是用Python實現的一款非常實用的進程管理工具。supervisord會幫你把管理的應用程序轉成daemon程序,而且可以方便的通過命令開啟、關閉、重啟等操作,而且它管理的進程一旦崩潰會自動重啟,這樣就可以保證程序執行中斷后的情況下有自我修復的功能。
⚠️我前面在應用中踩過一個坑,就是因為所有的應用程序都是由Supervisord父進程生出來的,那么當你修改了操作系統的文件描述符之后,別忘記重啟Supervisord,光重啟下面的應用程序沒用。當初我就是系統安裝好之后就先裝了Supervisord,然后開始部署程序,修改文件描述符,重啟程序,以為文件描述符已經是100000了,其實Supervisord這個時候還是默認的1024個,導致他管理的進程所有的描述符也是1024.開放之后壓力一上來系統就開始報文件描述符用光了,查了很久才找到這個坑。
學習Supervisord的使用——沒弄好
4》備份和恢復——文件同步工具rsync 沒弄
rsync能夠實現網站的備份,不同系統的文件的同步,如果是windows的話,需要windows版本cwrsync。
MySQL備份
應用數據庫目前還是MySQL為主流,目前MySQL的備份有兩種方式:熱備份和冷備份,熱備份目前主要是采用master/slave方式(master/slave方式的同步目前主要用於數據庫讀寫分離,也可以用於熱備份數據),關於如何配置這方面的資料,大家可以找到很多。冷備份的話就是數據有一定的延遲,但是可以保證該時間段之前的數據完整,例如有些時候可能我們的誤操作引起了數據的丟失,那么master/slave模式是無法找回丟失數據的,但是通過冷備份可以部分恢復數據。
冷備份一般使用shell腳本來實現定時備份數據庫,然后通過上面介紹rsync同步非本地機房的一台服務器。
MySQL恢復
前面介紹MySQL備份分為熱備份和冷備份,熱備份主要的目的是為了能夠實時的恢復,例如應用服務器出現了硬盤故障,那么我們可以通過修改配置文件把數據庫的讀取和寫入改成slave,這樣就可以盡量少時間的中斷服務。
但是有時候我們需要通過冷備份的SQL來進行數據恢復,既然有了數據庫的備份,就可以通過命令導入:
mysql -u username -p databse < backup.sql
可以看到,導出和導入數據庫數據都是相當簡單,不過如果還需要管理權限,或者其他的一些字符集的設置的話,可能會稍微復雜一些,但是這些都是可以通過一些命令來完成的。
redis備份
redis是目前我們使用最多的NoSQL,它的備份也分為兩種:熱備份和冷備份,redis也支持master/slave模式,所以我們的熱備份可以通過這種方式實現,相應的配置大家可以參考官方的文檔配置,相當的簡單。我們這里介紹冷備份的方式:redis其實會定時的把內存里面的緩存數據保存到數據庫文件里面,我們備份只要備份相應的文件就可以,就是利用前面介紹的rsync備份到非本地機房就可以實現。
redis恢復
redis的恢復分為熱備份恢復和冷備份恢復,熱備份恢復的目的和方法同MySQL的恢復一樣,只要修改應用的相應的數據庫連接即可。
但是有時候我們需要根據冷備份來恢復數據,redis的冷備份恢復其實就是只要把保存的數據庫文件copy到redis的工作目錄,然后啟動redis就可以了,redis在啟動的時候會自動加載數據庫文件到內存中,啟動的速度根據數據庫的文件大小來決定。
10)web框架——beego
beego的學習——沒弄
沒學習的部分之后再繼續學習,未完待續