上一章遇到的问题
从零开始搭建后台服务 - 编程语言Golang(01)主要对Golang进行了初步的搭建和学习,编写了Dockerfile文件,选择gin框架作为web端进行容器化部署,但是上一章还有以下问题:
- 运行的镜像文件1.2GB,真的需要这么多吗?
- 编写完代码后,Go语言的测试代码应该怎么写?
- 版本升级后,如何快速部署新版本代码?
解决运行的镜像文件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程序的测试,相当于发了一个请求,返回的httpcode
是200
,http body
是abc
,测试内容是主程序中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路由,指向新端口,这样就能做到无缝升级和部署了