go工程组织规范
go编码以workspace形式管理,一个workspace包含所有的Go编码,包含多个版本控制仓库(例如使用git管理的多个仓库)。每个仓库包含多个包package,每个package是一个单独的路径,包含所有go源码,包的路径就是包的导入路径(import path)。
1. Workspace
Workspace作为根目录,包含两个目录src和bin。bin包含可执行文件,src包含源码。典型的src包含多个版本控制仓库(记录源码开发过程)。形如:
上面的workspace包含两个仓库example和image。example包含两个命令(或包,hello和outyet)和一个库(或包,stringutil)。
$GOPATH指向当前的Workspace。
包的导入路径(import path),指定一个包在workspace或远程仓库的位置。标准库的导入路径可以简写,如“fmt”和“net/http”。但自有库的包的导入路径应该是绝对路径(src下级路径开始),包含基础路径(如:github.com/user)。可通过go help importpath获取更多导入路径的信息。
go源码的第一有效行(注释行除外)必须是
package name
name是导入路径(import path)的默认包名。在一个包中所有文件必须用相同包名,惯例:导入路径名的最后一项应与包名相同,以方便识别和记忆,不出错。
一个程序只有一个main包,一个包中只能有一个main函数,不能重复定义。
对一个可执行程序,不要求package名唯一,但要求导入路径唯一(即可明确确认一个包)。
go程序导出名字
在 Go 中,任何以大写字母开头的变量或者函数都是被导出的名字。其它包只能访问被导出的函数和变量。
同一个包中,所有变量和函数都可调用,无论首字母是否大小写。
first程序
用自己的github账号创建仓库路径:$GOPATH/src/github.com/yuxi-o/golang,此目录下创建包目录hello,然后在hello目录下创建hello.go程序。
目前hello.go路径为:src/github.com/yuxi-o/golang/hello/hello.go。
现在运行如下命令安装hello.go程序:
$go install github.com/yuxi-o/golang/hello
注意:go install或go build执行的路径为package路径,可执行程序名为包路径名。
可采用git管理yuxi-o/golang仓库,在golang目录下:
~/go/src/github.com/yuxi-o/golang$ ls hello ~/go/src/github.com/yuxi-o/golang$ git init Initialized empty Git repository in /home/wang/go/src/github.com/yuxi-o/golang/.git/ ~/go/src/github.com/yuxi-o/golang$ git add . ~/go/src/github.com/yuxi-o/golang$ git commit -m "first go program" [master (root-commit) bd2635a] first go program 1 file changed, 6 insertions(+) create mode 100644 hello/hello.go
first库
mkdir $GOPATH/src/github.com/yuxi-o/golang/stringutil cat ~/go/src/github.com/yuxi-o/golang/stringutil/reverse.go package stringutil func Reverse(s string) string { r := []rune(s) for i,j := 0, len(r)-1; i< len(r)/2; i,j= i+1, j-1 { r[i], r[j] = r[j], r[i] } return string(r) } $ go build github.com/yuxi-o/golang/stringutil
注:go build编译库不会输出文件,保存编译包到local build cache。
cat ~/go/src/github.com/yuxi-o/golang/hello/hello.go package main import ( "fmt" "github.com/yuxi-o/golang/stringutil" ) func main() { // fmt.Println("Hello, Go world") fmt.Println(stringutil.Reverse("!oG, olleH")) } $go install github.com/yuxi-o/golang/hello
现在目录如下:
go测试
go提供轻量级测试框架,包含go test和testing package。
go测试文件命令为_test.go,测试函数定义:func TestXXX(t *testing.T)。test框架运行每个测试函数。假如测试失败,返回t.Error或t.Fail。
cat ~/go/src/github.com/yuxi-o/golang/stringutil/reverse_test.go package stringutil import "testing" func TestReverse(t *testing.T) { cases := []struct { in, want string }{ {"Hello, world", "dlrow ,olleH"}, {"Hello, 世界", "界世 ,olleH"}, {"", ""}, } for _, c := range cases { got := Reverse(c.in) if got != c.want { t.Errorf("Reverse(%q) == %q, wang %q", c.in, got, c.want) } } } $ go test github.com/yuxi-o/golang/stringutil ok github.com/yuxi-o/golang/stringutil 0.002s
Remote包
导入路径可以描述怎样获取package源码(通过git或mercurial)。go get可以fetch、build和install包,若包不存在,go get会下载包到第一个workspace。可下载golang/example测试:
$ go get github.com/golang/example/hello $ $GOPATH/bin/hello Hello, Go examples!
2. go项目结构
go项目体检的目录结构:golang-standards/project-layout
├── LICENSE.md ├── Makefile ├── README.md ├── api ├── assets ├── build ├── cmd ├── configs ├── deployments ├── docs ├── examples ├── githooks ├── init ├── internal ├── pkg ├── scripts ├── test ├── third_party ├── tools ├── vendor ├── web └── website
/pkg
这个目录中存放的就是项目中可以被外部应用使用的代码库,其他的项目可以直接通过 import
引入这里的代码,所以当我们将代码放入 pkg
时一定要慎重,建议各位开发者对项目中公有和私有的代码进行妥善的划分。
/internal
私有代码推荐放到 /internal
目录中,真正的项目代码应该写在 /internal/app
里,同时这些内部应用依赖的代码库应该在 /internal/pkg
子目录和 /pkg
中,下图展示了一个使用 /internal
目录的项目结构:
当我们在其他项目引入包含 internal
的依赖时,Go 语言会在编译时报错:
An import of a path containing the element “internal” is disallowed
if the importing code is outside the tree rooted at the parent of the "internal" directory.
这种错误只有在被引入的 internal
包不存在于当前项目树中才会发生,如果在同一个项目中引入该项目的 internal
包并不会出现这种错误。
/src
在 Go 语言的项目最不应该有的目录结构其实就是 /src
了,社区中的一些项目确实有 /src
文件夹,但是这些项目的开发者之前大多数都有 Java 的编程经验,这在 Java 和其他语言中其实是一个比较常见的代码组织方式,但是作为一个 Go 语言的开发者,我们不应该允许项目中存在 /src
目录。
最重要的原因其实是 Go 语言的项目在默认情况下都会被放置到 $GOPATH/src
目录下,这个目录中存储着我们开发和依赖的全部项目代码,如果我们在自己的项目中使用 /src
目录,该项目的 PATH
中就会出现两个 src
:
$GOPATH/src/github.com/draveness/project/src/code.go
上面的目录结构看起来非常奇怪,这也是我们在 Go 语言中不建议使用 /src
目录的最重要原因。
平铺
另一种在 Go 语言中组织代码的方式就是项目的根目录下放项目的代码,这种方式在很多框架或者库中非常常见,如果想要引入一个使用 pkg
目录结构的框架时,我们往往需要使用 github.com/draveness/project/pkg/somepkg
,当代码都平铺在项目的根目录时只需要使用 github.com/draveness/project
,很明显地减少了引用依赖包语句的长度。
所以对于一个 Go 语言的框架或者库,将代码平铺在根目录下也很正常,但是在一个 Go 语言的服务中使用这种代码组织方法可能就没有那么合适了。
/cmd
/cmd
目录中存储的都是当前项目中的可执行文件,该目录下的每一个子目录都应该包含我们希望有的可执行文件,如果我们的项目是一个 grpc
服务的话,可能在 /cmd/server/main.go
中就包含了启动服务进程的代码,编译后生成的可执行文件就是 server
。
我们不应该在 /cmd
目录中放置太多的代码,我们应该将公有代码放置到 /pkg
中并将私有代码放置到 /internal
中并在 /cmd
中引入这些包,保证 main
函数中的代码尽可能简单和少。
cmd应用目录负责程序的启动、关闭、配置初始化等。
微服务中的app服务类型分为5类:interface(/cmd/myapp1-interface)、service(/cmd/myapp1-service)、job(/cmd/myapp1-job)、admin、task。
- interface:对外的BFF服务,接收来自用户的请求,比如暴露了HTTP/gRPC接口。
- service:对内的微服务,仅接收来自内部其他服务或网关的请求,比如暴露了gRPC接口只对内服务。
- admin:区别于service,更多的是面向运营测的服务,通常数据权限更高,隔离带来更好的代码级别安全。
- job:流式任务处理的服务,上游一般依赖message broker。
- task:定时任务,类似cronjob,部署到task托管平台中。
/api
/api
目录中存放的就是当前项目对外提供的各种不同类型的 API 接口定义文件了,其中可能包含类似 /api/protobuf-spec
、/api/thrift-spec
或者 /api/http-spec
的目录,这些目录中包含了当前项目对外提供的和依赖的所有 API 文件:
$ tree ./api api └── protobuf-spec └── oceanbookpb ├── oceanbook.pb.go └── oceanbook.proto
二级目录的主要作用就是在一个项目同时提供了多种不同的访问方式时,用这种办法避免可能存在的潜在冲突问题,也可以让项目结构的组织更加清晰。
/test
额外的外部测试应用程序和测试数据。可以随时根据需求构造/test目录。对于较大的项目,有一个数据子目录是有意义的。
注意:Go还会忽略以“."或”_”开头的目录或文件,因此在如何命名测试数据目录方面有更大的灵活性。
Makefile
最后要介绍的 Makefile
文件也非常值得被关注,在任何一个项目中都会存在一些需要运行的脚本,这些脚本文件应该被放到 /scripts
目录中并由 Makefile
触发,将这些经常需要运行的命令固化成脚本减少『祖传命令』的出现。
参考:
1. How to write Go Code https://golang.google.cn/doc/code.html
2. https://golang.google.cn/ 提供go在线测试环境和文档
3. https://golang.google.cn/doc/ go相关文档
4. https://golang.google.cn/pkg/ go标准库
6. 可在https://godoc.org/中搜索所有golang库的接口说明。
7. 在 GitHub 上构建一个看上去正规的 Golang 项目
9. Golang标准库文档 中文文档