Github Action

是Github 提供的免费可持续集成服务,在 GitHub Actions 的仓库中自动化、自定义和执行软件开发工作流程。 可以发现、创建和共享操作以执行任何作业(包括 CI/CD),并将操作合并到完全自定义的工作流程中。

GitHub Actions 简单易用,在仓库根目录下建立.github/workflows文件夹,把workflows工作流文件(YAML)放到这个目录下,就可以使用Github Action服务了

workflows 文件

由多个job组合而成,其指定了持续集成要完成的工作,每个工作流文件代表着一个持续集成工程

  • 每个workflow由多个job组成,每个job就是你指定的任务
  • 每个job由多个step组成,每个step代表任务的具体步骤
  • 每个step由多个action组成,代表具体要执行的指令,actoinworkflows中的最小单位

GitHub Actions Workflows

Action

GitHub提供了大量方便的工具,使你可以轻松的完成各种任务。比如克隆仓库并检出,向仓库提交内容,安装各种语言环境,同步文件等等,都有对应的工具,它们就是 Action,只需要调用它们就可以了,几乎不用写任何命令,你需要的在Action市场几乎全部可以找到。如 actions/checkout是用来checout仓库的,actions表示这个工具是官方的,如果是其它名称,则表示是第三方提供的

Hexo 发布到 Github Pages 及 Ailyun

没有使用 GitHub Actions 的发布流程

  1. push
  2. 生成静态页:Hexo或hugo,Jekyll等静态博客生成器
  3. push静态页,在 github pages上完成发布
  4. 登录远程服务器,拉取静态页,在自己的站点完成发布

使用 GitHub Actions 的发布流程

  1. push

后面的工作完全自动化完成

  • 你不需要在你的本机上安装Hexo等静态站点生成器运行环境
  • 文章的Build的工作在GitHub 的 work runner中完成,不需要占用的你电脑的资源
  • 在GitHub 上构建速度快,软硬件兼容性问题也少
  • 可以随时随地的部署:比如你发布后发现有个错别字要改,那么你用手机登录Github,直接在页面上修改,提交后就会触发部署,甚至都不需要打开电脑,方便快捷。

建立SSH密钥对

博客涉及到三个仓库:

  • Hexo.git,用来存放Hexo项目和文章
  • next.git博客主题
  • gagahappy.github.io 用来存放发布的HTML页,设置为 Github Pages

为什么会用到密钥对

  • Aliyun服务器:构建过程中要远程登录到Aliyun服务器,要用密钥进行身份验证,需要用到密钥对

  • Github Pages:Github的建议是把源文件即Hexo.git放到master分支,把gagahappy.github.io放到gh-pages分支。但Github Pages必须是公开仓库才可以发布,这样就让存放源文件的Hexo.git也可以被访问,这显然是不安全的,所以没有把这两个仓库放在一起,那么对gagahappy.github.io进行发布就需要使用密钥对,才能对仓库进行操作。如果你遵循了Github的建议,那这么在发布Github pages时就不用设置密钥对了。

生成密钥对

ssh-keygen -t rsa -b 4096 -C "$(git config user.email)" -f gh-pages -N ""
# You will get 2 files:
#   gh-pages.pub (public key)
#   gh-pages     (private key)

gh-pages.pub中的内容添加到远程服务器的~/.ssh/authorized_keys中或把文件上传到~/.ssh/目录,执行

cat gh-pages.pub >> authorized_keys
chmod 600 -R ~/.ssh # 设置~/ssh目录权限

设置 github Secrets

Secrets 是用来存放敏感信息的,如数据库的密码,用户名,密钥,等信息,不会让这些信息在日志输出中暴露出来,会以星号(*)代替这些敏感信息。

在Hexo.git仓库的首页点击 settings,在左侧找到Secrets,点击右上New repository secret name填写ACTIONS_DEPLOY_KEYvalue填写密钥gh-pages的内容, 点击Add secret,这样就创建了一个secret。之后可以用这个secret操作 gagahappy.github.io 仓库。

你还可以创建其它的secret,比如MySQL连接的密码,可以新建一个名称为mysql_passwd,内容为MySQL密码的Secret。更多信息可以查看官方 Secrets 帮助

# secret 在日志中使用 * 号输出敏感信息
SOURCE: public/
REMOTE_HOST: ***
REMOTE_USER: ***
TARGET: ***
SSH_PRIVATE_KEY: ***

设置 Deploy keys

打开gagahappy.github.io仓库,在仓库的首页点击 settings,在左侧找到Deploy keys,把gh-pages.pub的内容复制进去,勾选下面的Allow write access,点击Add Key,这样在之后的部署过程中,Hexo.git仓库所在的runner就可以通过secrets.ACTIONS_DEPLOY_KEY对其进行操作了。

编写工作流文件

在Hexo.git项目根目录下建立.github/wrokflows文件夹,在这个文件夹下创建以yml结尾的文件build.yml。每当你push到github后,github会自动寻找仓库根目录下的.github/wrokflows中扩展名为yml的所文件,运行其中的指令,完成自动部署。

mkdir -p .github/workflows
touch .github/workflows/build.yml

需要持续集成完成的工作有

  1. Checkout hexo.git中的内容
  2. Checkout next.git主题内容到 theme目录
  3. 安装 node.js环境
  4. 安装Hexo
  5. 使用Hexo生成静态页面
  6. 发布静态页面到 gagahappy.github.io
  7. 同步静态页到Aliyun服务器

把以上内容翻译为要执行的配置命令就是

on:
  push:
    branches: [master] # 在master分支push时触发部署
    paths-ignore: # 以下文件的变更不触发部署
      - README.md
  workflow_dispatch:

env: # 统一设置环境变量
  GIT_USER: git
  GIT_EMAIL: email@example.com
  THEME_REPO: git/next # 你使用的主题仓库地址
  THEME_BRANCH: master
  DEPLOY_REPO: git/gagahappy.github.io # 你用来发布github pages的仓库地址
  DEPLOY_BRANCH: master

jobs:
  build: # job_id,其它job可以引用,比如两个job有先后顺序
    name: Build Github Page And Deploy On Aliyun # job名称
    runs-on: ubuntu-latest # 构建使用的系统,支持 linux/mac/windows
    timeout-minutes: 5 # job超时时间,超过后job会被停止运行

    steps: # 一个job由多个 step 组成,本博的由7个step组成
    	# 1. Checkout hexo.git 中的内容
      - name: Checkout Hexo repo
      	# 检出项目,如果不指定名称,默认使用运行Github Action 的仓库,即hexo.git
        uses: actions/checkout@v2

    	# 2. Checkout next.git 主题内容到 theme目录
      - name: Checkout Theme repo
      	# actions/checkout 就是Acton工具,实现checkout项目,每个action也是一个git仓库
      	# 可以直接通过 github.com/工具名称进行访问,即github.com/actions/checkout
        uses: actions/checkout@v2
        with:
          repository: ${{ env.THEME_REPO }} # 要checkout的仓库地址
          ref: ${{ env.THEME_BRANCH }}
          path: themes/next # 要checkout到的路径

    	# 3. 安装 node.js 环境
      - name: Use Node.js ${{ matrix.node }}
        uses: actions/setup-node@v2 # 使用actions/setup-node安装nodejs
        with:
          node-version: ${{ matrix.node }}

    	# 4. 安装Heox
      - name: Install Hexo
        run: | # 运行command命令
          npm install -g hexo-cli
          npm install

    	# 5. 使用Hexo生成静态页面
      - name: Generate Blog
        run: |
          hexo generate

    	# 6. 发布静态页面到 gagahappy.github.io
			# 使用 peaceiris/actions-gh-pages 第三方Action工具,发布静态内容到 github pages
			# 重要:
			# 如果你的源码文件与Html文件在两个仓库,请在源码文件仓库设置
			# secrets 的 ACTIONS_DEPLOY_KEY 值为密钥内容,
			# 在html文件仓库即youname.github.io中设置 Deploy keys 的值设置为公钥内容,
			# 在构建过程中会使用密钥对youname.github.io进行操作
      - name: Deploy Github Page
        uses: peaceiris/actions-gh-pages@v3
        with:
          # 在 hexo.git 上设置的私钥
          deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} # ACTIONS_DEPLOY_KEY 不能修改
          external_repository: ${{ env.DEPLOY_REPO }} # Hexo.git 的仓库地址
          publish_branch: ${{ env.DEPLOY_BRANCH }}
          publish_dir: ./public # 使用Hexo/hugo等生成静态页面的目录
          user_name: ${{ env.GIT_USER }}
          user_email: ${{ env.GIT_EMAIL }}

    	# 7. 同步静态页到Aliyun服务器
      - name: Deploy Github Page on Aliyun
        uses: easingthemes/ssh-deploy@main
        env:
            SSH_PRIVATE_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}
            ARGS: "-rltgoDzvO --delete"
            SOURCE: "public/" # 使用Hexo/hugo等生成静态页面的目录
            REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
            REMOTE_USER: ${{ secrets.REMOTE_USER }}
            TARGET: ${{ secrets.REMOTE_TARGET }}

可以点击仓库主页上的Actions,查看正在运行的workflows,里面有详细的日志信息

GitHub Actions Workflows

参考链接

cpu 标记

用来设置运行时,P的最大数量,即GOMAXPROCS的值。可以设置一组值,在运行测试时,P被设置为不同的值分别运行测试函数。

最大P数量代表着 go 运行时系统同时运行goroutine的能力,可以被视为最大逻辑CPU的数量,在默认情况下,最大 P 数量就等于当前计算机 CPU 核心的实际数量,最大P可以大于或小于实际CPU的核心数量。

通过设置-cpu的值,就可以来模拟程序在不同CPU数量下的表现。设置-cpu=2,4,8

go test -cpu=2,4,8 -bench=. gott/hello
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-2         5081475               228.6 ns/op
BenchmarkHello-4         5190867               215.8 ns/op
BenchmarkHello-8         5256798               212.6 ns/op
PASS
ok      gott/hello      4.129s

性能测试函数BenchmarkHello被执行了3次,最大P数量分别为2,4,8

如果没有指定-cpu参数,测试运行时使用默认的最大P的数量,这个数量就等于当前CPU实际的核心数(比如你的CPU是4核,那么这个数量就是4)

go test -bench=.  gott/hello
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-4         4670545               234.7 ns/op
PASS
ok      gott/hello      1.381s

BenchmarkHello-4 表明默认P的值为4,即cpu是4核

count 标记

设置重复执行测试函数的次数

性能函数总的执行次数=-cpu标记的值中正整数的个数 x -count标记的值 x 探索式执行中测试函数的实际执行次数=2 * 2 * 5 =20

go test -count=2 -bench=. -cpu=2,4  gott/hello
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-2         5184015               224.1 ns/op
--- BENCH: BenchmarkHello-2
    hello_test.go:30: NNNNN: 1
    hello_test.go:30: NNNNN: 100
    hello_test.go:30: NNNNN: 10000
    hello_test.go:30: NNNNN: 1000000
    hello_test.go:30: NNNNN: 5184015
BenchmarkHello-2         5676196               214.5 ns/op
--- BENCH: BenchmarkHello-2
    hello_test.go:30: NNNNN: 1
    hello_test.go:30: NNNNN: 100
    hello_test.go:30: NNNNN: 10000
    hello_test.go:30: NNNNN: 1000000
    hello_test.go:30: NNNNN: 4618954
    hello_test.go:30: NNNNN: 5676196
BenchmarkHello-4         5304111               213.8 ns/op
--- BENCH: BenchmarkHello-4
    hello_test.go:30: NNNNN: 1
    hello_test.go:30: NNNNN: 100
    hello_test.go:30: NNNNN: 10000
    hello_test.go:30: NNNNN: 1000000
    hello_test.go:30: NNNNN: 5304111
BenchmarkHello-4         5286376               212.4 ns/op
--- BENCH: BenchmarkHello-4
    hello_test.go:30: NNNNN: 1
    hello_test.go:30: NNNNN: 100
    hello_test.go:30: NNNNN: 10000
    hello_test.go:30: NNNNN: 1000000
    hello_test.go:30: NNNNN: 5286376
PASS
ok      gott/hello      6.601s

功能函数总的执行次数=-cpu标记的值中正整数的个数 x -count标记的值=3 x 2 = 6

go test -v -count=2  -cpu=2,4,8  gott/hello
=== RUN   TestHello
    hello_test.go:17: number of runtime.GOMAXPROCS: 2
=== RUN   TestHello
    hello_test.go:17: number of runtime.GOMAXPROCS: 2
=== RUN   TestHello
    hello_test.go:17: number of runtime.GOMAXPROCS: 4
=== RUN   TestHello
    hello_test.go:17: number of runtime.GOMAXPROCS: 4
=== RUN   TestHello
    hello_test.go:17: number of runtime.GOMAXPROCS: 8
=== RUN   TestHello
    hello_test.go:17: number of runtime.GOMAXPROCS: 8
ok      gott/hello      0.009s

对于功能测试,并不需要重复的执行多次,只需要执行一次即可,所以可以把-cpu的数值设置为1,-count的值不用设置,使用默认值为1就可以。功能测试关注的重点是验证逻辑是否正确,而不是程序的性能

go test -v   -cpu=1 gott/hello
=== RUN   TestHello
    hello_test.go:17: number of runtime.GOMAXPROCS: 1
--- PASS: TestHello (0.00s)
PASS
ok      gott/hello      0.008s

Parallel 标记

-parallel标记可以指定同时运行测试用例的最大并发执行数,但仅适用于单个二进制测试文件,通过修改GOMAXPROCS的值实现,该标记的默认值是测试运行时的最大 P 数量。

可以通过在测试函数中添加t.Parallel()调用,以同时运行多个功能测试。默认情况下 go test 在运行测试时,为了加快测试速度,package 是被并行运行的,但每个包中的功能测试用例是被串行执行的。

func TestParallelPrintHello(t *testing.T) {
	t.Parallel()
	for i := 0; i < 5; i++ {
		time.Sleep(200 * time.Millisecond)
		t.Log("==========Hello==========")
	}

}

func TestParallelPrintWorld(t *testing.T) {
	t.Parallel()
	t.Log("number of runtime.GOMAXPROCS:", runtime.GOMAXPROCS(0))
	for i := 0; i < 5; i++ {
		time.Sleep(200 * time.Millisecond)
		t.Log("==========World==========")
	}
}

使用t.Parallel()指定需要并行执行的测试,在测试结果中可以看到TestParallelPrintWorld与TestParallelPrintHello交替输出,说明这两个测试函数是并行执行的

go test -v -parallel=8 gott/hello
=== RUN   TestParallelPrintHello
=== PAUSE TestParallelPrintHello
=== RUN   TestParallelPrintWorld
=== PAUSE TestParallelPrintWorld
=== CONT  TestParallelPrintHello
=== CONT  TestParallelPrintWorld
    hello_test.go:43: number of runtime.GOMAXPROCS: 4
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
=== CONT  TestParallelPrintWorld
    hello_test.go:46: ==========World==========
    hello_test.go:46: ==========World==========
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
    hello_test.go:36: ==========Hello==========
=== CONT  TestParallelPrintWorld
    hello_test.go:46: ==========World==========
    hello_test.go:46: ==========World==========
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
=== CONT  TestParallelPrintWorld
    hello_test.go:46: ==========World==========
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
--- PASS: TestParallelPrintWorld (1.02s)
--- PASS: TestParallelPrintHello (1.02s)
PASS
ok      gott/hello      1.026s
  • 测试结果中runtime.GOMAXPROCS: 4 的值为4,并不是指定的8,说明-parallel标记并没有生成,因为测试运行的不是二进制文件,此时,即使不指定-parallel也是可以的,如果想让更多的测试用例同时运行,可以在运行go test时,指定-p参数,运行更多的P,来同时执行更多的测试用例,比如设置p的值为4,让同时并行执行的最大并发数为4
  • 使用t.Parallel()后,输出的信息多了PAUSECONT字段

如果加入-count=2标记,对于测试用例来说,此时的并发不会是2个count之间的并发,这两个count也是串行执行的

go test -v -count=2  gott/hello
# 这里是第一遍count
=== RUN   TestParallelPrintHello
=== PAUSE TestParallelPrintHello
=== RUN   TestParallelPrintWorld
=== PAUSE TestParallelPrintWorld
=== CONT  TestParallelPrintHello
=== CONT  TestParallelPrintWorld
    hello_test.go:43: number of runtime.GOMAXPROCS: 4
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
=== CONT  TestParallelPrintWorld
    hello_test.go:46: ==========World==========
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
=== CONT  TestParallelPrintWorld
    hello_test.go:46: ==========World==========
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
=== CONT  TestParallelPrintWorld
    hello_test.go:46: ==========World==========
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
=== CONT  TestParallelPrintWorld
    hello_test.go:46: ==========World==========
    hello_test.go:46: ==========World==========
--- PASS: TestParallelPrintWorld (1.02s)
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
--- PASS: TestParallelPrintHello (1.02s)

# 这里是第二遍count
=== RUN   TestParallelPrintHello
=== PAUSE TestParallelPrintHello
=== RUN   TestParallelPrintWorld
=== PAUSE TestParallelPrintWorld
=== CONT  TestParallelPrintHello
=== CONT  TestParallelPrintWorld
    hello_test.go:43: number of runtime.GOMAXPROCS: 4
    hello_test.go:46: ==========World==========
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
    hello_test.go:36: ==========Hello==========
=== CONT  TestParallelPrintWorld
    hello_test.go:46: ==========World==========
    hello_test.go:46: ==========World==========
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
    hello_test.go:36: ==========Hello==========
=== CONT  TestParallelPrintWorld
    hello_test.go:46: ==========World==========
    hello_test.go:46: ==========World==========
=== CONT  TestParallelPrintHello
    hello_test.go:36: ==========Hello==========
--- PASS: TestParallelPrintWorld (1.01s)
--- PASS: TestParallelPrintHello (1.01s)
PASS
ok      gott/hello      2.036s

源码文件

// hello.go
func main() {
	hello("max")
}

func hello(name string) string {
	return fmt.Sprint("hello ", name)
}

// hello_test.go
func TestHello(t *testing.T) {
	name := "Max"
	expected := fmt.Sprintf("hello %s", name)
	greeting := hello(name)
	if greeting != expected {
		t.Errorf("hello(%s) = %s, expected = %s", name, greeting, expected)
	}
	t.Log(expected)
	t.Log("number of runtime.GOMAXPROCS:", runtime.GOMAXPROCS(0))

}

func TestPrint(t *testing.T) {
	t.Log("just Print")
}

func BenchmarkHello(b *testing.B) {
	for i := 0; i < b.N; i++ {
		hello("Max")
	}
	b.Log("NNNNN:", b.N)
}

func TestParallelPrintHello(t *testing.T) {
	t.Parallel()
	for i := 0; i < 5; i++ {
		time.Sleep(200 * time.Millisecond)
		t.Log("==========Hello==========")
	}

}

func TestParallelPrintWorld(t *testing.T) {
	t.Parallel()
	t.Log("number of runtime.GOMAXPROCS:", runtime.GOMAXPROCS(0))
	for i := 0; i < 5; i++ {
		time.Sleep(200 * time.Millisecond)
		t.Log("==========World==========")
	}
}

小结

  • 可以通过运行runtime.GOMAXPROCS(0)获得运行时的最大P的数量
  • 在功能测试中,无法避免依赖一些外部环境,比如数据库的连接,第三方接口的调用,应该对这些外部环境进行仿造(mock),从而得到一个纯净的测试环境
  • go help testflag 查看 flag 帮助文件

基准测试

是测量一个程序在固定工作负载下的性能,使用 -bench 标记可以对代码进行基准测试

go test -v -bench=. -run=^$ gott/hello
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-4         4964053               228.5 ns/op
PASS
ok      gott/hello      2.386s
  • .表示执行代码包中所有的基准测试用例(前缀为Benchmark的方法),由于功能测试也会被运行,但同时运行功能测试会影响性能测试的结果,通过加入-run=^$,来禁止功能测试与性能测试一起执行。^$意味着:只执行名称为空的功能测试函数,即不执行任何测试函数。

  • 指定运行某个基准测试

    go test -v -bench=BenchmarkHello gott/hello

    功能测试也会一起运行。。。。。。

b.N值的确定

func BenchmarkHello(b *testing.B) {
	for i := 0; i < b.N; i++ {
		hello("Max")
	}
}

当测试开始时,b.N的值被设置为1,执行后如果没有超过默认执行时间上限(默认为1秒),则加大b.N的值,按某种规则一直递增,直到执行时间等于或超过上限,那么就用这一次的b.N的值,做为测试的最终结果

BenchmarkHello-4         4964053               228.5 ns/op
PASS
ok      gott/hello      2.386s
  • BenchmarkHello-4表示执行 BenchmarkHello 时,所用的最大P的数量为4
  • 4964053: 表示hello()方法在达到这个执行次数时,等于或超过了1秒
  • 228.5 ns/op: 表示每次执行hello()所消耗的平均执行时间
  • 2.386s:表示测试总共用时

测试总时间的计算

既然4964053表示1秒或大于1秒时执行的次数,那么测试总时间用时却是2.386s,超出了不少,这是为什么呢

func BenchmarkHello(b *testing.B) {
	for i := 0; i < b.N; i++ {
		hello("Max")
	}
	b.Log("NNNNN:", b.N)
}

在测试中加入b.Log("NNNNN:", b.N),再执行基准测试,并加入-v,打印测试中的日志

go test -v -bench=. -run=^$ gott/hello
=== RUN   TestHello
    hello_test.go:15: hello Max
--- PASS: TestHello (0.00s)
=== RUN   TestPrint
    hello_test.go:19: just Print
--- PASS: TestPrint (0.00s)
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello
    hello_test.go:26: NNNNN: 1
    hello_test.go:26: NNNNN: 100
    hello_test.go:26: NNNNN: 10000
    hello_test.go:26: NNNNN: 1000000
    hello_test.go:26: NNNNN: 3541896
    hello_test.go:26: NNNNN: 4832275
BenchmarkHello-4         4832275               236.8 ns/op
PASS
ok      gott/hello      2.395s

可以看到b.Log("NNNNN:", b.N)被执行了6次,这证明了之前提到的,测试会对b.N依次递增,直到执行时间等于或超过上限。在对BenchmarkHello()运行基准测试时,N值依次按1,100,10000,1000000,3541896,4832275递增,直到执行次数为4832275时,执行时间等于或超过了上限。

同时也说明BenchmarkHello()一共被调用了6次,每次运行BenchmarkHello()都要消耗一定的时间,所以测试总耗时为这6次调用时间之和,2.395s,超过了1秒

平均执行时间的计算

应该用,运行4832275时所消耗的时间 tt / 4832275 = 236.8 ns/op

  • 如果用 测试总共用时 / 最多可以执行的次数 则不等于 平均执行时间,即 2.386 * (1000 ** 3) / 4832275 = 493.7 大于测试结果中的236.8 ns/op
  • 如果用 1 秒 / 4832275 = 206 ns ,与236.8 ns/op并不是很接近
  • 如果把尝试过程中的运行次数也加入进来total = 1 + 100 + 10000 + 1000000 + 3541896 + 4832275,即2.386 * (1000 ** 3) / 9384272 = 254.25236.8 ns/op接近
  • 反推运行时间:4832275 * 236.8 ns / 1000 ** 3 = 1.14s ,测试结果使用了运行时间超过1秒上限时的数值

问题:是否测试总时间一定会超过1秒?答:因为要找到最大可执行次数,而在这之前肯定要进行多次尝试,所以测试总时间应该总是会超过1秒的。

benchtime 标记

可以通过 -benchtime标记修改默认时间上限,比如改为3秒

go test -v -bench=. -benchtime=3s -run=^$ gott/hello
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello
    hello_test.go:31: NNNNN: 1
    hello_test.go:32: /Users/ga/m/opt/go/go_root
    hello_test.go:31: NNNNN: 100
    hello_test.go:32: /Users/ga/m/opt/go/go_root
    hello_test.go:31: NNNNN: 10000
    hello_test.go:32: /Users/ga/m/opt/go/go_root
    hello_test.go:31: NNNNN: 1000000
    hello_test.go:32: /Users/ga/m/opt/go/go_root
    hello_test.go:31: NNNNN: 15927812
    hello_test.go:32: /Users/ga/m/opt/go/go_root
BenchmarkHello-4   	15927812	       223.4 ns/op
PASS
ok  	gott/hello	3.802s

还可以设置具体的探索次数最大值,格式为-benchtime=Nx

go test gott/hello -run=^$ -bench=BenchmarkHello -benchtime=50x
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-4              50              2183 ns/op
--- BENCH: BenchmarkHello-4
    hello_test.go:35: NNNNN: 1
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 50
    hello_test.go:36: /Users/ga/m/opt/go/go_root
PASS
ok      gott/hello      0.011s

b.N的值被设置为50,函数运行了50次

benchmem 标记

可以通过-benchmem标记查看内存使用信息

go test gott/hello -run=^$ -bench=BenchmarkHello -benchmem
goos: darwin
goarch: amd64
pkg: gott/hello
BenchmarkHello-4         5137456               223.1 ns/op            32 B/op          2 allocs/op
--- BENCH: BenchmarkHello-4
    hello_test.go:35: NNNNN: 1
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 100
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 10000
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 1000000
    hello_test.go:36: /Users/ga/m/opt/go/go_root
    hello_test.go:35: NNNNN: 5137456
    hello_test.go:36: /Users/ga/m/opt/go/go_root
        ... [output truncated]
PASS
ok      gott/hello      1.399s
  • 32 B/op:平均每次迭代内存分配的字节数
  • 2 allocs/op:平均每次迭代内存分配的次数

平均每次迭代计算的依据应该使用的是 b.N=5137456迭代次数

基准测试的用途

一般用于对比两个不同的操作所消耗的时间,如

  • 渐近增长函数的运行时间

    一个函数需要1ms处理1,000个元素,处理10000或1百万将需要多少时间呢

  • I/O缓存该设置为多大

    基准测试可以帮助我们选择在性能达标情况下所需的最小内存

  • 确定哪种算法更好

比较型的基准测试代码

func benchmark(b *testing.B, size int) { /* ... */ }
func Benchmark10(b *testing.B)         { benchmark(b, 10) }
func Benchmark100(b *testing.B)        { benchmark(b, 100) }
func Benchmark1000(b *testing.B)       { benchmark(b, 1000) }

通过参数size来控制输入的大小,而不是直接修改b.N的值,除非你只是想知道一个固定大小的迭代的耗时,否则基准测试的结果将毫无意义

缓存目录

Go 总是会缓存程序构建的结果,以便在将来使用(可以加速构建速度)。当有任何变动时,缓存就会失效,构建过程会真正的被执行。被缓存的构建结果保存在go env GOCACHE目录中,为了防止目录中的数据越来越多,go会自动删除不经常使用的缓存文件。可以手动清除缓存结果,执行go clean -cache即可。

go test命令也会把测试成功的结果缓存起来,如果测试代码和源代码没有改动,再次运行测试时,直接使用缓存的结果。当源码和测试代码有改动时,缓存结果就会失效,测试会被真正的运行。运行go clean -testcache可以删除测试的缓存结果,但不会删除构建结果缓存

测试日志打印

可以使用t.Logt.Logf方法打印测试日志,这两个方法会在测试失败时,进行打印,在测试成功的时候,是不进行打印的。如果想在测试结果中看到所有的日志,可以使用-v参数

func TestIntroduce(t *testing.T) {
	intro := introduce()
	expected := "Welcome to my Golang column."
	if intro != expected {
		t.Errorf("The actual introduce %q is not the expected.",
			intro)
	}
	// 默认只在测试失败的时候,才打印日志内容
	t.Logf("The expected introduce is %q.\n", expected)
}

// 测试失败,显示t.Logf()中的内容
--- FAIL: TestIntroduce (0.00s)
    demo53_test.go:41: The actual introduce "Welcome to my Golang column." is not the expected.
    demo53_test.go:44: The expected introduce is "Welcome to my golang column.".
FAIL
FAIL	puzzlers/article20/q2	0.013s
FAIL

t.Fail()t.FailNow()

t.Fail()令测试结果为失败,但其后的代码依然会被执行。go 语言测试用例失败的触发必须手动调用t.Fail/t.FailNow/t.Errorf/t.Fatalf等方法才可以,并不像其它语言支持测试断言,由断言失败触发。

func TestFail(t *testing.T) {
	t.Fail()
	t.Log("Failed.") // 可以被执行到
}

t.FailNow()也会令测试结果为失败,但其后的代码不再执行,不会影响其它测试用例的执行

func TestFail(t *testing.T) {
	t.FailNow()
	t.Log("Failed") // 不能被执行到
}

t.Errorf()t.Error()

打印日志并使测试失败,等效于调用t.Logf/t.Log后,又调用了t.Fail

// t.Error 等效于在调用 t.Log 后,接着又调用了 t.Fail
// Error is equivalent to Log followed by Fail.
func (c *common) Error(args ...interface{}) {
	c.log(fmt.Sprintln(args...))
	c.Fail()
}

// Errorf is equivalent to Logf followed by Fail.
func (c *common) Errorf(format string, args ...interface{}) {
	c.log(fmt.Sprintf(format, args...))
	c.Fail()
}

t.Fatalt.Fatalf方法

打印日志并使测试失败,在其后的代码不会被执行,当前测试用例立即结束,但不会影响其它测试用例,等效于调用t.Logf/t.Log后,又调用了t.FailNow

// Fatal is equivalent to Log followed by FailNow.
func (c *common) Fatal(args ...interface{}) {
	c.log(fmt.Sprintln(args...))
	c.FailNow()
}

// Fatalf is equivalent to Logf followed by FailNow.
func (c *common) Fatalf(format string, args ...interface{}) {
	c.log(fmt.Sprintf(format, args...))
	c.FailNow()
}

覆盖率

语句的覆盖率是指在测试中至少被运行一次的代码占总代码数的比例。

生成测试报告

在生成报告之前,要确保所有的测试都正常通过。使用go test命令配合不同的参数标示可以生成不同类型的覆盖率分析报告

使用标志coverprofile

go test gott/prime -coverprofile=c.out
ok      gott/prime      0.013s  coverage: 93.8% of statements

在运行每个测试前,会把参与测试的源代码拷贝一份,并对每个词法块插入一个布尔变量,来统计代码块在测试中是否被执行过,以此来统计代码覆盖率,统计日志数据写入c.out文件

go 语言 测试覆盖率

使用标志covermode

如果同时使用了-covermode=count,会在每个代码块插入计数器以统计代码块被执行的次数,用这个功能可以看到哪些代码是频繁执行的热点代码

go test gott/prime -coverprofile=c.out -covermode=count
ok      gott/prime      0.015s  coverage: 93.8% of statements

go 语言 测试覆盖率

查看测试报告

go tool cover -html=c.out

运行后会自动在浏览器中打开,绿色的代码块代表被测试覆盖到了,红色的则表示没有被覆盖到。如果使用了 -covermode=count标志,会用红、灰、绿 三种颜色表示代码被调用的频率,红色表示没有调用,灰色表示频率较低,绿色随颜色深浅表示不同程度的频率调用。具体可见上图测试报告。

源文件

// primes_test.go
func TestPrimes(t *testing.T) {
	max := 1000
	primes := GetPrimes(max)
	t.Log(primes)
}

// primes.go
// 查找质数
func GetPrimes(max int) []int {
	if max <= 1 {
		return []int{}
	}
	marks := make([]bool, max)
	var count int
	squareRoot := int(math.Sqrt(float64(max)))
	for i := 2; i <= squareRoot; i++ {
		if !marks[i] {
			for j := i * i; j < max; j += i {
				if !marks[j] {
					marks[j] = true
					count++
				}
			}
		}
	}
	primes := make([]int, 0, max-count)
	for i := 2; i < max; i++ {
		if !marks[i] {
			primes = append(primes, i)
		}
	}
	return primes
}

小结

  • 测试不可能是完整的,计算机科学家Edsger Dijkstra曾说过:“测试能证明缺陷存在,而无法证明没有缺陷。”
  • 实现100%的测试覆盖率听起来很美,但是在具体实践中通常是不可行的,也不是值得推荐的做法。应该对更需要测试的地方添加测试代码,而不是一味的为每个方法都加入测试代码
  • 测试覆盖率工具可以帮助我们快速识别测试薄弱的地方

设置gzip_http_version

使用ab 测试网站,参数如下

ab -n 50 -c 10 -H "Accept-Encoding: gzip, deflate" https://example.com/

发现返回的文档没有被压缩,查看nginx日志也显示返回的是原始大小,查询资料后发现是nginx 配置gzip_http版本问题

gzip on;

gzip_vary on;
gzip_min_length 1k;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1

gzip_http_version 设置为了1.1,而 ab只支持http_version:1.0,改为1.0即可。

gzip_http_version 1.0

再次测试,返回数据是被压缩后的数据,从nginx日志也可以看到数据是压缩后的。类似的ab 的工具还有siege,可以很好的支持http 1.1

小结

nginx配置中,有些属性如果没有显示的进行设置,比如设置gzip on;时,没有设置gzip_http_versionnginx会启用默认值 gzip_http_version:1.1;,所以当你不想限制http_version的最低版本时,仅仅把gzip_http_version注释掉是不行的,你必须给其赋一个值才可以。类似这种问题不光在nginx配置中需要注意,在其它软件配置文件中也要注意

外部测试包

在测试代码的时候,会遇到包循环依赖导入的问题,Go语言规范是禁止包的循环依赖,为了解决这个问题,可以引入外部包。

外部包使用package xxx_test方式来命名,比如package bytes_test就是src/btyes/buffer外部包的命名方式,_test后缀告诉go test工具它应该建立一个额外的包来运行测试。

package xxx_test中的xxx_test并不需要真的创建这样一个目录,而是在xxx包下的测试文件中引入这个包名就可以了。btyes包的源码目录结构如下

/src/bytes
├── boundary_test.go
├── buffer.go
├── buffer_test.go
├── bytes.go
├── bytes_test.go
├── compare_test.go
├── example_test.go
├── export_test.go
├── reader.go
└── reader_test.go

通过外部测试包的方式可以解决导入包循环依赖的问题,因为外部测试包是一个独立的包,所以能够导入那些依赖待测代码本身的其他辅助包;包内的测试代码就无法做到这点。注意:外部包不能被其它包导入

// 文件 src/btyes/buffer_test.go
package bytes_test // 'btyes目录下并不存在 bytes_test 目录'
import (
	. "bytes"
	... 代码片段
)

func TestNewBuffer(t *testing.T) {
	buf := NewBuffer(testBytes)
	check(t, "NewBuffer", buf, testString)
}

... 代码片段
func empty(t *testing.T, testname string, buf *Buffer, s string, fub []byte) {}

go list 命令

  • 通过GoFiles查看bytes包的Go源文件

    go list -f={{.GoFiles}} bytes // => [buffer.go bytes.go reader.go]
  • 通过TestGoFiles查看bytes包内部的测试代码

    go list -f={{.TestGoFiles}} bytes // => [export_test.go]
  • 通过XTestGoFiles查看bytes包的外部测试代码,也就是bytes_test包

    go list -f={{.XTestGoFiles}} bytes
    // => [buffer_test.go bytes_test.go compare_test.go example_test.go reader_test.go]

访问内部代码

如果在测试中,需要对包内部的没有导出的函数进行测试,可以利用包内的 _test.go文件,如 export_test.go,在这个文件中将包的内部函数、方法导出,以供外部测试包使用。

indexBytePortable方法在 src/bytes/bytes.go 中定义

// src/bytes/bytes.go
func indexBytePortable(s []byte, c byte) int {/* ... */} // 这是一个内部方法

bytes包中的内部方法导出,供外部包package bytes_test使用。因为只有在测试时,才会把内部代码导出,所以导出内部代码是安全的

// src/bytes/export_test.go
package bytes
// Export func for testing
var IndexBytePortable = indexBytePortable // 赋值给包级可导出变量 IndexBytePortable

外部包使用导出的方法IndexBytePortable

// src/bytes/bytes_test.go
package bytes_test

func TestIndexByte(t *testing.T) {
	for _, tt := range indexTests {
		... 代码片段
		posp := IndexBytePortable(a, b) // 导出的内部方法在这里被使用
		if posp != tt.i {
			t.Errorf(`indexBytePortable(%q, '%c') = %v`, tt.a, b, posp)
		}
	}
}

测试文件export_test.go并没有定义测试代码,它只是通过bytes.IndexBytePortable简单导出了内部的indexBytePortable函数,这个技巧可以广泛用于位于外部测试包的白盒测试

示例:包循环依赖

目录结构

gott
├── go.mod
├── hi
│   └── hi.go
├── pprint
│   ├── pprint.go
│   └── pprint_test.go

hi.go 文件,引用了 pprint 包

package hi

import (
	"fmt"
	"gott/pprint"
)

func Say() {
	pprint := pprint.PPrint()
	fmt.Println(pprint)
}

pprint.go 文件

package pprint

func PPrint() string {
	return "I'm PPrint()"
}

pprint_test.go 测试文件,在 pprint 包中引入了 hi 包,而 hi 包中已经引入了 pprint 包,这就导致了 包循环依赖

package pprint

import (
	"gott/hi"
	"testing"
)

func TestPPrint(t *testing.T) {
	PPrint()
	hi.Say()
	t.Log("expect call PPrint")
}

执行测试,提示 import cycle not allowed in test

go test -v gott/pprint
# gott/pprint
package gott/pprint (test)
        imports gott/hi
        imports gott/pprint: import cycle not allowed in test
FAIL    gott/pprint [setup failed]
FAIL

修改 pprint_test.go 测试文件,使用外部测试包 pprint_test

package pprint_test

import (
	"gott/hi"
	// 导入 要进行测试的 pprint 包本身
	"gott/pprint"
	"testing"
)

func TestPPrint(t *testing.T) {
	pprint.PPrint()
	hi.Say()
	t.Log("expect call PPrint")
}

"gott/pprint":导入 要进行测试的 pprint 包本身,没有引入外部包时,不需要导入被测试的包本身

执行测试,测试通过

go test -v gott/pprint
=== RUN   TestPPrint
I'm PPrint()
    pprint_test.go:12: expect call PPrint
--- PASS: TestPPrint (0.00s)
PASS
ok      gott/pprint     (cached

使用 Go 官方的代码风格:pprint_test.go 文件,因为pprint_test在 pprint 目录下,通过在 import 时,使用 . 选项,可以直接调用PPrint()方法,这使得调用包内的方法和内部测试包一致,整体代码风格得到统一

package pprint_test

import (
	"gott/hi"
	. "gott/pprint"
	"testing"
)

func TestPPrint(t *testing.T) {
	PPrint()
	hi.Say()
	t.Log("expect call PPrint")
}

favicon图标不显示

发现博客的favicon图标一直不能在safari 的标签栏上显示,只能显示在地址栏,经过一番研究后发现是Hexo 的 Next主题的设置不正确造成的

favicon:
  small: /favicon-16x16.png
  medium: /favicon-32x32.png
  apple_touch_icon: /apple-touch-icon.png

  # 不能把 safari_pinned_tab 注释掉,只能留空,表示不启用,
  # 注释掉会默认使用 next 主题中的设置.

  # 注意: <link rel="mask-icon" href="logo.svg"> 会覆盖
  # <link rel="icon" href="favicon.png"> 导致safari favicon图标被替换为logo.svg
  safari_pinned_tab:

smallmedium会生成两个与favicon相关的设置,用来在地址栏与标签栏显示网站图标

<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">

safari_pinned_tab会生成<link rel="mask-icon" href="logo.svg">,它会覆盖标签栏上的favicon图标,由于在设置这个属性的时候,错误的使用了非svg文件(用的是png)导致了图标不能正确显示。

解决方法

设置正确的svg文件,或者就不设置<link rel="mask-icon">属性,只使用<link rel="icon" >设置favicon图标即可。如果不设置<link rel="mask-icon">属性,需要在配置文件中把safari_pinned_tab的值留空,而不是不设置或注释掉,这样做会使用next 主题中的默认设置,造成favicon图标显示为Next官网的图标

关于safari缓存图标的问题

safari会对图标按域名进行缓存,从而造成更新图标后不能马上更新显示的问题,需要对缓存的图标进行清理。

  • 首先要清理它的缓存:setting->privacy->Manage Website Date,搜索相关域名,删除即可。
  • 磁盘缓存文件:~/Library/Safari目录下,删除Template Icons文件夹,如果不起作用,删除Favicon CacheTouch Icons Cache文件夹

文件描述符

file descriptor,简称FD ,用于指代被打开的文件,用一个非负的整数表示

对文件描述符的误区

每个程序启动的时候,都会打开3个文件,分别是,0标准输入,1标准输出,2标准错误,此后再打开文件的文件描述符就是3,依次类推。之前的误区是,当在进程中打开一个文件,它的文件描述符应该已经很大了,不应该是3,因为系统本身已经运行了很多程序。正确的应该是:进程只能看到自己的文件描述符,每个进程的文件描述符的编号都是从0开始,进程启动,默认都会打开标准输入,标准输出,标准错误这三个文件,之后再打开的文件的描述符从编号3开始

文件件描述符列表

每个进程都有着自己的文件描述符列表。进程A启动后,拥有stdin,stdout,stderr这三个文件描述符 ,再打开一个文件,这个文件的文件描述符就是3。进程B启动后,拥有stdin,stdout,stderr这三个文件描述符,再打一个文件,它的文件描述符也是3,和进程A的一样。所以,不同进程,可以拥有相同的文件描述符。

当一个文件在同一个进程中,被打开多次,那么这个文件具有不同的文件描述符,在同一个进程中,每个文件描述符只能对应一个文件。同一个进程,不同文件描述符可以指向同一个文件。

不同进程,如果都打开了同一个文件(文件描述符都指向同一个文件),那么它们都具有这个文件的句柄,其中一个进程对文件的修改,对其它进程是可见的。比如进程A对文件写入了10个字节,进程B在对文件进行写入时,是从第11个字节处开始写入的,而不是从文件头开始写入,这些操作包括read(),write(),seek()

文件描述符限制

内核为了不让某个进程消耗掉所有的文件资源,会对单个进程最大打开文件数做默认值处理(称之为用户级限制),默认值一般是1024。注意:同一个文件,可以有多个文件句柄,这可能是不同进程打开了同一个文件,或者是同一个进程内,对一个文件打开了多次

截取0长度的slice

func main() {
	s1 := []int{1, 2, 3, 4, 5, 6}
	s2 := s1[1:1]

	fmt.Println(cap(s2), len(s2), s2) // 5 0 []

	s2 = s1[6:6]
	fmt.Println(cap(s2), len(s2), s2) // 0 0 []

	s2 = s1[1:2]
	fmt.Println(cap(s2), len(s2), s2) // 5 1 [2]
}

s1[1:1]表示截取的位置从第一个元素最后一个字节的后面开始,到第一个元素最后一个字节的后面结束,所以生成的slice的长度是0,容量是5(6-1)

s1[6:6]表示截取的位置从最后一个元素最后一个字节的后面开始,到最后一个元素最后一个字节的后面结束,所以生成的slice的长度是0,容量是0(6-6)

s1[1:2]表示截取的位置从第一个元素最后一个字节的后面开始,到第二个元素最后一个字节的后面结束,在这个范围内的元素只有 2,所以生成的slice的长度是1,容量是5(1-6)