从零开始搭建后台服务 - 编程语言Golang(02)

238 阅读6分钟

上一章遇到的问题

从零开始搭建后台服务 - 编程语言Golang(01)主要对Golang进行了初步的搭建和学习,编写了Dockerfile文件,选择gin框架作为web端进行容器化部署,但是上一章还有以下问题:

  1. 运行的镜像文件1.2GB,真的需要这么多吗?
  2. 编写完代码后,Go语言的测试代码应该怎么写?
  3. 版本升级后,如何快速部署新版本代码?

解决运行的镜像文件1.2GB

镜像文件1.2GB,这显然不符合我们对Go应用程序的设定,之所以1.2GB是因为镜像包含了源码的编译。实际上我们只需要在源码中编译工程,之后把执行文件新启动一个镜像即可,开始改造Dockerfile

FROM golang:1.18 as builder

MAINTAINER Swei
WORKDIR /go/src/web-gin
ENV GO111MODULE on
ENV GOPROXY https://goproxy.cn,direct


# Get dependancies - will also be cached if we won't change mod/sum
COPY go.mod .
COPY go.sum .

# Get dependancies - will also be cached if we won't change mod/sum
RUN go mod download
# Copy everything from the current directory to the PWD(Present Working Directory) inside the container
COPY . .

# Download dependencies
#RUN go get -d -v ./...

# Build the Go app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o /go/bin/go-docker

FROM alpine:latest #增加这一行,拉一个新镜像

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

WORKDIR /root/

COPY --from=builder /go/bin/go-docker /go/bin/go-docker  #复制go环境下编译好的执行文件到第二个镜像
#docker 运行时监听的端口
EXPOSE 80 

ENTRYPOINT ["/go/bin/go-docker"]

编译镜像docker build -t web-gin,得到两个镜像:docker images

REPOSITORY                 TAG       IMAGE ID       CREATED          SIZE
web-gin                    latest    93ee19bdb633   13 minutes ago   15.6MB
<none>                     <none>    49003aecac9a   13 minutes ago   1.2GB
<none>                     <none>    b7bdacb961b4   2 days ago       1.2GB

很显然49003aecac9a这个匿名镜像是为了编译go产生的临时镜像,且之前的web-gin镜像b7bdacb961b4由于名字被覆盖变成匿名镜像。执行脚本时,删除该镜像docker image prune,运行镜像docker run -d -p 10002:80 web-gin,查看镜像是否启动docker ps

CONTAINER ID IMAGE        COMMAND             CREATED        STATUS        PORTS                                                    NAMES
00b685e5d7e8 web-gin      "/go/bin/go-docker" 14 seconds ago Up 12 seconds 0.0.0.0:10002->80/tcp, :::10002->80/tcp                  gifted_rubin
6c12f2c0926a b7bdacb961b4 "/go/bin/go-docker" 2 days ago     Up 2 days     0.0.0.0:10001->80/tcp, :::10001->80/tcp                  suspicious_herschel

vim复制文件内容:按esc键后,先按gg,然后ggyG(此处不是复制到剪切板!)
docker image prune删除镜像时会提示危险操作让你确认

编写测试代码

go语言的测试代码有一个特点,命名为该类名后加 _test,例如gin.go,测试类就叫gin_test.go
通常去看API的使用也可以看_test.go文件,这一般都是作者自己写的test代码会包含如何使用,针对上次的程序编写单元测试代码。

修改main.go暴露出测试函数方法

package main

import (
   "fmt"
   "github.com/gin-gonic/gin"
   "web-gin/service"
)
func main() {
   fmt.Println(Hello())
   fmt.Println(service.Hello())

   start(Hello()).Run(":80")
}
func start(data string) *gin.Engine {
   r := gin.Default()
   r.GET("/ping", func(c *gin.Context) {
      c.JSON(200, gin.H{
         "message": data,
      })
   })
   return r
}
func Hello() string {
   return "hello"
}

监听80端口,运行此程序,输入http://127.0.0.1/ping,返回{ "message": "hello" }

新建main_test.go,我们编写两个测试用例,第一个用例是Hello()函数,这个函数有个特点,他是大写开头,即不在同一个包下也可以访问,第二个用例是针对start()函数做测试。

编写测试用例过程中,由于对go语法不熟,做了以下尝试:

  • 比如test.Hello(),这个意思是:test包名下的Hello函数,Hello函数相当于java中是静态方法,test相当于java中的类,发现最小粒度是包名

  • 在同一个包名下,无法命名同一个方法名,会无法编译过,参数、返回不同或个数不同都不行,即无法多态

  • 定义变量赋值a:="b"等同于var a string = "b",如果a变量没有被使用会编译不过,如果不使用可以用_匿名变量

  • main包名的函数无法被其它包名的函数调用

main_test.go第一个测试用例

package main
import "testing"
func TestHello(t *testing.T) {
   result := Hello()
   if result != "hello" {
      t.Errorf("error")
   }
}

IDE上可以直接运行TestHello方法,和java中的junit测试很相似输出,这个测试函数中,参数比较重要是t *testing.T,执行时等同于这个参数被赋值进来了,另外import标准testing,类别java的测试

=== RUN   TestHello
--- PASS: TestHello (0.00s)
PASS

main_test.go第二个测试用例

package main
import (
   "github.com/go-playground/assert/v2"
   "net/http"
   "net/http/httptest"
   "testing"
)
func TestStartHttpServer(t *testing.T) {
   w := httptest.NewRecorder()
   req, _ := http.NewRequest("GET", "/ping", nil)
   data := "abc"
   router := start(data)
   router.ServeHTTP(w, req)
   print("router end")
   assert.Equal(t, 200, w.Code)
   assert.MatchRegex(t, w.Body.String(), data)
}

第二个测试用例,本质上是对web程序的测试,相当于发了一个请求,返回的httpcode200http bodyabc,测试内容是主程序中start函数返回的*gin.Engine,利用该对象进行发送请求即router.ServeHTTP(w, req)
执行该测试方法,运行结果如下

[GIN-debug] GET    /ping                     --> web-gin.start.func1 (3 handlers)
[GIN] 2022/11/21 - 04:10:19 | 200 |            0s |                 | GET      "/ping"
router end--- PASS: TestStartHttpServer (0.01s)
PASS

符合测试预期,通过。
测试函数里面,有一项比较有意思,即req, _ := http.NewRequest("GET", "/ping", nil) 这里返回了两个参数,一个的需要被处理的req,还有是_匿名的返回.

进入其函数内部func NewRequest(method, url string, body io.Reader) (*Request, error)可以发现该函数第一个参数类型省略了,返回则有两个返回。这应该是golang语言特性,多参数返回、省略参数类型。

继续实验,_有返回时报错实验

req, error := http.NewRequest("gET", "好ht://www.ping", strings.NewReader("q=江"))
if error != nil {
   t.Errorf("req error")
} else {
   t.Logf("req ok")
}

这样构建req会报错,原因是url参数字符只要有一个超过ascaii码0x7f=127即返回异常,底层源码

u, err := urlpkg.Parse(url)

func Parse(rawURL string) (*URL, error) {
   // Cut off #frag 省略其它代码
   u, frag, _ := strings.Cut(rawURL, "#")
   url, err := parse(u, false)
   return url, nil
}
func parse(rawURL string, viaRequest bool) (*URL, error) {
      if stringContainsCTLByte(rawURL) {
       return nil, errors.New("net/url: invalid control character in URL")
    }
}
func stringContainsCTLByte(s string) bool {
   for i := 0; i < len(s); i++ {
      b := s[i]
      if b < ' ' || b == 0x7f {//0x7f = 127
         return true
      }
   }
   return false
}

此时返回如下:

[GIN-debug] GET    /ping                     --> web-gin.start.func1 (3 handlers)
    main_test.go:18: req error
--- FAIL: TestStartHttpServer (0.00s)
panic: runtime error: invalid memory address or nil pointer dereference [recovered]
	panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x8 pc=0xeac35a]

以上示例,主要看了在go语言中测试方法,go的基础语法,go的源码阅读,功能调用如何找到调用代码,对我们如何学习一门语言和API的记忆有较大的帮助。

快速部署

我的想法是利用git来对代码进行控制,本地写完代码并测试通过后push到远端分支,服务端更新代码时更新git,之后执行make_image.sh编译镜像,以及执行run_image.sh,更新时docker镜像端口发生改变,上线测试没问题后,修改Nginx路由,指向新端口,这样就能做到无缝升级和部署了