首次的Golang Web应用程序 ~ 测试,直至Docker容器化
大致来说
你好。我是kwashi。最近我对于容器相关技术很感兴趣,开始学习Golang。过去我一直使用Python,但是现在我完全迷上了Golang。因此,我写了一篇文章,从Golang的安装到Web应用程序的创建、测试以及Docker容器化都有涉及,也算是复习一下。对于已经了解了Golang语法但不知道下一步该怎么做的人来说,或许这篇文章正合适。
Golang的优点包括以下内容。
-
- Dockerイメージが軽量
- 例えば、本記事で作成したアプリケーションは、以下のように22.6MBとかなり軽量です。
REPOSITORY TAG IMAGE ID CREATED SIZE
kwashizaki/example-golang-rest-api v1.0.0 8d92d819d8ad 8 days ago 22.6MB
-
- パフォーマンス
-
- JavaやC++に匹敵するぐらい性能(処理速度)が良いです。
-
- 可読性
-
- Gofmtにより強制的にコードフォーマットされ、一貫したフォーマットになります。
-
- コンパイル時間が早い
-
- C++やJavaと比較してもかなり早いです。
-
- gRPCのサポート
-
- 昨今、マイクロサービスのはやりとともに、RPC経由の通信が増えています。
-
- GolangによるgRPCに関しては、Golangで始めるgRPCにて記事にしたので参考にしてみてください。
-
- 並列処理が書きやすい
- “go”というキーワードを関数呼び出し時に付け加えることで、goroutineというGoの軽量なスレッドを簡単に実行することができます。また、channelとうgoroutine間で通信する方法も用意されています。
对我个人而言,当涉及到机器学习、数据分析等复杂任务时,我会选择Python。而当需要创建与数据的CRUD操作、外部API的互联等服务(功能)时,我会选择Golang。
请注意
本文使用gin作为Web应用程序的框架。请注意,此框架仅在Mac OS上进行了测试。另外,请确保已经安装了git、Docker for Mac和VSCode。
git: k-washi/example-golang-rest-api的/health相关部分已在本文中提供支持。
Docker image: kwashizaki/example-golang-rest-api
关于创建Web应用程序
应用程序是一个支持对基础URL +路径表示的URL的查询的API。
/health/仅对GET请求返回状态码200。
基础 URL = http://localhost:8080
- path: /health/
- GET:
res: {"health": 200}
安裝和設置Go语言
安装
请从Golang官网下载并解压。
请确认已经使用以下命令安装。
go version
#go version go1.12.7 darwin/amd64
2. 环境设置
请将以下变量设置为环境变量。
GOPATH是用于构建Go项目的环境。GO111MODULE是用于管理Go1.11及以上版本引入的新版本管理工具Go Modules(vgo)的工具。请根据个人环境适当进行设置。
export GOPATH=$(go env GOPATH)
export PATH=$PATH:$GOPATH/bin
export GOPATH=/Users/xxxxxxxx/Documents/workspaces/golang
export GO111MODULE=on
因为每次设置这个环境变量都很麻烦,所以我会将其写入.bash_profile文件中。
vi ~/.bash_profile
cat ~/.bash_profile
#...
#export GOPATH=$(go env GOPATH)
#export PATH=$PATH:$GOPATH/bin
#export GOPATH=/Users/wxxxxxx/Documents/workspaces/golang
#export GO111MODULE=on
另外,在GOPATH目录中创建一个源代码文件夹(src)等。
tree -L 2
.
├── bin
│ ├── gobump
│ ├── ...
│ └── protoc-gen-go
├── pkg
│ └── mod
└── src
├── github.com
└── golang.org
本次我们假设在Github上管理源代码。例如,假设我们在Github上创建了一个名为k-washi/example-golang-rest-api的仓库。在$GOPATH/src/github.com/k-washi/目录下,可以使用git clone https://github.com/k-washi/example-golang-rest-api.git命令开始与Github相对应的项目。请根据个人设置进行调整。
3. VSCode的设置
安装Extension的Go。
实施
本章将创建一个Web应用程序,在 http://localhost/health/ 上返回状态码200。
由于我们使用轻量且简单的Gin作为Web框架,因此可以通过以下命令进行下载。
go get -u github.com/gin-gonic/gin
首先,我们将实现主要的处理逻辑。
版本在构建时使用。
package main
import (
"github.com/k-washi/example-golang-rest-api/src/app"
)
const version = "0.1.0"
func main() {
app.StartApp()
}
StartApp()在src/app中实现如下。请注意它是在package app中实现的。这个app的包在main.go中被调用。
-
- router.Use() – ミドルウェア処理を追加(ルーティングされる前に処理されます)
-
- mapUrls() – app.goの下にプログラムを記載しています。同じディレクトリで同じパッケージ名にてプログラムを始めているので、routerという変数が共有して使用できていることが見て取れます。ここでは、routerにGETなどのメソッド、ルーティングするパス、実行する処理(health pkgのGetHealthStatusOK関数)を登録しています。
- router.Run(:8080)で8080ポートでサーバーを実行しています。
package app
import (
"github.com/gin-gonic/gin"
"github.com/k-washi/example-golang-rest-api/src/middleware"
)
var (
router *gin.Engine
)
func init() {
router = gin.Default()
}
//StartApp call by main for starting app.
func StartApp() {
router.Use(middleware.OptionsMethodResponse())
mapUrls()
if err := router.Run(":8080"); err != nil {
panic(err)
}
}
package app
import (
"github.com/k-washi/example-golang-rest-api/src/controllers/health"
)
func mapUrls() {
router.GET("/health", health.GetHealthStatusOK)
}
接下来,我来解释一下中间件。中间件主要通过设置响应头和对OPTION请求的处理来实施CORS防护措施。
具体的设置内容请参考以下代码,在适当的情况下,需要根据应用程序的实际情况进行相应的配置。
当请求方法为OPTION时,我们使用Abord()函数来返回状态码200,而无需执行与URL相匹配的路由操作。
当请求方法不是OPTION时,我们使用Next()函数来执行路由操作。
关于CORS问题,CORS概述提供了很有参考价值的信息。
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
//OptionsMethodResponse CROS options response
func OptionsMethodResponse() gin.HandlerFunc {
return func(c *gin.Context) {
//アクセスを許可するドメインを設定
c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost")
//リクエストに使用可能なメソッド
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
//Access-Control-Allow-Methods 及び Access-Control-Allow-Headers ヘッダーに含まれる情報をキャッシュすることができる時間の長さ(seconds)
c.Writer.Header().Set("Access-Control-Max-Age", "86400")
//リクエストに使用可能なHeaders
c.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Origin, X-Requested-With, Content-Type, Accept")
//認証を使用するかの可否
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
//レスポンスのContent-Typeを設定する
c.Writer.Header().Set("Content-Type", "application/json")
if c.Request.Method != "OPTIONS" {
c.Next()
} else {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
c.Abort()
}
return
}
}
接下来,我们将实现在路由到/health/时调用的GetHealthStatusOK()函数。
该函数将执行ServiceHealth.GetHealth()服务,并在无错误的情况下返回结果。
package health
import (
"net/http"
"github.com/k-washi/example-golang-rest-api/src/services"
"github.com/gin-gonic/gin"
)
//GetHealthStatusOK status 200 response.
func GetHealthStatusOK(c *gin.Context) {
result, err := services.HealthService.GetHealth()
if err != nil {
c.JSON(err.GetStatus(), err)
return
}
c.JSON(http.StatusOK, result)
}
考虑到MVC模型,我们刚刚实现的health/controller.go对应于控制器(Controller)。接下来,我们要实现与MVC模型中的模型(Model)相对应的功能(Service)。
按照SOLID原则,我们将公开HealthService,通过在函数调用时初始化healthService结构体来插入。同时,作为HealthService的类型,我们实现了healthServiceInterface(通过这个实现,在测试时创建Mock变得更加容易)。在这个接口中,我们还会记录下被埋入到healthService的函数。
另外,我们将以以下的数据结构作为JSON返回的数据。
package health
type CreateHealthResponse struct {
Status int `json:"status"`
Description string `json:"description"`
}
关于错误,我们使用自己创建的内容。请参考git: example-golang-rest-api/src/utils/errors/。
※对于SOLID设计模式,我参考了《Hands-On Software Architecture with Golang》一书。
package services
import (
"net/http"
"github.com/k-washi/example-golang-rest-api/src/domain/health"
"github.com/k-washi/example-golang-rest-api/src/utils/errors"
)
type healthServiceInterface interface {
GetHealth() (*health.CreateHealthResponse, errors.APIError)
}
var (
//HealthService health check service
HealthService healthServiceInterface
)
type healthService struct{}
func init() {
HealthService = &healthService{}
}
func (s *healthService) GetHealth() (*health.CreateHealthResponse, errors.APIError) {
result := health.CreateHealthResponse{
Status: http.StatusOK,
Description: "Health check OK",
}
return &result, nil
}
执行
go run src/main.go
#もしappという名前でコンパイルするなら
go build -o app src/main.go
考试
在这里,我们将测试在上面实现的health/contorller.go文件中的GetHealthStatusOK函数。我们将使用一个名为testify的库进行测试。
为了进行测试,我们需要在文件名的末尾添加一个_test.go的文件来实现测试。另外,测试执行函数的名称需要以Test作为前缀。
首先,由于测试对象依赖于healthService.GetHealth(),我们需要创建healthService的模拟对象。接下来,
func init() {
HealthService = &healthService{}
}
在HealthService中,为了测试,我将新创建的healthServiceMock插入其中。
在实际进行测试的TestGetHealthStatusOK测试函数中,我创建了一个用于返回healthService.GetHealth()数据的值,并将其设置为Mock的GetHealth函数返回getHealthFunction函数的结果。结果是,healthService.GetHealth将返回创建的值。
然后,创建一个请求到/health/,并执行GetHealthStatusOK。
测试使用testify的功能来评估执行后的响应。
package health
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/gin-gonic/gin"
"github.com/k-washi/example-golang-rest-api/src/domain/health"
"github.com/k-washi/example-golang-rest-api/src/services"
"github.com/k-washi/example-golang-rest-api/src/utils/errors"
)
/*
#依存関係のあるhealthService.GetHealthのモックを作成
*/
type healthServiceMock struct {
}
var (
getHealthFunction func() (*health.CreateHealthResponse, errors.APIError)
)
func (m *healthServiceMock) GetHealth() (*health.CreateHealthResponse, errors.APIError) {
return getHealthFunction()
}
func init() {
services.HealthService = &healthServiceMock{}
}
//TestGetHealthStatusOK test of status 200 with service mock
func TestGetHealthStatusOK(t *testing.T) {
exsistHealthResponse := health.CreateHealthResponse{
Status: http.StatusOK,
Description: "Health check OK",
}
getHealthFunction = func() (*health.CreateHealthResponse, errors.APIError) {
return &exsistHealthResponse, nil
}
response := httptest.NewRecorder()
c, _ := gin.CreateTestContext(response)
request, _ := http.NewRequest(http.MethodGet, "/health", nil)
c.Request = request
GetHealthStatusOK(c)
assert.EqualValues(t, http.StatusOK, response.Code)
//Conver the JSON response to a map
var healthJSONResponse health.CreateHealthResponse
err := json.Unmarshal([]byte(response.Body.String()), &healthJSONResponse)
//Grab the balue & whether or not it exists
statusVal := healthJSONResponse.Status
descriptionVal := healthJSONResponse.Description
assert.Nil(t, err)
assert.Equal(t, exsistHealthResponse.Status, statusVal)
assert.Equal(t, exsistHealthResponse.Description, descriptionVal)
}
进行测试
用以下的命令执行测试。
go test ./...
Docker镜像和容器化
本节将进行Makefile、Dockerfile和Docker镜像的创建,将其容器化并执行实际的健康检查。
首先,创建一个类似以下的Makefile。
执行make help将显示可用的命令。
另外,可以使用make build或者make bin/app来编译,使用make devel-deps来安装其他开发所需的库。
NAME := example-golang-rest-api
VERSION := $(gobump show -r)
REVISION := $(shell git rev-parse --short HEAD)
LDFLAGS := "-X main.revision=$(REVISION)"
export GO111MODULE=on
## Install dependencies
.PHONY: deps
deps:
go get -v -d
# 開発に必用な依存をインストールする
## Setup
.PHONY: deps
devel-deps: deps
GO111MODULE=off go get \
golang.org/x/lint/golint \
github.com/motemen/gobump/cmd/gobump \
github.com/Songmu/make2help/cmd/make2help
# テストの実行
## Run tests
.PHONY: test
test: deps
go test ./...
## Lint
.PHONY: lint
lint: devel-deps
go vet ./...
golint -set_exit_status ./...
## build binaries ex. make bin/myproj
bin/%: ./src/main.go deps
go build -ldflags $(LDFLAGS) -o $@ $<
## build binary
.PHONY: build
build: bin/app
##Show heop
.PHONY: help
help:
@make2help $(MAKEFILE_LIST)
然后,您将创建Dockerfile。
为了减小应用程序运行的映像大小,将构建映像和运行映像分开,并进行多阶段构建(此应用程序的Docker映像大小约为20MB)。
在构建阶段,可能会先复制项目下的go.mod和go.sum文件,然后使用go mod download命令来安装依赖关系。
接着通过make文件进行构建。
在执行阶段,将编译结果的app从构建阶段复制过来,并设置为容器化时的执行入口点,并以8080端口进行公开。
FROM golang:1.12.7-alpine3.10 as build-step
RUN apk add --update --no-cache ca-certificates git make
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /go-app
COPY go.mod .
COPY go.sum .
RUN go mod download
COPY . .
RUN make devel-deps
RUN make bin/app
#runtime image
FROM alpine
COPY --from=build-step /go-app/bin/app /app
ENTRYPOINT ["./app"]
EXPOSE 8080
可以使用以下命令将其Docker化。
docker build -t kwashizaki/example-golang-rest-api:v1.0.0 .
然后,您可以使用以下命令进行容器化。
docker run -it -p 8080:8080 --rm --name example-golang-rest-api kwashizaki/example-golang-rest-api:v1.0.0
您可以使用以下命令来确保返回了实际的 JSON 数据。
curl http://localhost:8080/health
#{"status":200,"description":"Health check OK"}
辛苦了。以上就是简单地说,我们成功地用Golang创建了一个Web应用程序。
文献引用
-
- Hands-On Software Architecture with Golang
- Mastering Go