Go 包管理


在〈来个 Hello, World〉中,你已经看到 Go 开发中,一个 workspace 的基本样貌,你可以看到,里头会有 src、pkg、bin 目录,你会设置GOPATH环境变量指向这个目录,这些都是规范好的,也是强制的,正如〈How to Write Go Code〉中说到的:

The go tool is designed to work with open source code maintained in public repositories. Although you don't need to publish your code, the model for how the environment is set up works the same whether you do or not.

在〈来个 Hello, World〉已经稍微了解了packageGOPATH的关系,源码会是在GOPATH中设定的目录之 src 中,并有着对照于package设定名称之目录包括着它,当 Go 的工具(go buildgo install等)需要源码时,会到GOROOT底下,或者是GOPATH底下,查看是否有相应于包的源码存在,编译出来的结果,会是在相对应的 pkg 或 bin 底下。

本地包

在当时,为了简化说明,源码文件名故意与package设定的名称同名,这不是必要的,一个相应于package的目录底下,可以有许多个源码,而每个源码开头,只要package设定的名称都与目录相符就可以了。例如,你可以有个源码是 hello.go,位于 src/goexample 底下:

package goexample

import "fmt"

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

还可以有个 hi.go,位于 src/goexample 底下:

package goexample

import "fmt"

func Hi() {
    fmt.Println("Hi")
}

也就是说,一个package可以有数个源码文件,各自组织自己的任务,在执行go install goexample之后,上面两个源码会在 pkg 目录的$GOOS_$GOARCH目录中产生 goexample.a 文件。这包括了goexample包编译后的结果,如果想使用goexample包的功能,只需要编写个 main.go:

package main

import "goexample"

func main() {
    goexample.Hi()
    goexample.Hello()
}

你可以在包目录之前增加父目录,例如,可以创建一个 src/cc/openhome 目录,然后将方才的 hello.go 与 hi.go 移至该目录之中,接着执行go install cc/openhome/goexample,那么,在 pkg 目录的 $GOOS_$GOARCH 目录中,会产生对应的 cc/openhome 目录,其中放置着 goexample.a 文件,想要使用这个包的话,可以编写个 main.go:

package main

import "cc/openhome/goexample"

func main() {
    goexample.Hi()
    goexample.Hello()
}

远端包

由于 Go 的 workspace 设置,都必须是如此规范,因此,若你想将源码发布给他人使用时就很方便,例如,你可以创建 src/github.com/JustinSDK 目录,然后将方才的 goexample 目录移到 src/github.com/JustinSDK 当中,这么一来,显然地,你的 main.go 就要改成:

package main

import "github.com/JustinSDK/goexample"

func main() {
    goexample.Hi()
    goexample.Hello()
}

也就是说,你可以直接将 /src/github.com/JustinSDK/goexample 当作文件库(repository)发布到 Github,那么,其他人需要你的源码时,有个很方便的go get指令可以用,我将这个范例发布在 Github 的JustinSDK/goexample了,因此,你可以执行以下指令:

go get github.com/JustinSDK/goexample

go get会自行判断该使用的协定,以这边的例子来说,就会使用git来复制文件库至 src 目录底下,结果就是 src/github.com/JustinSDK 底下,会有个 goexample 目录,其中就是源码,go get在下载源码之后,就会开始进行编译,因此,你也会在 pkg 目录中的 $GOOS_$GOARCH 目录底下,github.com/JustinSDK 中找到编译好的 .a 文件。

接着,你就可以如上头的程序编写import "github.com/JustinSDK/goexample"来使用这个包。

当然,执行go install main的话,你的 pkg 目录中的$GOOS_$GOARCH目录,会有个 github.com/JustinSDK 目录,里头放置着 goexample.a 文件,而编译出来的可执行文件,则会放置在 bin 目录之中,此时,你的目录应该会像是:

go-exercise
        ├─bin
        │      main.exe
        │
        ├─pkg
        │  └─windows_amd64
        │      └─github.com
        │          └─JustinSDK
        │                  goexample.a
        │
        └─src
            ├─github.com
            │  └─JustinSDK
            │      └─goexample
            │              .gitignore
            │              hello.go
            │              hi.go
            │              LICENSE
            │              README.md
            │
            └─main
                    main.go

GOPATH 中多个路径

如果你在GOPATH中设定多个路径,那么,在哪个路径底下的 src 找到包的源码,编译出来的 .a 文件就会放在哪个路径底下的 pkg 目录之中。

如果是包括程序进入点的main包,那么执行go install main的话,默认会放在找到main包源码的 bin 目录之中。你可以设定GOBIN,指定编译出来的可执行文件放置的目录。

如果你在GOPATH中设定多个路径,那么,go get复制回来的源码,会被放置在GOPATH中设置的第一个目录 src 之中,同理,对应的 .a 文件,也会是GOPATH中设置的第一个目录的 pkg 之中。

有关 import

import时默认会使用包名称作为调用包中函数等的前置名称,你可以在import时指定别名。例如:

package main

import f "fmt"

func main() {
    f.Println("哈啰!世界!")
}

若指定别名时使用.,就不需要包名称作为前置名称,例如:

package main

import . "fmt"

func main() {
    Println("哈啰!世界!")
}

你不能只是import x "x"来试图只执行包的初始化函数,因为 Go 编译器不允许import了某个包而不使用,然而若指定别名时使用_,则不会导入包,只会执行包的初始化函数,也就是包中使用func init()定义的函数。

每个包可以有多个init定义在各个不同的原始文件中,包被import时会执行,若是main包,则会在所有init函数执行完毕后,再执行main函数,Go 执行包初始化化时,不会保证包中多个init的执行顺序。


展开阅读全文