<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從0寫一個 Go Web 服務 (上)

          共 16122字,需瀏覽 33分鐘

           ·

          2021-05-20 01:12

          學生時代曾和幾個朋友做了一個筆記本小應用,當時我的角色是pm + dba,最近心血來潮,想把這個玩意自己實現(xiàn)一遍,順便寫一篇文章記錄整個過程。

          筆者的職業(yè)目前是一個后端程序員,最常用的語言是Golang,恰好Golang自帶的的net/http包非常方便,這次就用Golang寫這個服務。首先打開我心愛的GoLand,New一個Project,給項目起一個酷炫的名字。

          接著寫個Hello, world。同時為方便項目管理,引入git這個版本控制工具來管理代碼。隨著一發(fā)git init 加commit,這個項目就算誕生了; )

          作為一個互聯(lián)網公司的程序員,公司追求的是快速試錯,迭代前進,此路不通換一條繼續(xù)試驗。這就需要管理PM和運營老板的預期,現(xiàn)在要從0到1寫一個web服務,就需要詳細拆解一下需求,搞一個TODO list。同時明確項目的milestone,快速迭代,到達stone立馬上線,隨后再慢慢地豐富細節(jié)~

          那么先來搞第一個todo,Go語言自帶的net/http包非常方便,寫web server比較簡單。讓我們先把項目的slogan輸出到瀏覽器上。

          package main

          import (
          "log"
          "net/http"
          )

          func Hello(wr http.ResponseWriter, r *http.Request, _ httprouter.Params) {
          _, err := wr.Write([]byte(`
          __ _ _ ____ ____ __ _ _ ____ __ _ __ ____ ____ ____ __ ____ ____
          / _\/ )( ( __) ___)/ \( \/ | __) ( ( \/ (_ _| __) ___) / _\( _ ( _ \
          / \ /\ /) _)\___ ( O ) \/ \) _) / ( O ))( ) _)\___ \ / \) __/) __/
          \_/\_(_/\_|____|____/\__/\_)(_(____) \_)__)\__/(__)(____|____/ \_/\_(__) (__)

          `))
          if err != nil {
          return
          }
          }

          func main() {
          http.HandleFunc("/", hello)
          err := http.ListenAndServe(":8010", nil)
          if err != nil {
          log.Fatal(err)
          }
          }

          然后啟動項目,隨便打開一個瀏覽器,輸入http://localhost:8000/,可以看到這樣的效果。

          此時有的同學可能會講,測試怎么還能老依賴瀏覽器呢,我電腦上如果沒有瀏覽器還不能測試了嗎?用瀏覽器測試確實不太優(yōu)雅,所以讓我們接著繼續(xù)寫一個測試文件,搞個http Client,自己跑一下效果,并來一發(fā)commit。

          package main

          import (
          "fmt"
          "io/ioutil"
          "net/http"
          "testing"
          "time"
          )

          func Test_hello(t *testing.T) {
          go main()
          time.Sleep(time.Second) // 給main函數(shù)續(xù)一秒,確保main在http.Get之前執(zhí)行
          resp, err := http.Get("http://localhost:8000/")
          if err != nil {
          t.Error("curl failed")
          }
          body, err := ioutil.ReadAll(resp.Body)
          if err != nil {
          t.Error("read body failed")
          }

          // TODO 這里不優(yōu)雅
          fmt.Println(string(body) == `
          __ _ _ ____ ____ __ _ _ ____ __ _ __ ____ ____ ____ __ ____ ____
          / _\/ )( ( __) ___)/ \( \/ | __) ( ( \/ (_ _| __) ___) / _\( _ ( _ \
          / \ /\ /) _)\___ ( O ) \/ \) _) / ( O ))( ) _)\___ \ / \) __/) __/
          \_/\_(_/\_|____|____/\__/\_)(_(____) \_)__)\__/(__)(____|____/ \_/\_(__) (__)

          `)
          }

          接下來,讓我們實現(xiàn)一下這個記事本的CURD功能讓它先稍微可用起來。首先設計一下路由:

          GET	    /note    // 獲取所有note 
          POST    /note    // 新增一個note
          GET     /note/:id    // 根據(jù)id獲取某個note
          DELETE    /note/:id    // 根據(jù)id刪除某個note

          敏銳的同學一下就可以看出來,這里我們用了早些年特別流行的RESTful路由設計。但是這里有個小問題:golang自帶的net/http包里面對牛逼的RESTful支持的并不好。但是問題不大,讓我們去程序員聚集的Github上找找有沒有支持REST的router可以使用。

          果不其然其他程序員有類似的困擾并解決了問題,這個12k星的項目完全可以cover我們的需求。httprouter可以根據(jù)HTTP方法(GET, POST, PUT, PATCH, DELETE等) build出一棵棵的壓縮字典樹(Radix Tree),樹的節(jié)點是URL中的一個path塊或path塊的公共前綴,將樹和 URL對應的handler函數(shù)綁定起來,就可以使用了。

          要引入第三方包,就涉及到包管理問題,golang之前沒有一個官方的包管理工具,一直被人所詬病。不過前些日子官方推出了Go Module這個工具,雖然目前這個工具還有一些問題,但是這是官方出品并強推的,肯定不會死,讓我們通過go module把httprouter引用進來。

          在package中執(zhí)行一下go mod init xxx 命令,此時你會發(fā)現(xiàn),項目的文件里面多了一個go.mod文件。

          接著我們設置幾個變量:

          1. GO111MODULE : 這個變量用來控制是否啟用Go Modules,選auto。

          2. GOPROXY : 由于一些無法抵抗的原因,我們無法訪問golang的proxy地址proxy.golang.org。這里可以通過設置Proxy地址來代替官方的proxy,比如說七牛的goproxy.cn。

          3. GOPATH: 寫go的人大概都知道這個變量,在沒有go module之前,項目中import只能通過GOPATH設置的GOPATH/src 的限定路徑來導入包,我們所有的go代碼都得放到GOPATH之下,有了Go Modules之后,我們可以想放哪里就放哪里了。

          同時,在Goland里也設置一下Go Modules。Go Modules可以使用之前的go get 命令來拉去包,比如這樣:go get -u github.com/julienschmidt/httprouter。
          接著,來更新一下項目的目錄結構,隨著項目的迭代,如果功能函數(shù)都寫到main.go的話非常不利于管理,這里把代碼做一下拆分。首先開一個logic目錄,用來放項目的主要邏輯;接著開一個model目錄,用來存放note模型,接著在main同級下寫一個route目錄,引入httprouter。

          此時項目的布局像這樣:

          $ tree
          .
          ├── go.mod
          ├── go.sum
          ├── logic
          │   ├── note.go
          │   └── note_test.go
          ├── main.go
          ├── main_test.go
          ├── model
          │   └── dto.go
          └── route.go

          route.go文件大概長這樣:

          func initRouter() *httprouter.Router {

          router := httprouter.New()
          router.GET("/", logic.Hello)

          router.GET("/note", logic.GetAll)
          ...
          router.DELETE("/note/:id", logic.Delete)

          return router
          }

          而main文件要把router做一下初始化,并注冊到HTTPHandler中。

          func main() {
          mux := initRouter()
          err := http.ListenAndServe(":8010", mux)
          if err != nil {
          log.Fatal(err)
          }
          }

          note.go中,我們把之前規(guī)劃的curd功能實現(xiàn)一下:

          package logic

          import (
          "crypto/md5"
          "encoding/json"
          "fmt"
          "math/rand"
          "net/http"
          "notes/model"
          "time"

          "github.com/julienschmidt/httprouter"
          )

          func Hello(wr http.ResponseWriter, r *http.Request, _ httprouter.Params) {
          _, err := wr.Write([]byte("This is a awesome note app!"))
          if err != nil {
          return
          }
          }

          var notes = map[string]model.Note{}

          func GetAll(wr http.ResponseWriter, r *http.Request, _ httprouter.Params) {

          data := map[string]interface{}{
          "msg": "success",
          "data": notes,
          }

          res, err := json.Marshal(data)
          if err != nil {
          return
          }

          _, err = wr.Write(res)
          if err != nil {
          return
          }

          }

          func GetOne(wr http.ResponseWriter, r *http.Request, p httprouter.Params) {
          id := p.ByName("id")
          if _, exist := notes[id]; !exist {
          return
          }

          data := map[string]interface{}{
          "msg": "success",
          "data": notes[id],
          }

          res, err := json.Marshal(data)
          if err != nil {
          return
          }

          _, err = wr.Write(res)
          if err != nil {
          return
          }
          }

          func Add(wr http.ResponseWriter, r *http.Request, _ httprouter.Params) {
          content := r.URL.Query().Get("content")
          if content == "" {
          return
          }

          id := genID(content)
          note := model.Note{
          ID: id,
          Content: content,
          StartTime: time.Now(),
          UpdateTime: time.Now(),
          }
          notes[id] = note

          data := map[string]interface{}{
          "msg": "success",
          "data": "",
          }

          res, err := json.Marshal(data)
          if err != nil {
          return
          }

          _, err = wr.Write(res)
          if err != nil {
          return
          }
          }

          func Delete(wr http.ResponseWriter, r *http.Request, p httprouter.Params) {
          id := r.URL.Query().Get("id")
          if _, exist := notes[id]; exist {
          delete(notes, id)
          }

          data := map[string]interface{}{
          "msg": "success",
          "data": "",
          }

          res, err := json.Marshal(data)
          if err != nil {
          return
          }

          _, err = wr.Write(res)
          if err != nil {
          return
          }

          }

          // 生成一個隨機id
          func genID(content string) string {
          rand.Seed(time.Now().UnixNano())
          i := rand.Intn(10000)
          return fmt.Sprintf("%x", md5.Sum([]byte(content+string(i))))
          }

          看到這里,可能有人要噴了:

          哎,你這個玩意,怎么在業(yè)務邏輯里各種拼json,寫回去response中啊,這塊明顯是可以復用的邏輯?。?/p>

          哎,你這個玩意,獲取入?yún)⒌臅r候怎么這么挫啊,直接從URL里面拿,別人傳啥也不知道,還得自己做參數(shù)校驗,而且你這么寫,和寫動態(tài)語言有啥區(qū)別,根本看不出來入?yún)?、出參是什么?/p>

          哎,你這個玩意,怎么數(shù)據(jù)都不入數(shù)據(jù)庫啊,服務一重啟,數(shù)據(jù)就沒了!

          哎,你這個玩意,怎么連個日志都沒有啊,你這線上出問題了,怎么查??!

          沒錯,這些都是問題,讓我們來一個一個解決,首先先寫個公共的handler,規(guī)范一下我們HTTP handle func 入?yún)?、出參。這里通過反射分析handleFunc這個interface來做,這樣在注冊路由的時候就會對我們的handler函數(shù)參數(shù)做規(guī)范:

          type Controller struct {
          // handleFunc 的格式需要是func(ctx context.Context, req interface{}) (interface{}, error)
          handleFunc interface{}
          }

          func HTTPHandler(handleFunc interface{}) httprouter.Handle {
          controller := &Controller{handleFunc: handleFunc}

          checkHandleFunc(handleFunc)

          return controller.HandleHTTP
          }

          func checkHandleFunc(handleFunc interface{}) {
          value := reflect.ValueOf(handleFunc)
          typeOf := reflect.TypeOf(handleFunc)

          if value.Kind() != reflect.Func {
          panic("[checkHandleFunc] handleFunc is not a func")
          }
          ...
          }

          我們規(guī)定函數(shù)的簽名統(tǒng)一為:func(ctx context.Context, req interface{}) (interface{}, error),并在CheckHandlerFunc中對interface進行校驗確保其符合我們的簽名。這里用反射去哪接口的屬性即可。

          接著,我們把用戶入?yún)⒔壎ǖ蕉x好的結構體上,讓我們不用手動去parserHTTP參數(shù),這個需求本質上是bind功能,原理就是根據(jù)不同的Content-Type去用不同的方式解析出http 參數(shù),并通過tag用反射綁定到用戶自定義的結構體上。這塊自己寫比較無聊,因為需要大量重復的Content-Type判斷,于是再去github上找找開源庫。找到兩個star比較多的庫:https://github.com/mholt/bindinghttps://github.com/martini-contrib/binding,但這兩個庫各有缺點:mholt/binding的問題是你需要顯式實現(xiàn)一個FiledMap方法,把參數(shù)顯示綁定到FieldMap上,這個不能忍,pass。martini-contrib/binding是Martini框架中單獨抽出來的binding部分,由于其是Martini框架中抽出來的,很多類型在martini中都被預先改寫了,這對對只想用binding功能的我來說不太友好,于是也被pass。

          接著,我們去一些star數(shù)多的開源web框架上打打主意,gin框架里面的binding包沒有上面兩個包的缺點。我們可以單獨把binding部分fork出來搞一個項目,地址是:https://github.com/yigenshutiao/binding。引用這個包的時候遇到了一些go module的一些小問題。解決方法也比較簡單,把binding里go.mod模塊的聲明改一下即可。

          github.com/yigenshutiao/binding: github.com/yigenshutiao/[email protected]: parsing go.mod:
          module declares its path as: binding
          but was required as: github.com/yigenshutiao/binding

          replace binding => github.com/yigenshutiao/binding v0.0.2 // indirect

          有了bind之后,我們就可以非常開心把http.Request中的參數(shù)綁定到我們的結構體上,但是注意由于我們使用RESTful的方式傳參,還需要把http.router中Params的參數(shù)也綁定到結構體上,這里寫了一個Map2Struct的util函數(shù),原理也是根據(jù)tag做反射綁定。

          if len(p) > 0 {
          for i := range p {
          param[p[i].Key] = p[i].Value
          }

          if err := util.ConvertMapToStruct(param, request); err != nil {
          return
          }
          }
          request, err := c.bindRequest(r, request)
          if err != nil {
          return
          }

          綁定了參數(shù)之后,可以對用戶傳進來的參數(shù)進行校驗,校驗要做的工作是在處理業(yè)務邏輯之前,提前看參數(shù)是否符合我們的預期,這里引入一個叫validator的東西,它的功能如同Python里中裝飾器一樣,但是原理不太相同。首先我們對每個請求定義一個獨立的結構體,并在結構體中加入validatetag,做校驗。

          type Note struct {
          ID string `json:"id" form:"id" validate:"gt=0"`
          Content string `json:"content" form:"content" validate:"required"`
          StartTime time.Time
          UpdateTime time.Time
          }

          這里繼續(xù)找到了一個開源庫:gopkg.in/go-playground/validator.v9。使用也比較簡單:初始化一個validator,并調用其Struct方法來做校驗即可。這玩意對嵌套的struct也可以做validate,原理是這樣的:內部有一些自定義的校驗函數(shù),把用戶定義的Struct看做一顆嵌套結構的樹(如果有嵌套結構體的話),然后去遍歷這個樹上面的每一個節(jié)點,用反射現(xiàn)場去取節(jié)點的tag規(guī)則,節(jié)點的值,并調用其自定義函數(shù)進行校驗,如果校驗失敗,直接返回false,否則繼續(xù)遍歷這棵樹。

          if err := Validate.Struct(request); err != nil {
          return
          }

          到現(xiàn)在,調用業(yè)務邏輯的前置工作已經做得差不多了,接著,讓我們寫一個比較惡心的反射函數(shù)來調用業(yè)務函數(shù)

          func (c *Controller) callFunc(r *http.Request, request interface{}) (interface{}, error) {

          var err error

          f := reflect.ValueOf(c.handleFunc)
          returnVal := f.Call([]reflect.Value{reflect.ValueOf(r.Context()), reflect.ValueOf(request)})

          response := returnVal[0].Interface()
          if returnVal[1].Interface() != nil {
          var ok bool
          err, ok = returnVal[1].Interface().(error)
          if !ok {
          fmt.Println(returnVal[1].Interface())
          }
          }

          return response, err
          }

          隨后,把函數(shù)返回值Response2JSON這部分的功能也完善一下,這里就是預先定義好我們的返回格式結構體,并把數(shù)據(jù)放到data部分,并寫入ResponseWriter即可。

          type HTTPResponse struct {
          Msg string `json:"msg"`
          Data interface{} `json:"data"`
          }

          func response2JSON(ctx context.Context, wr http.ResponseWriter, resp interface{}, err error) string {

          respData := &HTTPResponse{
          Msg: Success,
          Data: resp,
          }

          if err != nil {
          respData = &HTTPResponse{
          Msg: Failed,
          Data: resp,
          }
          }

          res, err := json.Marshal(respData)
          if err != nil {
          respData = &HTTPResponse{
          Msg: JSONMarshalFailed,
          Data: nil,
          }

          res = []byte(form2JSON(respData))
          }

          if err := writeResponse(wr, res); err != nil {
          logging.Errorf("[response2JSON] writeResponse err :%v", err)
          return ""
          }

          return string(res)
          }

          接著讓我們喝一口可樂,再把數(shù)據(jù)做持久化,這就涉及到數(shù)據(jù)庫以及數(shù)據(jù)庫的選擇,數(shù)據(jù)庫有關系型數(shù)據(jù)庫、kv數(shù)據(jù)庫、列式數(shù)據(jù)庫、NoSQL等。

          我們這里選MySQL這個關系型數(shù)據(jù)庫做持久化存儲,MySQL采用固定的schema存儲數(shù)據(jù),支持事務,內置多種查詢引擎,還有完善的Binlog供數(shù)據(jù)導出和恢復。雖然我們的應用和事務沒什么關系~

          接著來看一下如何與數(shù)據(jù)庫交互,每種數(shù)據(jù)庫都會提供一種查詢DSL供用戶訪問數(shù)據(jù)庫,其中最流行的DSL非SQL莫屬,我們可以通過MySQL提供的client終端編寫SQL來訪問數(shù)據(jù)庫里的數(shù)據(jù),像這樣:

          在web程序中,有這幾種方式讓我們與DB進行交互:SQL、SQL Query Builder和ORM。SQL其實就是在程序中寫最原始的SQL與DB進行交互,SQL Query Builder在SQL上做了一層抽象,他規(guī)范了查詢模式,提供一些方法讓你可以拼出SQL,由于其抽象程度不高,其實可以根據(jù)SQL builder想象出原始的SQL是什么樣;ORM全名是object-relational mappers,做的事情是把數(shù)據(jù)庫中的表映射為類或者結構體,然后用其自帶的方法對這些類做和數(shù)據(jù)庫中CRUD等價的操作。

          在這幾種方式中,ORM的對DB的抽象程度最高,手工管理的成本最低,SQL Builder本質是拼出一個SQL去執(zhí)行,而直接寫SQL的方式需要對返回結果做一些處理。ORM把很多細節(jié)都因此掉了,SQL Builder這種方式對于DBA同學來說可能還是有點不直觀。比如筆者見過某DBA同學吐槽RD使用SQL builder時不管SQL查詢的字段是什么,都先拷貝一段的SQL Builder代碼,關鍵是這段代碼大部分內容用不上。。。

          最終在本項目中采用了介于SQL和SQL Builder之間的與DB的交互方式。這樣做的好處是,寫到文件最上方的SQL常量可以給DBA做SQL審計,下面對返回結果進行封裝,不必寫重復的代碼。最終的代碼像這樣:

          // 一段dao層的go代碼

          const (
          getAllNotes = `SELECT * FROM %v WHERE xx=:xx`
          )

          func GetAllNotes(ctx context.Context) ([]model.NewNote, error) {
          var (
          err error
          res []model.NewNote
          params = map[string]interface{}{}
          )

          if err = GetList(ctx, sourceTableName,
          getAllNotes, params, &res); err != nil {
          return nil, err
          }

          return res, nil
          }

          接下來讓我們給項目加上日志,目前我們的程序像這張圖一樣:代碼中出了問題根本沒有排查的依據(jù)。日志是線上排查問題的重要途徑。沒有日志的程序就像一個黑洞一樣,你根本不知道它發(fā)生了什么事情。打日志這件事情需要貫徹到具體的業(yè)務代碼邏輯中,這里為了說明,所以前期沒加日志,讀者看到了不要模仿,沒加日志不是我的本意~讓我們趕緊在代碼的各個關鍵地方把日志補上。

          go語言自帶的log庫定義了一個名為std的默認的Logger,它只有三個種Level:Print、Panic、Fatal,我們可以用其提供的New函數(shù)自定義一個logger并簡單定義一些行為:

          var Logger *log.Logger

          func InitLog() {
          openFile, err := os.OpenFile("./log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
          if err != nil {
          Logger.Printf("[InitLog] open log file failed | err:%v", err)
          }
          Logger = log.New(openFile, "NOTE:", log.Lshortfile|log.LstdFlags)
          }

          func Info(v ...interface{}) {
          Logger.SetPrefix(INFO)
          Logger.Println(v)
          }

          func Infof(fmt string, v ...interface{}) {
          Logger.SetPrefix(INFO)
          Logger.Printf(fmt, v)
          }

          ...

          寫到這里程序基本差不多了,現(xiàn)在讓我們寫個簡單的腳本mock一些數(shù)據(jù),為一會測試程序的吞吐量做準備:

          import requests

          def main():
          for i in range(10000):
          requests.post("http://127.0.0.1:8000/note",{'content': '呵呵'+str(i)})

          main()

          跑完這個腳本之后可以看到MySQL表里滿屏幕的呵呵.

          現(xiàn)在來用wrk測試一下GET /note接口,跑個性能測試:

          ?  ~ wrk -c 10 -d 10s -t10 http://localhost:8000/note
          Running 10s test @ http://localhost:8000/note
          10 threads and 10 connections
          Thread Stats Avg Stdev Max +/- Stdev
          Latency 1.22s 419.12ms 1.74s 50.00%
          Req/Sec 0.09 0.30 1.00 90.91%
          11 requests in 10.05s, 21.19MB read
          Socket errors: connect 0, read 0, write 0, timeout 7
          Requests/sec: 1.09
          Transfer/sec: 2.11MB

          結果有些尷尬,QPS 1,延遲平均1.22s,原因是這個接口每次都會把DB里面全部的數(shù)據(jù)枚舉出來。讓我們改造一下接口,傳入一個size, offset參數(shù),并讓結果按照create_time排序:

           ~ wrk -c 10 -d 10s -t10 http://localhost:8000/note?size=20&offset=100
          Running 10s test @ http://localhost:8000/note?size=20&offset=100
          10 threads and 10 connections
          Thread Stats Avg Stdev Max +/- Stdev
          Latency 55.99ms 75.48ms 455.66ms 83.92%
          Req/Sec 50.52 46.93 200.00 83.09%
          4252 requests in 10.10s, 9.08MB read
          Requests/sec: 420.92
          Transfer/sec: 0.90MB

          接著在表中的的create_time列建一個索引,結果有一些優(yōu)化:

          ?  ~ wrk -c 10 -d 10s -t10 http://localhost:8000/note?size=20&offset=0
          Running 10s test @ http://localhost:8000/note?size=20&offset=1
          10 threads and 10 connections
          Thread Stats Avg Stdev Max +/- Stdev
          Latency 26.54ms 31.08ms 196.73ms 84.79%
          Req/Sec 61.07 56.17 252.00 80.92%
          6001 requests in 10.07s, 12.37MB read
          Requests/sec: 595.65
          Transfer/sec: 1.23MB

          還可以根據(jù)業(yè)務場景把首頁熱點信息放入cache中,這樣服務的吞吐量會進一步增加:

          set note_info_0_20 "[{\"ID\":1,\"Content\":\"hello,world\",\"StartTime\":\"2020-11-17T13:19:13Z\",\"UpdateTime\":\"2020-11-17T13:19:13Z\"},\......\{\"ID\":21,\"Content\":\"\xe5\x91\xb5\xe5\x91\xb516\",\"StartTime\":\"2020-11-24T00:09:05Z\",\"UpdateTime\":\"2020-11-24T00:09:05Z\"}]"

          目前我們在連接db時候,連接的配置都是直接寫死在代碼中的,這好嗎,這很不好。意味著如果你換個環(huán)境運行這份代碼,還得去代碼里翻連接配置,讓我們把這些配置抽出來,放到一個公共的地方~

          setting, err := mysql.ParseURL("root:123456@/notes")
          client := redis.NewClient(&redis.Options{
          Addr: "127.0.0.1:6379",})

          配置加載的原理是把配置寫到json文件里面,通過實現(xiàn)定義好的結構體讀入變量中,連接db時候去讀取這些變量的值。

          type MySQLConfig struct {
          Database string `json:"database"`
          Dsn string `json:"dsn"`
          DbDriver string `json:"dbdriver"`
          MaxOpenConn int `json:"maxopenconn"`
          MaxIdleConn int `json:"maxidleconn"`
          ConnMaxLifetime int `json:"connmaxlifetime"`
          }


          var config MySQLConfig

          if err := confutil.LoadConfigJSON(MySQLPath, &config); err != nil {
          logging.Fatal("[initDB] load config failed")
          }

          接著,為了看清楚調用服務所花費的時間,我們完善一下日志,在HandleHTTP中defer一發(fā)記個時間。同理,如果想看清楚調用緩存花費的時間、調用MySQL花費的時間,在存儲公用的調用函數(shù)中用這種方式看清即可。

          start := time.Now()

          defer func() {
          procTimeMS := time.Since(start).Seconds() * 1000
          logging.Infof("request=%v|resp=%v|proc_time%.2f", request, respStr, procTimeMS)
          }()

          到這里,項目的milestone基本達成,讓我們來加個tag

          git tag v0.0.1 
          git push --tag

          接著給我們的項目加個Makefile,寫Makefile個人比較傾向于直接找牛逼的開源框架,看看人家的Makefile是什么寫的,然后抄就完事了。比如,之前發(fā)現(xiàn)這么一篇文章。https://colobu.com/2017/06/27/Lint-your-golang-code-like-a-mad-man/,這里很多工具都可以放到我們的Makefile中,如果像我這樣比較懶的人,可以順著這位大佬的博客找到他的github,找到star最多的框架,點開Makefile,哇,真是一片寶藏啊: https://github.com/smallnest/rpcx/blob/master/Makefile ;)

          最終項目的目錄結構像是這樣:

          ├── Makefile
          ├── README.md
          ├── common
          │   ├── bind.go
          │   ├── server.go
          │   ├── server_test.go
          │   ├── validator.go
          │   └── validator_test.go
          ├── config
          │   ├── database.json
          │   └── redis.json
          ├── go.mod
          ├── go.sum
          ├── init.go
          ├── logging
          │   └── logger.go
          ├── logic
          │   ├── note.go
          │   └── note_test.go
          ├── main.go
          ├── main_test.go
          ├── model
          │   └── dto.go
          ├── route.go
          ├── storage
          │   ├── mock_data.py
          │   ├── note.go
          │   ├── note_test.go
          │   ├── notes.sql
          │   └── util
          │   └── command.go
          └── util
          ├── confutil.go
          └── datautil.go

          至此,項目本身的代碼編寫就結束了。我們的標題是從0到1寫一個web服務,服務還包括部署相關的內容。這里先按下不表,下篇內容再著重聊聊服務部署、golang性能調優(yōu)相關的內容吧。


          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關注公眾號 「polarisxu」,回復 ebook 獲??;還可以回復「進群」,和數(shù)萬 Gopher 交流學習。

          瀏覽 55
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91成人大片 | 影音先锋中文字幕av | 免费有码精品一区四区 | 亚洲成人性爱网站 | 天天干天天射天天操 |