首次的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
广告
将在 10 秒后关闭
bannerAds