Go語言中http和mysql的實現程式碼
http 程式設計
Go 原生支援http:
import "net/http"
Go 的http服務效能和nginx比較接近:
就是說用Go寫的Web程式上線,程式前面不需要再部署nginx的Web伺服器,這裡省掉的是Web伺服器。如果伺服器上部署了多個Web應用,還是需要反向代理的,一般這也是nginx或apache。
幾行程式碼就可以實現一個web服務:
package main import ( "fmt" "net/http" ) func Hello(w http.ResponseWriter,r *http.Request) { fmt.Println(*r) fmt.Fprintf(w,"Hello World") } func main() { http.HandleFunc("/",Hello) err := http.ListenAndServe("0.0.0.0:8000",nil) if err != nil { fmt.Println("http Listen failed") } }
http client
http 常見的請求方法:
- Get請求
- Post請求
- Put請求
- Delete請求
- Head請求
Get 請求
使用Get請求網站的示例:
package main import ( "fmt" "io/ioutil" "net/http" ) func main() { res,err := http.Get("http://edu.51cto.com") if err != nil { fmt.Println("http get ERRPR:",err) return } data,err := ioutil.ReadAll(res.Body) if err != nil { fmt.Println("get data ERROR:",err) return } fmt.Println(string(data)) }
Head請求
Head請求只返回響應頭。如果只想要獲取一些狀態資訊的話,可以用Head請求。這樣避免返回響應體,響應體的資料是比較多的,適合做監控。Head請求的示例:
package main import ( "fmt" "net/http" ) var urls = []string{ "http://×××w.baidu.com","http://×××w.google.com","http://×××w.sina.com.cn","http://×××w.163.com",} func main() { for _,v := range urls { resp,err := http.Head(v) if err != nil { fmt.Println("Head request ERROR:",err) continue } fmt.Println(resp.Status) } }
http 常見狀態碼
http.StatusContinue = 100
http.StatusOK = 200
http.StatusFound = 302 跳轉
http.StatusBadRequest = 400 非法請求
http.StatusUnanthorized = 401 沒有許可權
http.StatusForbidden = 403 禁止訪問
http.Status.NotFound = 404 頁面不存在
http.StatusInternalServerError = 500 內部錯誤
處理form表單
package main import ( "fmt" "io" "net/http" ) const form = ` <html> <body> <form action="#" method="post" name="bar"> <input type="text" name="in" /> <input type="text" name="in" /> <input type="submit" value="Submit" /> </form> </body> </html>` func FormServer(w http.ResponseWriter,request *http.Request) { w.Header().Set("content-Type","text/html") switch request.Method { case "GET": io.WriteString(w,form) case "POST": request.ParseForm() io.WriteString(w,request.Form["in"][0]) // 注意上面的2個input的name是一樣的 io.WriteString(w,request.Form["in"][1]) // 所以這是一個數組 io.WriteString(w,"</br>") io.WriteString(w,request.FormValue("in")) // 一般去一個值,就用這個方法 } } func main() { http.HandleFunc("/form",FormServer) if err := http.ListenAndServe(":8000",nil); err != nil { fmt.Println("監聽埠ERROR:",err) } }
panic 處理
如果處理函式裡有panic,會導致整個程式崩潰,所以要 defer revoer()
來處理 panic。在處理函式開頭defer一個匿名函式:
func FormServer(w http.ResponseWriter,request *http.Request) { // 增加一個defer來處理panic defer func() { if x := recover(); x != nil { log.Println(request.RemoteAddr,"捕獲到異常:",x) } }() // 原本的處理函式的內容 w.Header().Set("content-Type",request.FormValue("in")) // 一般去一個值,就用這個方法 } // 搞個panic出來 zero := 0 tmp := 1 / zero io.WriteString(w,string(tmp)) }
優化統一處理
按照上面的做法,要在每個處理函式的開頭都加上panic的處理。由於每個處理函式的panic處理方法都一樣,所以可以寫一個自定義的處理函式:
// 自定義的panic處理的函式 func logPanics(handle http.HandlerFunc) http.HandlerFunc { return func(writer http.ResponseWriter,request *http.Request) { defer func() { if x := recover(); x != nil { log.Println(request.RemoteAddr,x) } }() // 上面先處理panic,再接著下面呼叫業務邏輯 handle(writer,request) } } func main() { // http.HandleFunc("/form",FormServer) // 修改呼叫處理函式的方法 http.HandleFunc("/form",logPanics(FormServer)) // 把處理函式傳給自己寫的封裝了panic處理的函式裡 if err := http.ListenAndServe(":8000",err) } }
原本直接呼叫處理函式。現在呼叫自定義的函式,把處理函式傳進去。在自定義的函式裡先載入defer,然後再呼叫執行原本的處理函式。邏輯很簡單,就是把處理函式作為引數傳給自定義的函式,在自定義的函式裡再呼叫處理函式。在自定義的函式裡寫上defer,這樣就相當於所有的處理函式都有defer了。
模板
使用模板需要用到 "text/template" 包。然後呼叫模板的t.Execute()方法輸出。
替換
先準備一個簡單的模板:
<p>Hello {{.Name}}</p> <p>Age: {{.Age}}</p>
然後在Go裡使用模板:
package main import ( "fmt" "os" "text/template" ) type Person struct { Name string Age int } func main() { t,err := template.ParseFiles("index.html") if err != nil { fmt.Println("模板解析異常:",err) return } p := Person{"Bob",32} if err := t.Execute(os.Stdout,p); err != nil { fmt.Println("模板載入資料異常:",err) } } /* 執行結果 PS H:\Go\src\go_dev\day10\http\use_template> go run main.go <p>Hello Bob</p> <p>Age: 32</p> PS H:\Go\src\go_dev\day10\http\use_template> */
如果直接用 {{.}} 不加欄位名的話,就是輸出結構體列印的效果。
輸出到瀏覽器裡
要輸出到瀏覽器裡,只需要在 t.Execute(os.Stdout,p)
裡,把原本輸出到終端換成輸出到處理函式的 w http.ResponseWriter 型別,就好了。
html模板的內容不變,下面是go的程式碼:
package main import ( "fmt" "net/http" "text/template" ) func Hello(w http.ResponseWriter,r *http.Request) { fmt.Fprintf(w,"Hello World") } type Person struct { Name string Age int } func Index(w http.ResponseWriter,r *http.Request) { p := Person{"Cara",18} t,err := template.ParseFiles("index.html") if err != nil { fmt.Println("載入模板ERROR:",err) return } t.Execute(w,p) } func main() { http.HandleFunc("/",Hello) http.HandleFunc("/index",Index) err := http.ListenAndServe("0.0.0.0:8000",nil) if err != nil { fmt.Println("http Listen failed") } }
判斷
用法示例:
<body> {{if gt .Age 18}} <p>已成年</p> {{else}} <p>未成年</p> {{end}} </body>
更多判斷邏輯:
not 非
{{if not .condition}}
{{end}}
and 與
{{if and .condition1 .condition2}}
{{end}}
or 或
{{if or .condition1 .condition2}}
{{end}}
eq 等於
{{if eq .var1 .var2}}
{{end}}
ne 不等於
{{if ne .var1 .var2}}
{{end}}
lt 小於
{{if lt .var1 .var2}}
{{end}}
le 小於等於
{{if le .var1 .var2}}
{{end}}
gt 大於
{{if gt .var1 .var2}}
{{end}}
ge 大於等於
{{if ge .var1 .var2}}
{{end}}
with 封裝
with語句就是建立一個封閉的作用域,在其範圍內,{{.}}代表with的變數,而與外面的{{.}}無關,只與with的引數有關:
<body> {{with .Name}} <p>{{.}}</p> {{end}} </body>
上面這樣包在 {{with .Var}} 裡,with 裡的 {{.}} 代表的就是 Var 這個變數。
with 可以封裝常數:
{{ with "world"}} Now the dot is set to {{ . }} {{ end }}
迴圈(遍歷)
golang的template支援range迴圈來遍歷map、slice內的內容,在range迴圈內,還可以使用$設定迴圈變數,我們可以通過 $i $v 來訪問遍歷的值。語法為:
{{range $i,$v := .slice}} <li>key: {{ $key }},value: {{ $value }}</li> {{end}}
這是另外一種遍歷方式,這種方式無法訪問到index或者key的值,需要通過點來訪問對應的value:
{{range .slice}} {{.field}} {{end}}
在迴圈內,點是代表遍歷的值。原本使用點來訪問的變數,那麼在迴圈內部就要用 $. 來訪問。下面的例子表示迴圈內和迴圈外 ArticleConten 這個變數訪問的方式:
{{.ArticleContent}} {{range .slice}} {{$.ArticleContent}} {{end}}
定義變數
模板的引數可以是go中的基本資料型別,如字串,數字,布林值,陣列切片或者一個結構體。在模板中設定變數可以使用 $variable := value。我們在range迭代的過程使用了設定變數的方式。
{{$article := "hello"}} {{$name := .Name}}
mysql 使用
這裡只簡單講了資料的增刪改查,所以測試程式碼前,需要先把資料庫準備好。
先建立一個數據庫,指定了編碼,這樣應該可以支援中文:
CREATE DATABASE 庫名 CHARSET "utf8";
然後建2張表:
CREATE TABLE person ( user_id int primary key auto_increment,username varchar(260),gender varchar(260),email varchar(260) ); CREATE TABLE place ( country varchar(200),city varchar(200),telcode int );
匯入資料庫驅動
sql 包提供了通用的SQL(或類SQL)資料庫介面。
sql 包必須與資料庫驅動結合使用。
驅動包需要安裝:
go get -u github.com/go-sql-driver/mysql
使用前,先要匯入mysql的包:
import ( "database/sql" _ "github.com/go-sql-driver/mysql" )
上面匯入了2個包。第一個是sql包,就是我們呼叫操作資料庫用的。
第二個是驅動包,這裡前面加了佔位符,所以這個包只是引入,但是不使用它。並且如果要操作別的資料庫的話,只需要修改驅動包就行了。
連線資料庫
構建連線,格式是:”使用者名稱:密碼@tcp(IP:埠)/資料庫?charset=utf8” :
package main import ( "fmt" "time" "database/sql" _ "github.com/go-sql-driver/mysql" ) var DB *sql.DB func init() { database,err := sql.Open("mysql","admin:admin123@tcp(192.168.3.103:3306)/Golang_week10") if err != nil { fmt.Println("連線資料庫失敗:",err) return } DB = database } func main() { fmt.Println(DB) DB.SetMaxIdleConns(16) //設定閒置連線數 DB.SetMaxOpenConns(100) //設定最大連線數 DB.SetConnMaxLifetime(100*time.Second) //最大連線週期,超過時間的連線就close fmt.Println(DB) }
插入資料
下面是插入資料,並且再獲取id的示例:
// 資料庫連線的init函式就省略了 func insert() { r,err := DB.Exec("insert into person(username,gender,email) values(?,?,?)","Barry","Male","[email protected]") if err != nil { fmt.Println("插入資料ERROR:",err) return } fmt.Println(r) id,err := r.LastInsertId() if err != nil { fmt.Println("獲取id ERROR:",err) return } fmt.Println(id) } func main() { insert() }
上面的 values(?,?) 裡的問號,是佔位符,具體的值可以寫在後面的引數裡。當然如果不用佔位符,直接就傳1個字串作為引數也是可以的。
查詢
查詢單個欄位:
func query() { row := DB.QueryRow("select username from person where user_id=?",1) var name string // 建立變數用於存放查詢到的資料 if err := row.Scan(&name); err != nil { fmt.Println("Scan Failed:",err) return } fmt.Println(name) } func main() { query() }
也可以一次查詢多個欄位或所有欄位,查詢之前按照表的型別建立結構體,用查詢到的資料為結構體賦值:
type Person struct { ID int `db:"user_id"` Username sql.NullString `db:"username"` Gender sql.NullString `db:"gender"` Email sql.NullString `db:"email"` } func query() { row := DB.QueryRow("select * from person where user_id=?",6) var person = new(Person) // row.scan中的欄位必須是按照資料庫存入欄位的順序,否則報錯 if err := row.Scan(&person.ID,&person.Username,&person.Gender,&person.Email); err != nil { fmt.Println("Scan Failed:",err) return } fmt.Println(person) } func main() { query() }
資料模型,就是上面定義的結構體。這裡的型別可以是Go的標準資料型別。但是如果資料庫的欄位允許為空,並且該欄位的值也為空,那麼查詢後該欄位會返回nil。如果是string型別,則無法接收nil,但sql.NullString則可以接收nil值。
另外,結構體裡的tag標籤在這裡沒有意義。不過上面的tag標註了該欄位在資料庫裡對應的欄位名,可能在別處會有用。
查詢多行
func query() { rows,err := DB.Query("select * from person where user_id > ?",1) defer func() { if rows != nil { rows.Close() } }() if err != nil { fmt.Println("Query 查詢 ERROR:",err) return } var person = new(Person) for rows.Next() { if err = rows.Scan(&person.ID,&person.Email); err != nil { fmt.Println("Scan Failed:",err) return } fmt.Println(person) } } func main() { query() }
查詢用起來還是不太方法,不過還可以選擇其他第三方庫,那裡會有一些很好的擴充套件。後面會舉例。
其他操作
由於基本都是用SQL的命令進行操作,所以其他操作就不一個一個舉例了
update 更新資料
result,err := DB.Exec("UPDATE person set email=? where username=?","Cara",[email protected])
delete 刪除資料
result,err := DB.Exec("DELETE FROM person where id=?",1)
注意:更新資料不返回LastInsertID,所以result.LastInsertID一直為0。刪除資料可以拿到LastInsertID,用法和插入資料裡一樣。
第三方庫 sqlx
sqlx是一個go語言包,在內建database/sql包之上增加了很多擴充套件,簡化資料庫操作程式碼的書寫。
由於database/sql介面是sqlx的子集,所有database/sql的用法,在sqlx中一樣可以用。不過sqlx還有更多擴充套件,用起來更方便。
安裝:
go get github.com/jmoiron/sqlx
查詢 Select() 方法
Select是一個非常省時的擴充套件。它們把query和非常靈活的scan語法結合起來。Select用來獲取結果切片:
// 這裡的tag標籤就有意義了,下面的Select()方法應該就是根據tag標籤對號入座的 type Person struct { ID int `db:"user_id"` Username sql.NullString `db:"username"` Gender sql.NullString `db:"gender"` Email sql.NullString `db:"email"` } func select() { var persons []Person // 這裡建立的是存放結構體的切片 if err := DB.Select(&person,"select * from person where userid > ?",1); err != nil { fmt.Println("Select ERROR:",err) return } fmt.Println(person) }
Select可以提高編碼效率,還有更多擴充套件。sqlx 號稱 golang 資料庫開發神器,這裡就提一下,等到真正用的時候再去深入學習了。