Golang web starter

背景

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

2 个评论

nice ~~~
dasheng

dasheng 回复 suc

thank you~

要回复文章请先登录注册