Go 学习笔记(四)构建 go 程序

本文原创地址:博客园骏马金龙Go 基础系列:构建 go 程序

hello world

从一个简单的程序开始解释,将下面的内容放进 test.go 文件中,路径随意:

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World")
}

Go 通过包的方式管理程序,每个 Go 源代码文件都必须声明自己所在的包,正如上面的package main声明自己所在的包是 main 包。

每个程序都必须有一个 main 包,main 包作为整个程序的编译入口包,main 包中的 main() 函数作为程序的执行入口。

import 关键字用来导入其它包,导入某个包之后就能在当前文件中使用这个包中的函数,例如上面的 main 包导入 fmt 包后,可以使用 fmt 包中的函数 Println()。

然后可以使用 go 的 build 工具编译这个 test.go 文件:

$ go build test.go

编译后,将在当前路径下生成一个可执行二进制文件:Windows 下生成的是 test.exe 文件,Unix 下生成的是 test 文件。既然是可执行文件,当然可以直接执行:

$ ./test

将输出 "Hello World"。

也可以直接通过 go 的 run 工具将编译和运行两个步骤合二为一:

$ go run test.go
Hello World

go run不会生成可执行的二进制文件,它实际上是将编译得到的文件放进一个临时目录,然后执行,执行完后自动清理临时目录。

关于包和 go 文件

每个 go 代码文件只能且必须使用 package 语句声明一个包,也就是说一个文件中不能包含多个包。

Go 中有 两种类型的包 ,或者说有两种类型的文件:

  1. 编译后成为可执行文件的包,也就是 main 包编译后的得到的文件
  2. 编译后成为共享库的包,只要 go 程序文件中声明的不是 main 包,就是库文件

注意:
在 go 的官方文档中将 go 的二进制可执行程序称为命令,有时候还会将 go 的源代码文件称为命令的源文件。可执行程序和包相反,包一般是作为 "库" 文件存在,用于导入而非用于执行

共享库中包含一些函数,这些函数比较通用,所以放进共享库方便函数复用。例如 fmt 包中的 Println 函数,到处都在使用这个函数,且因为 fmt 包是标准库 (Standary library),无论是谁都可以去使用这个包。

有两种类型的库文件:标准库和第三方的库。标准库是随 Go 安装的时候放在 go 安装目录下的 ($GOROOT/src/),第三方库是放在 workspace 下的。关于 workspace 后文再说。

共享库可以被 import 导入 (例如 fmt 包)。由于导入操作是在编译期间实现的,共享库中不应该包含任何输出型语句。

Go 中对库文件要求比较严格,或者说强制性的规范。它要求 库文件中 package 声明的包名必须和目录名称相同,且同一个目录下只允许有一个包,但同一个目录下可以有多个库文件片段,只不过这些库文件中必须都使用 package 声明它的包名为目录名 。例如:

src/mycode
     |- first.go
     |- second.go
     |- third.go

如果这三个文件都是库文件,则它们都必须且只能使用package mycode声明自己的包为 mycode。go build的时候,会将它们合并起来。如果声明的包名不是 mycode,go build 会直接忽略它。

当然,对 main 包就无所谓了,它不是库文件,可以放在任何地方,对目录名没有要求。但如果使用go install,则有额外的要求,见后文。

库文件中的大小写命名

Go 通过名称首字母的大小写决定属性是否允许导出:

  • 首字母大写的属性是允许导出的属性
  • 首字母小写的属性不允许被导出

所以当库文件被导入时,只有这个库文件中以大写字母开头的常量、变量、函数等才会被导出,才可以在其他文件中使用。

例如,库文件 abc.go 中:

func first() {}
func Second() {}

当导入这个包的时候,由于 first()函数首字母小写,外界无法使用它,它只能在自己的包 abc.go 中使用,对外界不可见。大写字母开头的 Second() 函数会被导入,所以可用。

工作空间 (workspace)

速览

  • 通过环境变量GOPATH设置 workspace 的路径
  • Go 编程人员一般将它们的 Go 代码放在一个workspace下,当然,这不是必须的
  • workspace 包含一个或多个版本控制系统的仓库 (如 git)
  • 每个仓库包含一个或多个 package
  • 每个 package 由单个目录下的一个或多个 Go 源文件组成,它们都必须声明目录名作为它们的包名
  • package 的目录路径决定导入包时 import 的路径

Go 和其它编程语言在组织项目的时候有所不同,其它语言一般每个项目都有一个单独的 workspace,且 workspace 一般和版本控制仓库进行绑定。

现在设置 GOPATH 环境变量,假设设置为/gocode

echo 'export GOPATH=/gocode' >>/etc/profile.d/gopath.sh
chmod +x /etc/profile.d/gopath.sh
source /etc/profile.d/gopath.sh

go env GOPATH确定是否正确:

$ go env GOPATH
/gocode

workspace 目录结构

每个 workspace 都是一个目录,这个目录下至少包含三个目录:

  • src:该目录用于存放 Go 源代码文件 (也称为命令的源文件)
  • bin:该目录用于存放可执行命令 (即构建后可执行的二进制 go 程序,也称为命令文件)
  • pkg:该目录用于存放共享库文件 (即构建后非可执行程序的库包,也称为包对象文件)

括号中给的名称是 go 官方文档中常见的别名称呼。

所以,先创建这 3 个目录

mkdir -p /gocode/{src,pkg,bin}

GOPATH 和 GOROOT 环境变量

GOPATH 环境变量指定 workspace 的位置,用来指示 go 从哪里搜索 go 源文件 / 包,例如 import 时从哪个路径搜索包并导入。GOROOT 环境变量用于指定 go 的安装位置。go 需要导入包时,会从 GOPATH 和 GOROOT 所设置的位置处搜索包。

默认位置为$HOME/go(Unix) 或%USERPROFILE\go%(Windows)。可以手动设置 GOPATH 环境变量的路径从而指定 workspace 的位置,可以指定为多个目录,多个目录时使用冒号分隔目录 (Unix 系统) 或使用分号分隔目录(Windows 系统)。注意,绝对不能将其设置为 go 的安装目录,即不能和 GOROOT 环境变量重复。

例如,windows 下设置d:\gocode目录为 GOPATH 的路径:

setx GOPATH d:\gocode

Unix 下设置$HOME/gocode目录为 GOPATH 的路径:

mkdir ~/gocode
export GOPATH=~/gocode
echo 'GOPATH=~/gocode' >>~/.bashrc

go envgo env GOPATH命令可以输出当前有效的 GOPATH 路径。

$ go env | grep GOPATH
GOPATH="/root/gocode"

$ go env GOPATH
/root/gocode

go build

先写两个 go 文件,一个是可执行 go 文件 test.go,一个是共享库 strutils.go,将它们放在 workspace 的 src 下。

$ mkdir -p $GOPATH/src/{hello,strutils}
$ tree -C
.
├── bin
├── pkg
├── src
│   ├── hello
│   │   └── test.go
│   └── strutils
│       └── strutils.go

注意,上面故意将 test.go 放在名为 hello 的目录下,可以将其放在 src 下的任何非库文件目录下 (例如不能放进 strutils 目录下),名称不要求。

hello/test.go 的内容如下:

package main

import (
    "fmt"
    "strutils"
)

func main() {
    fmt.Println("Hello World")
    fmt.Println(strutils.ToUpperCase("hello world"))
}

strutils/strutils.go 的内容如下:

package strutils

import (
    "strings"
)

func ToUpperCase(s string) string{
    return strings.ToUpper(s)
}

func ToLowerCase(s string) string{
    return strings.ToLower(s)
}

go build可以用于编译,编译时会对 import 导入的包进行搜索,搜索的路径为标准库所在路径$GOROOT/src、workspace 下的 src 目录。它只会生成额外的 可执行文件放在当前目录下,不会生成额外的库文件 。但需要注意,生成的可执行文件名称可能会出乎意料:

例如进入到目录src/hello下,对 test.go 的文件进行编译,以下三种 build 路径都可用成功编译:

cd src/hello
go build             # 生成的可执行文件名为hello
go build .           # 生成的可执行文件名为hello
go build test.go     # 生成的可执行文件名为test

前两者是等价的,当go build 以目录的形式进行编译,则生成的可执行文件名为目录名 。当go build 以 go 代码文件名的方式进行编译,则生成的可执行程序名为 go 源码文件名 (去掉后缀.go 或增加后缀.exe)。

go install

go 还有一个工具 install,go install的操作称为安装,将文件安装到合适的位置。go install时会先进行编译,然后将编译后的二进制文件保存到 workspace 的 bin 目录下,将编译后的库文件 (称为包对象文件,以 ".a" 为后缀) 放在 pkg 目录下。

注意,go install必须先进入到$GOPATH/src ,且只能对目录进行操作,不能对具体的 go 文件操作,因为 go 认为包和目录名相同。给go install指定一个目录名,就表示编译这个包名。

例如,对 src/hello 下的 test.go 进行安装,由于它导入了 strutils 包,所以会自动将 strutils 也安装好:

$ cd $GOPATH/src
$ go install hello
$ tree $GOPATH
/gocode
├── bin
│   └── hello           # 二进制程序文件名为hello,而非test
├── pkg
│   └── linux_amd64     
│       └── strutils.a  # 库文件
└── src
    ├── hello
    │   └── test.go
    └── strutils
        └── strutils.go

还可以单独对库文件进行安装:

$ rm -rf $GOPATH/bin/* $GOPATH/pkg/*
$ cd $GOPATH/src
$ go install strutils
/gocode
├── bin
├── pkg
│   └── linux_amd64
│       └── strutils.a
└── src
    ├── hello
    │   └── test.go
    └── strutils
        └── strutils.go

如果省略目录名,则表示对当前目录下的包进行安装:

$ cd $GOPATH/src/hello
$ go install

再次提醒,go install前先进入到$GOPATH/src目录下。

由于 go install 可以直接安装二进制文件到$GOPATH/bin,所以出于方便执行这些二进制程序,可以将这个目录放进 PATH 环境变量。

$ export PATH=$PATH:`go env GOPATH`/bin

构建 go 程序的规范建议

1. 由于可以将所有 go 项目放在同一个$GOPATH目录下,为了区分 src 下的项目目录和库文件目录,建议将每个项目目录设置深一点

例如:

bin
pkg
src
 |- /first/project
            |- main.go
            |- myliba
                |- a.go
                |- b.go
            |- mylibb
                |- c.go
                |- d.go
 |- /second/project
            |- main.go
            |- lib
                |- a.go
                |- b.go

2.go install 时,先进入到项目目录下

3. 库文件的名称 (也是目录名) 要选取合理,尽量短,但却尽量见名知意,也尽量减少名称重复的几率

例如 util 这种名称到处都是,可以修改为 numutil、nameutil 等。
上一篇 Go 学习笔记(三)结构 struct
Go 学习笔记(目录)
下一篇 Go 学习笔记(五)import 导包和初始化阶段