# go项目基本要素的个人之见

看到一条推文

> hate 所有更新版本后不写 changelog 或者 release note 的软件或者库

催生了本篇博文的产生，结合笔者过往的开发经验，想讨论一下关于在公司内参与开发go(其它项目也基本适合)项目的基本要素。

## **CHANGELOG.md**

人的记忆是不可靠，也不可能每次都去翻代码看看某次版本增加的功能、修复的问题、优化、库升级等，所以changelog是有必要的，至少可以成为项目版本迭代信息的一个参考和指引

一个示例如下

```go
# Change Log
All notable changes to this project will be documented in this file.
​
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
​
## [0.9.0]
### Changed
- 更新版本号
​
### Removed
- 删除deadcode
​
### Fixed
- 修复XX问题
```

其实并不用写太多，在多人协作的时候，每个人写自己负责的那一部分即可，要做到这一点，其实并难，难的是建立起编写changelog的整体意识。

## **golangci-lint**

> `golangci-lint` is a fast Go linters runner. It runs linters in parallel, uses caching, supports `yaml` config, has integrations with all major IDE and has dozens of linters included.

笔者曾经跟一个同事说过一句话：lint 是多人协作编程的基础之一，决定项目的下限。现在想想，这句话真的讲的蛮好的。其实笔者也不知何时开始重视静态检查的，印象中大概是早年参与TiDB的十分钟成为contributor活动那会开始的吧，这个开源项目的静态检查可是很棒的，同时也是我参与开源的启蒙(当年因参与贡献而收到的马克杯用到现在)。

下面会举一个静态检查的例子，目录结构如下：

```go
.
├── .golangci.yml
├── go.mod
└── main.go
```

变量`unused` 没有地方在用，属于deadcode，也即死代码

```go
package main
​
var unused int
​
func main() {
  println("deadcode example")
}
```

检查deadcode的.golangci.yml配置如下

```go
linters:
  # Disable all linters.
  # Default: false
  disable-all: true
  # Enable specific linter
  # https://golangci-lint.run/usage/linters/#enabled-by-default
  enable:
    - deadcode
```

执行lint

```go
golangci-lint run -v
```

输出

```go
main.go:3:5: `unused` is unused (deadcode)
var unused int
    ^
INFO File cache stats: 1 entries of total size 75B 
INFO Memory: 4 samples, avg is 31.6MB, max is 31.6MB 
INFO Execution took 247.852683ms 
```

可以检测出unused变量未被使用，属于deadcode。如果出于一些原因想让`unused`变量跳过deadcode检查，只需增加一个注释即可，如下所示

```go
package main
​
//nolint:deadcode
var unused int
​
func main() {
  println("deadcode example")
}
```

当然，还有其它用法，可以参与golangci的官方使用文档

## **Dockerfile**

笔者一直坚持Dockerfile应该放置于项目repo中，所以在贵司看到Dockerfile放置在Jenkins的agent节点上时，冲击不可谓不大，这些细节处理不足容易引起混乱。

改造前

* jenkins节点上事先安装好需要的golang版本，去到指定的环境目录提前放置好Dockerfile，go build, docker build, 然后docker push
    
    ```go
    FROM alpine:3.18.0
    WORKDIR /workspace
    COPY app /workspace/app
    ```
    

改造后

* Dockerfile放在项目repo中，利用docker的multi-stage，保持了编译环境的纯净，也方便go升级，使得编译不再依赖jenkins节点，以下是一个示例
    
    ```go
    FROM golang:1.20.5-alpine3.18 as builder
    ​
    WORKDIR /workspace
    # Copy the Go Modules manifests
    COPY go.mod go.mod
    COPY go.sum go.sum
    ​
    RUN go env -w GO111MODULE=on && \
        go env -w GOPROXY=https://goproxy.cn,direct && \
        go mod download
    ​
    # Copy the go source
    ​COPY main.go main.go
    
    # Build
    RUN CGO_ENABLED=0 GOOS=linux go build -a -o app main.go
    ​
    FROM busybox:1.35.0-glibc
    COPY --from=builder /workspace/app /workspace/app
    ```
    

## **Makefile**

这算是一个可选项吧，通常我会在makefile中封装一些常用命令，比如make lint就可以完成项目的lint，比执行golangci-lint run还是要简短些的，非常方便。另外，笔者在快速部署项目中也使用了makefile，效果也很好，以下是一个简单的示例

```makefile
.PHONY: lint
lint:
	golangci-lint run
  
.PHONY: build
build:
	go build -a -o app main.go
  
.PHONY: dev
dev:
	go run main.go
```

## **README.md**

公司内的项目，能够讲清楚项目是干什么的，如何启动就已经不错了，倒不用有多复杂 ，因为一般就是给新人看的，如果能描述清楚项目背景、设计、功能、部署及注意事项等，那就更好了。

## 总结

代码格式化、go mod tidy这些已经不能算是要素了，而应该成为一种本能，但是笔者在从业过程中，也发现好多项目连上述两点也做不到。如果无法决定项目的上限，至少可以遵循一些基本原则守住下限，共勉。

参考：

* [https://keepachangelog.com/zh-CN/1.0.0/](https://keepachangelog.com/zh-CN/1.0.0/)
    
* [https://docs.docker.com/build/building/multi-stage/](https://docs.docker.com/build/building/multi-stage/)
    

* [https://seisman.github.io/how-to-write-makefile/overview.html](https://seisman.github.io/how-to-write-makefile/overview.html)
    
* [https://github.com/golang/example/blob/master/README.md](https://github.com/golang/example/blob/master/README.md)
