標籤:遍曆 source 簡單 through drive 執行個體化 重點 入口 初始化
build-web-application-with-golang 學習教程
這幾周學習以上教程,僅記錄一些重點痛點部分。
Go語言Go語言基礎
Go是一門類似C的編譯型語言,但是它的編譯速度非常快。這門語言的關鍵字總共也就二十五個:
break default func interface selectcase defer go map structchan else goto package switchconst fallthrough if range typecontinue for import return var
- var和const參考2.2Go語言基礎裡面的變數和常量申明
- package和import已經有過短暫的接觸
- func 用於定義函數和方法
- return 用於從函數返回
- defer 用於類似解構函式
- go 用於並發
- select 用於選擇不同類型的通訊
- interface 用於定義介面,參考2.6小節
- struct 用於定義抽象資料類型,參考2.5小節
- break、case、continue、for、fallthrough、else、if、switch、goto、default這些參考2.3流程介紹裡面
- chan用於channel通訊
- type用於聲明自訂類型
- map用於聲明map類型資料
- range用於讀取slice、map、channel資料
Go程式是通過package來組織的,package <pkgName>這一行告訴我們當前檔案屬於哪個包,而包名main則告訴我們它是一個可獨立啟動並執行包,它在編譯後會產生可執行檔。除了main包之外,其它的包最後都會產生*.a檔案(也就是包檔案)並放置在$GOPATH/pkg/$GOOS_$GOARCH中(以Mac為例就是$GOPATH/pkg/darwin_amd64)。
每一個可獨立啟動並執行Go程式,必定包含一個package main,在這個main包中必定包含一個入口函數main,而這個函數既沒有參數
,也沒有傳回值。
包的概念和Python中的package類似,它們都有一些特別的好處:模組化(能夠把你的程式分成多個模組)和可重用性(每個模組都能被其它應用程式反覆使用)。
Go語言重點痛點interface
簡單的說,interface是一組method簽名的組合,我們通過interface來定義對象的一組行為。
package mainimport "fmt"type Human struct { name string age int phone string}type Student struct { Human //匿名欄位 school string loan float32}type Employee struct { Human //匿名欄位 company string money float32}//Human實現SayHi方法func (h Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)}//Human實現Sing方法func (h Human) Sing(lyrics string) { fmt.Println("La la la la...", lyrics)}//Employee重載Human的SayHi方法func (e Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) }// Interface Men被Human,Student和Employee實現// 因為這三個類型都實現了這兩個方法type Men interface { SayHi() Sing(lyrics string)}func main() { mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000} //定義Men類型的變數i var i Men //i能儲存Student i = mike fmt.Println("This is Mike, a Student:") i.SayHi() i.Sing("November rain") //i也能儲存Employee i = tom fmt.Println("This is tom, an Employee:") i.SayHi() i.Sing("Born to be wild") //定義了slice Men fmt.Println("Let‘s use a slice of Men and see what happens") x := make([]Men, 3) //這三個都是不同類型的元素,但是他們實現了interface同一個介面 x[0], x[1], x[2] = paul, sam, mike for _, value := range x{ value.SayHi() }}
interface就是一組抽象方法的集合,它必須由其他非interface類型實現,而不能自我實現。
空interface
空interface(interface{})不包含任何的method,正因為如此,所有的類型都實現了空interface。空interface對於描述起不到任何的作用(因為它不包含任何的method),但是空interface在我們需要儲存任意類型的數值的時候相當有用,因為它可以儲存任意類型的數值。它有點類似於C語言的void*類型。
// 定義a為空白介面var a interface{}var i int = 5s := "Hello world"// a可以儲存任意類型的數值a = ia = s
goroutine
goroutine是Go並行設計的核心。goroutine說到底其實就是協程,但是它比線程更小,十幾個goroutine可能體現在底層就是五六個線程,Go語言內部幫你實現了這些goroutine之間的記憶體共用。執行goroutine只需極少的棧記憶體(大概是4~5KB),當然會根據相應的資料伸縮。也正因為如此,可同時運行成千上萬個並發任務。goroutine比thread更易用、更高效、更輕便。
goroutine是通過Go的runtime管理的一個線程管理器。goroutine通過go關鍵字實現了,其實就是一個普通的函數。
package mainimport ( "fmt" "runtime")func say(s string) { for i := 0; i < 5; i++ { runtime.Gosched() fmt.Println(s) }}func main() { go say("world") //開一個新的Goroutines執行 say("hello") //當前Goroutines執行}// 以上程式執行後將輸出:// hello// world// hello// world// hello// world// hello// world// hello
channels
goroutine運行在相同的地址空間,因此訪問共用記憶體必須做好同步。那麼goroutine之間如何進行資料的通訊呢,Go提供了一個很好的通訊機制channel。channel可以與Unix shell 中的雙向管道做類比:可以通過它發送或者接收值。這些值只能是特定的類型:channel類型。定義一個channel時,也需要定義發送到channel的值的類型。注意,必須使用make 建立channel:
package mainimport "fmt"func sum(a []int, c chan int) { total := 0 for _, v := range a { total += v } c <- total // send total to c}func main() { a := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(a[:len(a)/2], c) go sum(a[len(a)/2:], c) x, y := <-c, <-c // receive from c fmt.Println(x, y, x + y)}
Web Service With GolangGo搭建一個Web伺服器
Web是基於http協議的一個服務,Go語言裡面提供了一個完善的net/http包,通過http包可以很方便的就搭建起來一個可以啟動並執行Web服務。同時使用這個包能很簡單地對Web的路由,靜態檔案,模版,cookie等資料進行設定和操作。
package mainimport ( "fmt" "net/http" "strings" "log")func sayhelloName(w http.ResponseWriter, r *http.Request) { r.ParseForm() //解析參數,預設是不會解析的 fmt.Println(r.Form) //這些資訊是輸出到伺服器端的列印資訊 fmt.Println("path", r.URL.Path) fmt.Println("scheme", r.URL.Scheme) fmt.Println(r.Form["url_long"]) for k, v := range r.Form { fmt.Println("key:", k) fmt.Println("val:", strings.Join(v, "")) } fmt.Fprintf(w, "Hello astaxie!") //這個寫入到w的是輸出到用戶端的}func main() { http.HandleFunc("/", sayhelloName) //設定訪問的路由 err := http.ListenAndServe(":9090", nil) //設定監聽的連接埠 if err != nil { log.Fatal("ListenAndServe: ", err) }}
看到上面這個代碼,要編寫一個Web伺服器很簡單,只要調用http包的兩個函數就可以了(類似於Python的tornado)。我們build之後,然後執行web.exe,這個時候其實已經在9090連接埠監聽http連結請求了。
在瀏覽器輸入http://localhost:9090
可以看到瀏覽器頁面輸出了Hello astaxie!
可以換一個地址試試:http://localhost:9090/?url_long=111&url_long=222
看看瀏覽器輸出的是什麼,伺服器輸出的是什嗎?
Go的Web服務底層
http包執行流程
建立Listen Socket, 監聽指定的連接埠, 等待用戶端請求到來。
Listen Socket接受用戶端的請求, 得到Client Socket, 接下來通過Client Socket與用戶端通訊。
處理用戶端的請求, 首先從Client Socket讀取HTTP請求的協議頭, 如果是POST方法, 還可能要讀取用戶端提交的資料, 然後交給相應的handler處理請求, handler處理完畢準備好用戶端需要的資料, 通過Client Socket寫給用戶端。
這整個的過程裡面我們只要瞭解清楚下面三個問題,也就知道Go是如何讓Web運行起來了
- 如何監聽連接埠?
- 如何接收用戶端請求?
- 如何分配handler?
前面小節的代碼裡面我們可以看到,Go是通過一個函數ListenAndServe來處理這些事情的,這個底層其實這樣處理的:初始化一個server對象,然後調用了net.Listen("tcp", addr),也就是底層用TCP協議搭建了一個服務,然後監控我們設定的連接埠。
下面代碼來自Go的http包的源碼,通過下面的代碼我們可以看到整個的http處理過程:
func (srv *Server) Serve(l net.Listener) error { defer l.Close() var tempDelay time.Duration // how long to sleep on accept failure for { rw, e := l.Accept() if e != nil { if ne, ok := e.(net.Error); ok && ne.Temporary() { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } if max := 1 * time.Second; tempDelay > max { tempDelay = max } log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay) time.Sleep(tempDelay) continue } return e } tempDelay = 0 c, err := srv.newConn(rw) if err != nil { continue } go c.serve() }}
監控之後如何接收用戶端的請求呢?上面代碼執行監控連接埠之後,調用了srv.Serve(net.Listener)函數,這個函數就是處理接收用戶端的請求資訊。這個函數裡面起了一個for{},首先通過Listener接收請求,其次建立一個Conn,最後單獨開了一個goroutine,把這個請求的資料當做參數扔給這個conn去服務:go c.serve()。這個就是高並發體現了,使用者的每一次請求都是在一個新的goroutine去服務,相互不影響。
那麼如何具體分配到相應的函數來處理請求呢?conn首先會解析request:c.readRequest(),然後擷取相應的handler:handler := c.server.Handler,也就是我們剛才在調用函數ListenAndServe時候的第二個參數,我們前面例子傳遞的是nil,也就是為空白,那麼預設擷取handler = DefaultServeMux,那麼這個變數用來做什麼的呢?對,這個變數就是一個路由器,它用來匹配url跳轉到其相應的handle函數,那麼這個我們有設定過嗎?有,我們調用的代碼裡面第一句不是調用了http.HandleFunc("/", sayhelloName)嘛。這個作用就是註冊了請求/的路由規則,當請求uri為"/",路由就會轉到函數sayhelloName,DefaultServeMux會調用ServeHTTP方法,這個方法內部其實就是調用sayhelloName本身,最後通過寫入response的資訊反饋到用戶端。
詳細的整個流程如所示:
Go代碼的執行流程
通過對http包的分析之後,現在讓我們來梳理一下整個的代碼執行過程。
首先調用Http.HandleFunc
按順序做了幾件事:
1 調用了DefaultServeMux的HandleFunc
2 調用了DefaultServeMux的Handle
3 往DefaultServeMux的map[string]muxEntry中增加對應的handler和路由規則
其次調用http.ListenAndServe(":9090", nil)
按順序做了幾件事情:
1 執行個體化Server
2 調用Server的ListenAndServe()
3 調用net.Listen("tcp", addr)監聽連接埠
4 啟動一個for迴圈,在迴圈體中Accept請求
5 對每個請求執行個體化一個Conn,並且開啟一個goroutine為這個請求進行服務go c.serve()
6 讀取每個請求的內容w, err := c.readRequest()
7 判斷handler是否為空白,如果沒有設定handler(這個例子就沒有設定handler),handler就設定為DefaultServeMux
8 調用handler的ServeHttp
9 在這個例子中,下面就進入到DefaultServeMux.ServeHttp
10 根據request選擇handler,並且進入到這個handler的ServeHTTP
mux.handler(r).ServeHTTP(w, r)
11 選擇handler:
A 判斷是否有路由能滿足這個request(迴圈遍曆ServeMux的muxEntry)
B 如果有路由滿足,調用這個路由handler的ServeHTTP
C 如果沒有路由滿足,調用NotFoundHandler的ServeHTTP
表單
表單是我們平常編寫Web應用常用的工具,通過表單我們可以方便的讓用戶端和伺服器進行資料的互動。
表單是一個包含表單元素的地區。表單元素是允許使用者在表單中(比如:文本域、下拉式清單、單選框、複選框等等)輸入資訊的元素。表單使用表單標籤(
build-web-application-with-golang學習筆記