代码规范

辅助工具

goimports

goimports是Go官方提供的工具,能够为我们自动格式化Go语言代码并对引入的包进行管理,其中包括自动增删依赖包引用,将依赖包进行分类排序。

goimports等价于gofmt加依赖包的管理。

因此建议所有GO语言开发人员在开发时,使用goimports进行格式化,并在IDE中进行配置,保存时自动格式化。

在Goland中进行设置

golint

golint是Go官方提供的静态检查工具,该工具的诟病在于可定制化不足,但是在Go的社区中,保证一致性的编程规范是有益的事情,因此该工具大多数用在基础库和框架项目中,上层业务应用可以选用golangci-lint这个工具进行静态检查。

建议所有Go语言开发人员在项目中加入golint或golangci-lint工具进行静态检查,甚至于对于基础库和框架来说,可以同时使用上述两个工具来进行检查。

自动化

无论是上述的格式化还是静态检查,我们都要在我们的CI流程中进行自动化的处理,从而减少代码审核人员的工作,将重心放在代码逻辑上。

Drone结合上述工具进行自动化的示例。

项目开发

目录结构

在项目开发中,Go官方并没有给出一个推荐的目录规划,而在社区中有一些常见的约定,具体的可以看golang-standards/project-layout ,我们也是建议采用该方式进行项目目录规划。

├── LICENSE
├── Makefile
├── README.md
├── api
├── assets
├── build
├── cmd
├── configs
├── deployments
├── docs
├── examples
├── githooks
├── init
├── internal
├── pkg
├── scripts
├── test
├── third_party
├── tools
├── vendor
├── web
└── website

下面介绍常见并且重要的目录及文件。

README.md

该文件写明该项目的简介,功能说明等。

CONTRIBUTING.md

该文件说明其他开发人员如何合作开发该项目。

/pkg

该目录存放的是项目中可以被外部项目或者本项目使用的代码仓库,外部项目直接通过import即可引入该包代码。

/internal

该目录存放的是本项目中的私有代码包,不对外提供。该目录可以和pkg目录合并在一起。

/cmd

该目录存放的是当前项目中的可执行程序入口,即main入口。

/api

该目录存放的是当前项目对外提供的各种RESTful api接口定义文件,包含grpc的protobuf等文件。

/scripts

该目录放置当前项目中各种脚本文件。

Makefile

该文件用于编译、测试等操作的入口,建议所有/scripts下的脚本文件均由Makefile触发。

注意: /src不要使用该目录,该目录与$GOPATH/src类似,会导致其他项目引用本项目出现歧义。

模块划分

Go语言推荐按职责进行模块划分,不同于MVC的这种按层来分,这种划分的方式,可以使得项目中模块在拆分成微服务的时候,直接把某一模块目录拆分出来,不需要过多处理。而按MVC的方式划分模块,很容易出现循环依赖。

显示声明

Go语言社区建议我们在开发的时候,显式的初始化、调用和错误处理。

init

init建议只做一些简单的条件判断,不要掺杂复杂的逻辑及一些数据库或者tcp初始化。

var grpcClient *grpc.Client

func init() {
    var err error
    grpcClient, err = grpc.Dial(...)
    if err != nil {
        panic(err)
    }
}

func GetPost(postID int64) (*Post, error) {
    post, err := grpcClient.FindPost(context.Background(), &pb.FindPostRequest{PostID: postID})
    if err != nil {
        return nil, err
    }

    return post, nil
}

上述代码虽然可以正常工作,但是在这里init函数隐式地初始化了grpc的连接,如果该包被引用的话,这个隐式初始化可能会导致引入这个依赖的项目在遇到错误时很迷茫。

合理的做法是,我们定义一个Client结构体和用于初始化这个结构体的函数NewClient,这个函数返回grpc的连接。

type Client struct {
    grpcClient *grpc.ClientConn
}

func NewClient(grpcClient *grpcClientConn) Client {
    return &Client{
        grpcClient: grpcClient,
    }
}

func (c *Client) GetPost(postID int64) (*Post, error) {
    post, err := c.grpcClient.FindPost(context.Background(), &pb.FindPostRequest{PostID: postID})
    if err != nil {
        return nil, err
    }

    return post, nil
}

在其他项目引用该包的时候,可以按下面的方式进行。

func main() {
    grpcClient, err := grpc.Dial(...)
    if err != nil {
        panic(err)
    }

    postClient := post.NewClient(grpcClient)
    // ...
}

error

建议在处理错误逻辑的时候,对于所有可能发生的错误进行显式处理,对于一个方法或者函数是否需要返回error给上层,也需要我们进行思考再决定。