第三章:项目管理

1 依赖管理:包导入很重要的 8 个知识点

1.1 单行导入与多行导入

在 Go 语言中,一个包可包含多个 .go 文件(这些文件必须得在同一级文件夹中),只要这些 .go 文件的头部都使用 package 关键字声明了同一个包。
导入包主要可分为两种方式:

  • 单行导入
import "fmt"
import "sync"
  • 多行导入
import(
    "fmt"
    "sync"
)

如你所见,Go 语言中 导入的包,必须得用双引号包含,在这里吐槽一下。

1.2 使用别名

在一些场景下,我们可能需要对导入的包进行重新命名,比如

  • 我们导入了两个具有同一包名的包时产生冲突,此时这里为其中一个包定义别名
import (
    "crypto/rand"
    mrand "math/rand" // 将名称替换为mrand避免冲突
)
  • 我们导入了一个名字很长的包,为了避免后面都写这么长串的包名,可以这样定义别名
import hw "helloworldtestmodule"
  • 防止导入的包名和本地的变量发生冲突,比如 path 这个很常用的变量名和导入的标准包冲突。
import pathpkg "path"

1.3 使用点操作

如里在我们程序内部里频繁使用了一个工具包,比如 fmt,那每次使用它的打印函数打印时,都要 包名+方法名。
对于这种使用高频的包,可以在导入的时,就把它定义会 “自己人”(方法是使用一个 . ),自己人的话,不分彼此,它的方法,就是我们的方法。
从此,我们打印再也不用加 fmt 了。

import . "fmt"

func main() {
    Println("hello, world")
}

但这种用法,会有一定的隐患,就是导入的包里可能有函数,会和我们自己的函数发生冲突。

1.4 包的初始化

每个包都允许有一个 init 函数,当这个包被导入时,会执行该包的这个 init 函数,做一些初始化任务。
对于 init 函数的执行有几点需要注意

  1. init 函数优先于 main 函数执行
  2. 在一个包引用链中,包的初始化是深度优先的。比如,有这样一个包引用关系:main→A→B→C,那么初始化顺序为
C.init→B.init→A.init→main
  1. 同一个包甚至同一个源文件,可以有多个 init 函数
  2. init 函数不能有入参和返回值
  3. init 函数不能被其他函数调用
  4. 同一个包内的多个 init 顺序是不受保证的
  5. 在 init 之前,其实会先初始化包作用域的常量和变量(常量优先于变量),具体可参考如下代码
package main

import "fmt"

func init()  {
 fmt.Println("init1:", a)
}

func init()  {
 fmt.Println("init2:", a)
}

var a = 10
const b = 100

func main() {
 fmt.Println("main:", a)
}
// 执行结果
// init1: 10
// init2: 10
// main: 10

1.5 包的匿名导入

当我们导入一个包时,如果这个包没有被使用到,在编译时,是会报错的。
但是有些情况下,我们导入一个包,只想执行包里的 init 函数,来运行一些初始化任务,此时怎么办呢?
可以使用匿名导入,用法如下,其中下划线为空白标识符,并不能被访问

// 注册一个PNG decoder
import _ "image/png"

由于导入时,会执行 init 函数,所以编译时,仍然会将这个包编译到可执行文件中。

1.6 导入的是路径还是包?

当我们使用 import 导入 testmodule/foo 时,初学者,经常会问,这个 foo 到底是一个包呢,还是只是包所在目录名?

import "testmodule/foo"

为了得出这个结论,专门做了个试验(请看「第七点里的代码示例」),最后得出的结论是:

  • 导入时,是按照目录导入。导入目录后,可以使用这个目录下的所有包。
  • 出于习惯,包名和目录名通常会设置成一样,所以会让你有一种你导入的是包的错觉。

1.7 相对导入和绝对导入

据我了解在 Go 1.10 之前,好像是不支持相对导入的,在 Go 1.10 之后才可以。
绝对导入:从 $GOPATH/src 或 $GOROOT 或者 $GOPATH/pkg/mod 目录下搜索包并导入
相对导入:从当前目录中搜索包并开始导入。就像下面这样

import (
    "./module1"
    "../module2"
    "../../module3"
    "../module4/module5"
)

分别举个例子吧
一、使用绝对导入
有如下这样的目录结构(注意确保当前目录在 GOPATH 下)
image.png
其中 main.go 是这样的

package main

import (
    "app/utilset"   // 这种使用的就是绝对路径导入
)

func main() {
    utils.PrintHello()
}

而在 main.go 的同级目录下,还有另外一个文件夹 utilset ,为了让你理解 「第六点:import 导入的是路径而不是包」,我在 utilset 目录下定义了一个 hello.go 文件,这个go文件定义所属包为 utils。

package utils

import "fmt"

func PrintHello(){
    fmt.Println("Hello, 我在 utilset 目录下的 utils 包里")
}

运行结果如下
image.png
二、使用相对导入
还是上面的代码,将绝对导入改为相对导入后
将 GOPATH 路径设置回去(请对比上面使用绝对路径的 GOPATH)
image.png
然后再次运行
image.png
总结一下,使用相对导入,有两点需要注意

  • 项目不要放在 $GOPATH/src 下,否则会报错(比如我修改当前项目目录为GOPATH后,运行就会报错)image.png
  • Go Modules 不支持相对导入,在你开启 GO111MODULE 后,无法使用相对导入。

最后,不得不说的是:使用相对导入的方式,项目可读性会大打折扣,不利用开发者理清整个引用关系。
所以一般更推荐使用绝对引用的方式。使用绝对引用的话,又要谈及优先级了

1.8 包导入路径优先级

前面一节,介绍了三种不同的包依赖管理方案,不同的管理模式,存放包的路径可能都不一样,有的可以将包放在 GOPATH 下,有的可以将包放在 vendor 下,还有些包是内置包放在 GOROOT 下。
那么问题就来了,如果在这三个不同的路径下,有一个相同包名但是版本不同的包,我们导入的时候,是选择哪个进行导入呢?
这就需要我们搞懂,在 Golang 中包搜索路径优先级是怎样的?
这时候就需要区分,是使用哪种模式进行包的管理的。
如果使用 govendor
当我们导入一个包时,它会:

  1. 先从项目根目录的 vendor 目录中查找
  2. 最后从 $GOROOT/src 目录下查找
  3. 然后从 $GOPATH/src 目录下查找
  4. 都找不到的话,就报错。

为了验证这个过程,我在创建中创建一个 vendor 目录后,就开启了 vendor 模式了,我在 main.go 中随便导入一个包 pkg,由于这个包是我随便指定的,当然会找不到,找不到就会报错, Golang 会在报错信息中打印中搜索的过程,从这个信息中,就可以看到 Golang 的包查找优先级了。
image.png
如果使用 go modules
你导入的包如果有域名,都会先在 $GOPATH/pkg/mod 下查找,找不到就连网去该网站上寻找,找不到或者找到的不是一个包,则报错。
而如果你导入的包没有域名(比如 “fmt”这种),就只会到 $GOROOT 里查找。
还有一点很重要,当你的项目下有 vendor 目录时,不管你的包有没有域名,都只会在 vendor 目录中想找。
image.png
通常vendor 目录是通过 go mod vendor 命令生成的,这个命令会将项目依赖全部打包到你的项目目录下的 verdor 文件夹中。

2 依赖管理:超详细解读 Go Modules 应用

在以前,Go 语言的的包依赖管理一直都被大家所诟病,Go官方也在一直在努力为开发者提供更方便易用的包管理方案,从最初的 GOPATH 到 GO VENDOR,再到最新的 GO Modules,虽然走了不少的弯路,但最终还是拿出了 Go Modules 这样像样的解决方案。
目前最主流的包依赖管理方式是使用官方推荐的 Go Modules ,这不前段时间 Go 1.14 版本发布,官方正式放话,强烈推荐你使用 Go Modules,并且有自信可以用于生产中。
本文会大篇幅的讲解 Go Modules 的使用,但是在那之前,我仍然会简要介绍一下前两个解决方案 GOPATH 和 go vendor 到底是怎么回事?我认为这是有必要的,因为只有了解它的发展历程,才能知道 Go Modules 的到来是有多么的不容易,多么的意义非凡。

2.1 最古老的 GOPATH

GOPATH 应该很多人都很眼熟了,之前在配置环境的时候,都配置过吧?
你可以将其理解为工作目录,在这个工作目录下,通常有如下的目录结构
image.png
每个目录存放的文件,都不相同

  • bin:存放编译后生成的二进制可执行文件
  • pkg:存放编译后生成的 .a 文件
  • src:存放项目的源代码,可以是你自己写的代码,也可以是你 go get 下载的包

将你的包或者别人的包全部放在 $GOPATH/src 目录下进行管理的方式,我们称之为 GOPATH 模式。
在这个模式下,使用 go install 时,生成的可执行文件会放在 $GOPATH/bin 下
image.png
如果你安装的是一个库,则会生成 .a 文件到 $GOPATH/pkg 下对应的平台目录中(由 GOOS 和 GOARCH 组合而成),生成 .a 为后缀的文件。
image.png
GOOS,表示的是目标操作系统,有 darwin(Mac),linux,windows,android,netbsd,openbsd,solaris,plan9 等
而 GOARCH,表示目标架构,常见的有 arm,amd64 等
这两个都是 go env 里的变量,你可以通过 go env 变量名 进行查看
image.png
至此,你可能不会觉得上面的方案会产生什么样的问题,直到你开始真正使用 GOPATH 去开发程序,就不得不开始面临各种各样的问题,其中最严重的就是版本管理问题,因为 GOPATH 根本没有版本的概念。
以下几点是你使用 GOPATH 一定会碰到的问题:

  • 你无法在你的项目中,使用指定版本的包,因为不同版本的包的导入方法也都一样
  • 其他人运行你的开发的程序时,无法保证他下载的包版本是你所期望的版本,当对方使用了其他版本,有可能导致程序无法正常运行
  • 在本地,一个包只能保留一个版本,意味着你在本地开发的所有项目,都得用同一个版本的包,这几乎是不可能的。

2.2 go vendor 模式的过渡

为了解决 GOPATH 方案下不同项目下无法使用多个版本库的问题,Go v1.5 开始支持 vendor 。
以前使用 GOPATH 的时候,所有的项目都共享一个 GOPATH,需要导入依赖的时候,都来这里找,正所谓一山不容二虎,在 GOPATH 模式下只能有一个版本的第三方库。
解决的思路就是,在每个项目下都创建一个 vendor 目录,每个项目所需的依赖都只会下载到自己vendor目录下,项目之间的依赖包互不影响。在编译时,v1.5 的 Go 在你设置了开启 GO15VENDOREXPERIMENT=1 (注:这个变量在 v1.6 版本默认为1,但是在 v1.7 后,已去掉该环境变量,默认开启 vendor 特性,无需你手动设置)后,会提升 vendor 目录的依赖包搜索路径的优先级(相较于 GOPATH)。
其搜索包的优先级顺序,由高到低是这样的

  • 当前包下的 vendor 目录
  • 向上级目录查找,直到找到 src 下的 vendor 目录
  • 在 GOROOT 目录下查找
  • 在 GOPATH 下面查找依赖包

虽然这个方案解决了一些问题,但是解决得并不完美。

  • 如果多个项目用到了同一个包的同一个版本,这个包会存在于该机器上的不同目录下,不仅对磁盘空间是一种浪费,而且没法对第三方包进行集中式的管理(分散在各个角落)。
  • 并且如果要分享开源你的项目,你需要将你的所有的依赖包悉数上传,别人使用的时候,除了你的项目源码外,还有所有的依赖包全部下载下来,才能保证别人使用的时候,不会因为版本问题导致项目不能如你预期那样正常运行。

这些看似不是问题的问题,会给我们的开发使用过程变得非常难受,虽然我是初学者,还未使用过 go vendor,但能有很明显的预感,这个方案照样会另我崩溃。

2.3 go mod 的横空出世

go modules 在 v1.11 版本正式推出,在最新发布的 v1.14 版本中,官方正式发话,称其已经足够成熟,可以应用于生产上。
从 v1.11 开始,go env 多了个环境变量: GO111MODULE ,这里的 111,其实就是 v1.11 的象征标志, go 里好像很喜欢这样的命名方式,比如当初 vendor 出现的时候,也多了个 GO15VENDOREXPERIMENT环境变量,其中 15,表示的vendor 是在 v1.5 时才诞生的。
GO111MODULE 是一个开关,通过它可以开启或关闭 go mod 模式。
它有三个可选值:off、on、auto,默认值是auto。

  1. GO111MODULE=off禁用模块支持,编译时会从GOPATH和vendor文件夹中查找包。
  2. GO111MODULE=on启用模块支持,编译时会忽略GOPATH和vendor文件夹,只根据 go.mod下载依赖。
  3. GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,自动开启模块支持。

go mod 出现后, GOPATH(肯定没人使用了) 和 GOVENDOR 将会且正在被逐步淘汰,但是若你的项目仍然要使用那些即将过时的包依赖管理方案,请注意将 GO111MODULE 置为 off。
具体怎么设置呢?可以使用 go env 的命令,如我要开启 go mod ,就使用这条命令

$ go env -w GO111MODULE="on"

2.4 go mod 依赖的管理

接下来,来演示一下 go modules 是如何来管理包依赖的。
go mod 不再依靠 $GOPATH,使得它可以脱离 GOPATH 来创建项目,于是我们在家目录下创建一个 go_test 的目录,用来创建我的项目,详细操作如下:
image.png
接下来,进入项目目录,执行如下命令进行 go modules 的初始化
image.png
接下来很重要的一点,我们要看看 go install 把下载的包安装到哪里了?
image.png
上面我们观察到,在使用 go modules 模式后,项目目录下会多生成两个文件也就是 go.mod 和 go.sum 。
这两个文件是 go modules 的核心所在,这里不得不好好介绍一下。
image.png

go.mod 文件

go.mod 的内容比较容易理解

  • 第一行:模块的引用路径
  • 第二行:项目使用的 go 版本
  • 第三行:项目所需的直接依赖包及其版本

在实际应用上,你会看见更复杂的 go.mod 文件,比如下面这样

module github.com/BingmingWong/module-test

go 1.14

require (
    example.com/apple v0.1.2
    example.com/banana v1.2.3
    example.com/banana/v2 v2.3.4
    example.com/pear // indirect
    example.com/strawberry // incompatible
)

exclude example.com/banana v1.2.4
replace(
    golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
    golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
    golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)

主要是多出了两个 flag:

  • exclude: 忽略指定版本的依赖包
  • replace:由于在国内访问golang.org/x的各个包都需要FQ,你可以在go.mod中使用replace替换成github上对应的库。

go.sum 文件

反观 go.sum 文件,就比较复杂了,密密麻麻的。
可以看到,内容虽然多,但是也不难理解
每一行都是由 模块路径,模块版本,哈希检验值 组成,其中哈希检验值是用来保证当前缓存的模块不会被篡改。hash 是以h1:开头的字符串,表示生成checksum的算法是第一版的hash算法(sha256)。
值得注意的是,为什么有的包只有一行

<module> <version>/go.mod <hash>

而有的包却有两行呢

<module> <version> <hash>
<module> <version>/go.mod <hash>

那些有两行的包,区别就在于 hash 值不一行,一个是 h1:hash,一个是 go.mod h1:hash
而 h1:hash 和 go.mod h1:hash两者,要不就是同时存在,要不就是只存在 go.mod h1:hash。那什么情况下会不存在 h1:hash 呢,就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的h1 hash,就会出现不存在 h1 hash,只存在 go.mod h1:hash 的情况。[引用自 3]
go.mod 和 go.sum 是 go modules 版本管理的指导性文件,因此 go.mod 和 go.sum 文件都应该提交到你的 Git 仓库中去,避免其他人使用你写项目时,重新生成的go.mod 和 go.sum 与你开发的基准版本的不一致。

2.5 go mod 命令的使用

  • go mod init:初始化go mod, 生成go.mod文件,后可接参数指定 module 名,上面已经演示过。
  • go mod download:手动触发下载依赖包到本地cache(默认为$GOPATH/pkg/mod目录)
  • go mod graph: 打印项目的模块依赖结构

image.png

  • go mod tidy :添加缺少的包,且删除无用的包
  • go mod verify :校验模块是否被篡改过
  • go mod why: 查看为什么需要依赖
  • go mod vendor :导出项目所有依赖到vendor下

image.png

  • go mod edit :编辑go.mod文件,接 -fmt 参数格式化 go.mod 文件,接 -require=golang.org/x/text 添加依赖,接 -droprequire=golang.org/x/text 删除依赖,详情可参考 go help mod edit

image.png

  • go list -m -json all:以 json 的方式打印依赖详情

image.png
如何给项目添加依赖(写进 go.mod)呢?
有两种方法:

  • 你只要在项目中有 import,然后 go build 就会 go module 就会自动下载并添加。
  • 自己手工使用 go get 下载安装后,会自动写入 go.mod 。

image.png

2.6 总结写在最后

如果让我用一段话来评价 GOPATH 和 go vendor,我会说
GOPATH 做为 Golang 的第一个包管理模式,只能保证你能用,但不保证好用,而 go vendor 解决了 GOPATH 忽视包版的本管理,保证好用,但是还不够好用,直到 go mod 的推出后,才使 Golang 包的依赖管理有了一个能让 Gopher 都统一比较满意的方案,达到了能用且好用的标准。
如果是刚开始学习 Golang ,那么 GOPATH 和 go vendor 可以做适当了解,不必深入研究,除非你要接手的项目由于一些历史原因仍然在使用 go vender 械管理,除此之外,任何 Gopher 应该从此刻就投入 go modules 的怀抱。
以上是我在这几天的学习总结,希望对还未入门阶段的你,有所帮助。另外,本篇文章如有写得不对的,请后台批评指正,以免误导其他朋友,非常感谢。

3 开源发布:如何开源自己写的包给别人用?

通常之前的学习,我们知道了在 Go 的项目中,可以 import 一个托管在远程仓库的模块,这个模块在我们使用 go get 的时候,会下载到本地。
既然是放在远程仓库上,意味着所有人都可以发布,并且所以人也都可以使用。
今天就来学习一下,如何发布一个开源的模块,并且使用它。

3.1 新建仓库

先在你的 Github 上新建一个仓库,记得选 Public(默认)
image.png
然后你会得到一个仓库地址,在你的电脑上 使用 git clone 命令克隆下来

3.2 编写模块代码

使用前面学过的 go mod init 命令进行初始化,注意这里的模块名,填写我们的git仓库地址(但是要去掉.git哈)

$ git clone https://github.com/BingmingWong/goutils.git
$ go mod init github.com/BingmingWong/goutils

image.png
然后新建一个 hash 文件夹,存放编写的一个计算 md5 值工具包,编辑 md5.go

package hash

import (
    "crypto/md5"
    "encoding/hex"
    "errors"
    "fmt"
    "io"
    "os"
)

// get file md5
func FileMd5(filename string) (string, error) {
    file, err := os.Open(filename)
    if err != nil {
    return "", errors.New(
        fmt.Sprintf("md5.go hash.FileMd5 os open error %v", err))
    }
    h := md5.New()
    _, err = io.Copy(h, file)
    if err != nil {
        return "", errors.New(fmt.Sprintf("md5.go hash.FileMd5 io copy error %v", err))
    }

    return hex.EncodeToString(h.Sum(nil)), nil
}

// get string md5
func StringMd5(s string) string {
    md5 := md5.New()
    md5.Write([]byte(s))
    return hex.EncodeToString(md5.Sum(nil))
}

由于我们使用的都是内置包,没有引入第三方的包,所以接下来可以把你刚刚那些新增的文件,全部 push 到 git 仓库。

$ git add -A
$ git commit -m "Add a md5 function"
$ git push

3.3 发布版本

一切完成后,刷新我们的仓库,就可以看到我们的刚刚上传的项目代码了,点击 release 发布一个版本
image.png image.png
然后像下图一样,添加一些版本说明
image.png
最后点击一个 Publish release,就发布了一个版本
image.png

3.4 如何使用?

使用 go get 命令下载我们的发布的模块

$ go get github.com/BingmingWong/goutils

image.png
再使用 tree 命令,查看一下我们下载的包已经放入了 $GOPATH/pkg/mod 下。
有一点很有趣的是,我的 Github 用户名(BingmingWong)是有大写字母的,下载下来后,在目录中大写字母会对应变成 !小写字母,如下所示
image.png
这个用户名看起来有点非主流,你要想改的话,也是可以的。如果你有其他的开源项目,github 并不会为你做重定向,你需要自己评估这个风险。
image.png
回过头来,我还是继续讲如何使用吧。
下载下来后,我们试着去调用一下他的函数,有一点需要注意的是,在这个示例里,你不能使用 github.com/BingmingWong/goutils 去导入,因为在这个目录下并没有 package,所以你必须导入 github.com/BingmingWong/goutils/hash 。
整个过程如下所示,供你参考:
image.png

4 代码规范:Go语言中编码规范

每个语言都有自己特色的编码规范,学习该语言的命名规范,能让你写出来的代码更加易读,更加不容易出现一些低级错误。
本文根据个人编码习惯以及网络上的一些文章,整理了一些大家能用上的编码规范,可能是一些主流方案,但不代表官方,这一点先声明一下。

4.1 文件命名

  1. 由于 Windows平台文件名不区分大小写,所以文件名应一律使用小写
  2. 不同单词之间用下划线分词,不要使用驼峰式命名
  3. 如果是测试文件,可以以 _test.go 结尾
  4. 文件若具有平台特性,应以 文件名_平台.go 命名,比如 utils_ windows.go,utils_linux.go,可用的平台有:windows, unix, posix, plan9, darwin, bsd, linux, freebsd, nacl, netbsd, openbsd, solaris, dragonfly, bsd, notbsd, android,stubs
  5. 一般情况下应用的主入口应为 main.go,或者以应用的全小写形式命名。比如MyBlog 的入口可以为 myblog.go

4.2 常量命名

目前在网络上可以看到主要有两种风格的写法

  1. 第一种是驼峰命名法,比如 appVersion
  2. 第二种使用全大写且用下划线分词,比如 APP_VERSION

这两种风格,没有孰好孰弱,可自由选取,我个人更倾向于使用第二种,主要是能一眼与变量区分开来。
如果要定义多个变量,请使用 括号 来组织。

const (
    APP_VERSION = "0.1.0"
  CONF_PATH = "/etc/xx.conf"
)

4.3 变量命名

和常量不同,变量的命名,开发者们的喜好就比较一致了,统一使用 驼峰命名法

  1. 在相对简单的环境(对象数量少、针对性强)中,可以将完整单词简写为单个字母,例如:user写为u
  2. 若该变量为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头。例如:isExist ,hasConflict 。
  3. 其他一般情况下首单词全小写,其后各单词首字母大写。例如:numShips 和 startDate 。
  4. 若变量中有特有名词(以下列出),且变量为私有,则首单词还是使用全小写,如 apiClient。
  5. 若变量中有特有名词(以下列出),但变量不是私有,那首单词就要变成全大写。例如:APIClient,URLString

这里列举了一些常见的特有名词:

// A GonicMapper that contains a list of common initialisms taken from golang/lint
var LintGonicMapper = GonicMapper{
    "API":   true,
    "ASCII": true,
    "CPU":   true,
    "CSS":   true,
    "DNS":   true,
    "EOF":   true,
    "GUID":  true,
    "HTML":  true,
    "HTTP":  true,
    "HTTPS": true,
    "ID":    true,
    "IP":    true,
    "JSON":  true,
    "LHS":   true,
    "QPS":   true,
    "RAM":   true,
    "RHS":   true,
    "RPC":   true,
    "SLA":   true,
    "SMTP":  true,
    "SSH":   true,
    "TLS":   true,
    "TTL":   true,
    "UI":    true,
    "UID":   true,
    "UUID":  true,
    "URI":   true,
    "URL":   true,
    "UTF8":  true,
    "VM":    true,
    "XML":   true,
    "XSRF":  true,
    "XSS":   true,
}

4.4 函数命名

  1. 函数名还是使用 驼峰命名法
  2. 但是有一点需要注意,在 Golang 中是用大小写来控制函数的可见性,因此当你需要在包外访问,请使用 大写字母开头
  3. 当你不需要在包外访问,请使用小写字母开头

另外,函数内部的参数的排列顺序也有几点原则

  1. 参数的重要程度越高,应排在越前面
  2. 简单的类型应优先复杂类型
  3. 尽可能将同种类型的参数放在相邻位置,则只需写一次类型

4.5 接口命名

使用驼峰命名法,可以用 type alias 来定义大写开头的 type 给包外访问。

type helloWorld interface {
    func Hello();
}

type SayHello helloWorld

当你的接口只有一个函数时,接口名通常会以 er 为后缀

type Reader interface {
    Read(p []byte) (n int, err error)
}

4.6 注释规范

注释分为

1 包注释

  1. 位于 package 之前,如果一个包有多个文件,只需要在一个文件中编写即可
  2. 如果你想在每个文件中的头部加上注释,需要在版权注释和 Package前面加一个空行,否则版权注释会作为Package的注释。
  3. 如果是特别复杂的包,可单独创建 doc.go 文件说明
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package net

2 代码注释

用于解释代码逻辑,可以有两种写法
单行注释使用 // ,多行注释使用 /* comment */

// 单行注释

/*
多
行
注
释
*/

另外,对于代码注释还有一些更加苛刻的要求,这个看个人了,摘自网络:

  • 所有导出对象都需要注释说明其用途;非导出对象根据情况进行注释。
  • 如果对象可数且无明确指定数量的情况下,一律使用单数形式和一般进行时描述;否则使用复数形式。
  • 包、函数、方法和类型的注释说明都是一个完整的句子。
  • 句子类型的注释首字母均需大写;短语类型的注释首字母需小写。
  • 注释的单行长度不能超过 80 个字符。
  • 类型的定义一般都以单数形式描述:
// Request represents a request to run a command.  type Request struct { ...
  • 如果为接口,则一般以以下形式描述:
// FileInfo is the interface that describes a file and is returned by Stat and Lstat.
type FileInfo interface { ...
  • 函数与方法的注释需以函数或方法的名称作为开头:
// Post returns *BeegoHttpRequest with POST method.
  • 如果一句话不足以说明全部问题,则可换行继续进行更加细致的描述:
// Copy copies file from source to target path.
// It returns false and error when error occurs in underlying function calls.
  • 若函数或方法为判断类型(返回值主要为 bool 类型),则以 returns true if 开头:
// HasPrefix returns true if name has any string in given slice as prefix.
func HasPrefix(name string, prefixes []string) bool { ...

3 特别注释

  • TODO:提醒维护人员此部分代码待完成
  • FIXME:提醒维护人员此处有BUG待修复
  • NOTE:维护人员要关注的一些问题说明

4.7 包的导入

单行的包导入

import "fmt"

多个包导入,请使用 () 来组织

import (
  "fmt"
  "os"
)

另外根据包的来源,对排版还有一定的要求

  1. 标准库排最前面,第三方包次之、项目内的其它包和当前包的子包排最后,每种分类以一空行分隔。
  2. 尽量不要使用相对路径来导入包。
import (
    "fmt"
    "html/template"
    "net/http"
    "os"

    "github.com/codegangsta/cli"
    "gopkg.in/macaron.v1"

    "github.com/gogits/git"
    "github.com/gogits/gfm"

    "github.com/gogits/gogs/routers"
    "github.com/gogits/gogs/routers/repo"
    "github.com/gogits/gogs/routers/user"
)

4.8 善用 gofmt

除了命名规范外,Go 还有很多格式上的规范,比如

  1. 使用 tab 进行缩进
  2. 一行最长不要超过 80 个字符

因此在格式上的问题,你大部分都可以放心交由 gofmt 帮你调整。关于 gofmt 的文章还在写,应该这两天就会更新。你可以过两天再来看看。

5 编译流程:结合 Makefile 简化编译过程

在另一篇文章中(使用 -ldflags 实现动态信息注入) 我详细介绍了如何利用 -ldflags 动态往程序中注入信息,但这种技巧需要指定一大串的参数,相信你已经崩溃了吧?
更合理的做法,是将这些参数 Makefile 来管理维护,在 Makefile 中可以用 shell 命令去获取一些 git 的信息,比如下面这样子

# gitTag
gitTag=$(git log --pretty=format:'%h' -n 1)

# commitID
gitCommit=$(git rev-parse --short HEAD)

# gitBranch
gitBranch=$(git rev-parse --abbrev-ref HEAD)

我先在该项目下初始化 Git 仓库

# 初始化
git init .

# 添加所有文件到暂存区
git add -A

# 提交 commit
git commit -m "init repo"

然后编写出如下的 Makefile 到项目的根目录

BINARY="demo"
VERSION=0.0.1
BUILD=`date +%F`
SHELL := /bin/bash

versionDir="github.com/iswbm/demo/utils"
gitTag=$(shell git log --pretty=format:'%h' -n 1)
gitBranch=$(shell git rev-parse --abbrev-ref HEAD)
buildDate=$(shell TZ=Asia/Shanghai date +%FT%T%z)
gitCommit=$(shell git rev-parse --short HEAD)

ldflags="-s -w -X ${versionDir}.version=${VERSION} -X ${versionDir}.gitBranch=${gitBranch} -X '${versionDir}.gitTag=${gitTag}' -X '${versionDir}.gitCommit=${gitCommit}' -X '${versionDir}.buildDate=${buildDate}'"

default:
    @echo "build the ${BINARY}"
    @GOOS=linux GOARCH=amd64 go build -ldflags ${ldflags} -o  build/${BINARY}.linux  -tags=jsoniter
    @go build -ldflags ${ldflags} -o  build/${BINARY}.mac  -tags=jsoniter
    @echo "build done."

接下来就可以直接使用 make 命令,编译出 mac 和 linux 两个版本的二进制执行文件
image.png

6 依赖管理:好用的工作区模式

对我来说,Go1.18 最 “实用” 的功能,应该是 Go 工作区模式,本篇来介绍一下
若要使用 workspace mode,请升级到 Go 1.18 版本再体验。

6.1 诞生背景

在介绍 工作区模式 (workspace mode)之前,先简单地说一下工作区诞生的背景,只有了解了痛点之后,学习一个新的知识点,才更有动力。
我在 $GOPATH/src 目录下创建 github.com/iswbm/demo 及 github.com/iswbm/util 两个空的 go 包
image.png
然后分别使用 go mod init 来初始化

$ cd github.com/iswbm/demo
$ go mod init github.com/iswbm/demo

$ cd ../util
$ go mod init github.com/iswbm/util

并在 demo 中写入 main.go,在 util 写入 util.go ,内容如下
image.png
我们都知道,正常 Go 项目中引用的包,都需要在对应代码托管网站上有该包,才能编译及运行。
但如果 demo 引用 util 项目的包,而 util 本身也还在自己的本地上开发,并没有上传到 github,那么 demo 包在调试过程中肯定是无法找到 util 包的。
在没有工作区的情况下(也即 Go 1.18 之前),可以通过在 go.mod 中使用 replace 来重定向

module github.com/iswbm/demo

go 1.17

require github.com/iswbm/util v1.0.0

replace github.com/iswbm/util => ../util

修改完 go.mod 后,运行正常输出
image.png
可使用 replace 存在两个问题:

  • replace 本身编辑就比较麻烦,我一直是手动编辑的
  • 开发完成后,还要记得将 replace 删除,并执行 go mod tidy ,否则别人就无法使用

6.2 工作区模式

Go 1.18 提供的工作区模式,就可以优雅的解决如上出现的问题。
上面我使用的是 go 1.17 初始化的项目,现在要用 go 1.18 的工作区模式,因此先将 go.mod 里的 go 版本改为 go 1.18,并都所有的依赖全部删除。
image.png
然后退回到 $GOPATH/src 目录,执行如下命令创建工作区文件 go.work

go18 work init github.com/iswbm/demo github.com/iswbm/util

创建的 go.work 文件如下所示
image.png
然后无论我在哪个目录下,只要所在位置的父级目录有 go.work 文件,即处于该工作区内,go 的编译器都会自动引用本地的 util 包
image.png

6.3 写在最后

有了工作区模式,将使整个开发流程更加流畅,个人认为这可能是 go1.18 最为实用的功能,强烈推荐大家使用起来~
另外 go.work 文件是工作区的标志,该文件不用上传至 github,只用于本地开发测试使用。

posted @ 2024-03-14 23:21  liuyang9643  阅读(8)  评论(0编辑  收藏  举报