Golang web starter

dasheng發表於2017-07-30

背景

Web應用長期以來是Ruby、Java、PHP等開發語言的戰場。

  • Ruby可以實現快速原型開發,Ruby On Rails “全能”框架實現“全棧”開發,缺點有大型應用效能差、除錯困難;
  • Java 20多年的發展歷程,各種第三方庫、框架健全,執行效率高,但是隨著應用的功能膨脹,臃腫的get/set方法,JVM佔用大量計算機資源、效能除錯困難,函數語言程式設計不友好。
  • PHP,TL;DR

本文實現了一個最小化web應用,以此來了解Golang web的生態,通過使用Docker隔離開發環境,使用Posgres持久化資料,原始碼請參考這裡

Why Go?

  • 效能優越
  • 部署簡單,只需要將打包好的二進位制檔案部署到伺服器上
  • 內建豐富的標準庫,讓程式設計師的生活變得簡單美好
  • 靜態語言,型別檢查
  • duck typing
  • goroutine將開發人員從併發程式設計中解放出來
  • 函式作為“一等公民”
  • ...

Golang第三方框架選擇

  • Web框架: Gin,效能卓越,API友好,功能完善
  • ORM: GORM,支援多種主流資料庫方言,文件清晰
  • 包管理工具: Glide,類似於Ruby的bundler或者NodeJS中的npm
  • 測試工具:
    • GoConvey,符合BDD測試風格,支援瀏覽器測試結果的視覺化
    • Testify,提供豐富的斷言和Mock功能
  • 資料庫migration: migrate
  • 日誌工具: Logrus,結構化日誌輸出,完全相容標準庫的logger

Dockerize開發環境

釋出應用base image

Dockerfile如下:

FROM golang:1.8

# 包管理工具
RUN curl https://glide.sh/get | sh  

# 程式碼熱載入    
RUN go get github.com/codegangsta/gin  

# 資料庫migration工具
RUN go get -u -d github.com/mattes/migrate/cli github.com/lib/pq
RUN go build -tags 'postgres' -o /usr/local/bin/migrate github.com/mattes/migrate/cli

釋出資料庫base image

Dockerfile如下:

FROM postgres:9.6

# 初始化資料庫配置
COPY ./init-user-db.sh /docker-entrypoint-initdb.d/init-user-db.sh

啟動服務

執行auto/dev即可啟動,具體的配置如下。

  • docker-compose.yml:
version: "3"

services:
  dev:
    links:
      - db
    image: 415148673/golang-web-base-image@sha256:18de5eb058a54b64f32d58b57a1eb3009b9ed49d90bd53056b95c5c8d5894cd6
    environment:
      - PORT=8080
      - DB_USER=docker
      - DB_HOST=db
      - DB_NAME=webstarter
    volumes:
      - .:/go/src/golang-web-starter
    working_dir: /go/src/golang-web-starter
    ports:
      - "3000:3000"
    command: gin

  db:
    image: 415148673/postgres@sha256:6d4800c53e68576e05d3a61f2b62ed573f40692bcc72a3ebef3b04b3986bb70c
    volumes:
      - go-web-starter-db-cache:/var/lib/postgresql/data

volumes:
  go-web-starter-db-cache:
  • 安裝第三方依賴所需的glide配置檔案,通過在容器內執行glide install進行安裝:
package: golang-web-starter
import:
- package: github.com/gin-gonic/gin
  version: ^1.1.4
- package: github.com/jinzhu/gorm
  version: ^1.0.0
- package: github.com/mattes/migrate
  version: ^3.0.1
- package: github.com/lib/pq
- package: github.com/stretchr/testify
  version: ^1.1.4
- package: github.com/smartystreets/goconvey
  version: ^1.6.2
  • 資料庫migration的指令碼:
migrate -source file://migrations -database "postgres://$DB_USER:$DB_PASSWORD@$DB_HOST:5432/$DB_NAME?sslmode=disable" up

業務實現

Router

router := gin.Default()
router.GET("/", handler.ShowIndexPage)        // 顯示主頁面
router.GET("/book/:book_id", handler.GetBook) // 通過id查詢書籍
router.POST("/book", handler.SaveBook)        // 儲存書籍

handler

以儲存書籍為例:

func SaveBook(c *gin.Context)  {
    var book models.Book
    if err := c.Bind(&book); err == nil {
    // 呼叫model的儲存方法
        book := models.SaveBook(book)   

    // 繫結前端頁面所需資料
        utility.Render(
            c,
            gin.H{
                "title": "Save",
                "payload": book,
            },
            "success.html",
        )
    } else {
    // 異常處理
        c.AbortWithError(http.StatusBadRequest, err)
    }
}

model

func SaveBook(book Book) Book {
  // 持久化資料
    utility.DB().Create(&book)
    return book;
}

建立DB連線

func DB() *gorm.DB {
    dbInfo := fmt.Sprintf(
        "host=%s user=%s dbname=%s sslmode=disable password=%s",
        os.Getenv("DB_HOST"),
        os.Getenv("DB_USER"),
        os.Getenv("DB_NAME"),
        os.Getenv("DB_PASSWORD"),
    )
    db, err := gorm.Open("postgres", dbInfo)
    if err != nil {
        log.Fatal(err)
    }
    return db
}

View

<body class="container">
        {{ template "menu.html" . }}
        <label>儲存成功</label>
        <h1>{{.payload.Title}}</h1>
        <p>{{.payload.Author}}</p>
        {{ template "footer.html" .}}
</body>

測試

func TestSaveBook(t *testing.T) {
    r := utility.GetRouter(true)
    r.POST("/book", SaveBook)

    Convey("The params can not convert to model book", t, func() {
        req, _ := http.NewRequest("POST", "/book", nil)
        req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

        utility.TestHTTPResponse(r, req, func(w *httptest.ResponseRecorder) {
            So(w.Code, ShouldEqual, http.StatusBadRequest)
        })
    })

    Convey("The params can convert to model book", t, func() {
        req, _ := http.NewRequest("POST", "/book", strings.NewReader("title=Hello world&author=will"))
        req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
        utility.TestHTTPResponse(r, req, func(w *httptest.ResponseRecorder) {
            p, _ := ioutil.ReadAll(w.Body)
            So(w.Code, ShouldEqual, http.StatusOK)
            So(string(p), ShouldContainSubstring, "儲存成功")
        })
    })
}

總結

Go生態之活躍令我大開眼界,著名的應用如ocker, Ethereum都是使用Go編寫的。使用Go進行web開發的過程,感覺和搭積木一樣,一個合適的第三方庫需要在多個候選庫中精心篩選,眾多的開源作者共同構建了一個“模組”王國。在這樣的環境中,程式設計變成了一件很自由的事情。由於Go的標準庫提供了很多內建的實用命令如go fmt,go test,讓程式設計變得異常輕鬆,簡直是強迫型程式設計師的“天堂”。 當然Go語言還處在發展過程中,也有許多不完善的地方,比如

  • 缺少標準的依賴管理工具(正在開發的dep
  • 非中心化的依賴倉庫會出現由於某個依賴被刪除導致應用不可用等。

歡迎關注我的微信公眾平臺,更多隨筆隨後更新: whisperd

相關文章