Levy's ink.
Doodles, whimsy & life.
About
Blog
Mess
Catalog

Golang学习日记II - 源码组织: 包和包的管理

Golang学习日记的第二篇更新了!上一篇是九个月前的Golang学习日记I - 入坑篇,这期间我并没有停止学习Golang,(而是...懒,不想填坑而已。)
相反,我迄今为止用Golang完成了两个中等规模的项目:Http服务器框架Gurgling(4K lines)和分布式存储中间件Metaless H2(9K lines)。关于这两个项目的详细内容有时间另开博客。这篇文章我主要记录下一个现代语言最基础而重要的东西:包管理。

什么是包?什么是包管理?

包是一个项目的组件,而一个语言的包管理的作用,就在于控制组件的引入、移除、组成成最终项目的流程以及包间互相引用。类比来说,就和手机操作系统对app的管理类似,操作系统负责管理其安装、卸载,并控制其启动流程和与其他app的调用、通信。

C/C++由于太底层,在这方面就做的比较混乱:其并不存在包,而是由头文件构成的平面结构,二进制文件/源码文件的地址很松散,其引入外来组件也各种麻烦:引用头文件,告诉链接器二进制文件的地址、动态链接库地址或告诉编译器源码地址,其后如果地址有迁移则需修改一系列目录信息。

相对比地,Java的import,C#的using, Nodejs的require, Python的import等,则将源文件/二进制文件和引用信息结合,推出"包"的概念,允许程序员轻松地引入他人的包,不用考虑配置其他信息,仅仅一句 import example就可以完成对一个组件的其他依赖。

而对于包管理器,不同语言有不同的工具。如Java非原生的Maven,Nodejs的npm(已集成),Python的pip都托管了对应语言主流的包,或被主流包所支持。使用对应语言进行开发,包管理器几乎是除了开发环境外必装的东西。

Golang的源码组织

Golang的源码组织是以文件系统目录结构作为包层次结构、以包为最小引用单位、包名和目录名分离的。如一个项目的源码文件夹如下:

--- src/
 └---- package1/
    └---- file1.go
    └---- file2.go
    └---- file3.go
    └---- file4.go
 └---- package2/
    └---- sp1/
       └---- fileAlpha.go
       └---- fileBeta.go
    └---- fileA.go
    └---- fileB.go

则, package1, package2, sp1分别为一个包的目录名,在其他程序对其进行引用时,包为最小的引入对象。对比Python的函数为最小引入对象,Nodejs的js文件为最小引入对象,go这么做更想引导开发者忽略被引入包的内部实现,如

// 合法的引入
import "package1"
import "package2"
import "package2/sp1"
// 不合法的引入
import "package1/file1.go"

需要注意的是,尽管 sp1package2的“子包”,但引入 package2并不会自动引入 sp1的任何内容,事实上,子包和父包在编译时是完全独立的,他们之间仅仅存在包源码目录名上的从属关系和语义联系。

包名则和上文中包的目录名不同。包名是一个包中所有go文件在开头用

package imapackage

显式声明的名字,也是在被他人引用后默认的命名空间名字。其可以和本包的路径名不同,但一个包中所有源文件声明的包名必须一致。如上文例子中 package2路径下的源文件( fileA.go, fileB.go)可以都以 package pkg2开头,在这种情况下,引用者使用如下方式引入并使用该包:

import "package2" // 引入时提供的是包目录名

func foo() {
    pkg2.Bar()   // 使用时需用包名
}

说这么复杂我搞不清楚包名和包路径名怎么办

的确,go关于包路径和包分离的设定可能让初学者不可接受,我当时也花了好一段时间才分清。不过所幸的是,大部分go开发者约定俗成地将包路径名里最末一个文件夹名作为该包的包名,这样引用者大部分情况下都可以直接这样用了:

import "package2/sp1" // 引入时提供的是包目录名

func foo() {
    sp1.Bar()   // 因为包名是该包路径名里最后一个文件夹的名字,故为sp1
}

如果碰到极少数不守规矩的包开发者或包名太长,引入者也可以手动规定引入包的名字:

import shortname "packagex/thisisaverylongpackagename" // 引入时提供的是包目录名,并在前面对其重命名

func foo() {
    shortname.Bar()   // 源码中都使用重命名的值
}

更进一步偷懒,直接省略包名可以么?行呀:

import . "packagex/thisisaverylongpackagename" // 引入时提供的是包目录名,并用小数点表示省略该包包名

func foo() {
    Bar()   // Bar可以直接用了!
}

需要注意的是,由于省略包名引入的包可以有任意多个(如 builtin就是默认省略包名引入的包),这种引入方式往往带来成员归属的不清、引入成员的冲突等。故不建议。

GOPATH

说到源码组织和包,就不得不提到有一个让很多Go初学者抓狂的环境变量: GOPATH。事实上,如果对python比较熟悉的小伙伴应该很容易理解——其作用和 PYTHONPATH类似。

上文既然说了,包的引入是依靠包路径指明的,那路径的根目录在哪里呢?Nodejs会从该源码的目录开始向上逐层寻找 node_modules/文件夹并寻找该包,而Python和Go则依赖上述环境变量寻找。

GOPATH的目录层次如下:

--- $GOPATH/
 └---- src/
    └---- ... // 按路径存储各包
 └---- bin/
    └---- ... // 编译器会将编译、链接好的可执行文件放在这里
 └---- pkg/
    └---- ... // 编译器会将各个包的中间二进制文件放在这里

如上图所示,当我们import一个包的时候,编译器会在go预制包目录里寻找,若找不到则会在 $GOPATH/src/下寻找。而相对的包路径,在包的引入、编译、测试等操作时均遵循该准则获得绝对路径。如:

# 获取路径为github.com/levythu/gurgling的包且置于$GOPATH/src/github.com/levythu/gurgling/下
go get github.com/levythu/gurgling    

# 编译该包,中间文件在$GOPATH/src/github.com/levythu/gurgling.a
go install github.com/levythu/gurgling

尽管Go官方推荐采用固定的GOPATH,而后将所有go项目都分目录(也就是分包)放在 src/下,但我个人还是更喜欢分工程维护各自的GOPATH以达到更高相互的独立性。这种情况下,我会在每个工程的 bin/文件夹下维护一个 setenv脚本以快速设置GOPATH。如果不想每次新增工程都修改脚本内容,则可以用下述脚本:

#! /bin/bash
# for Linux

DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
export GOPATH="$DIR/../"
echo "Successfully change GOPATH to $GOPATH"
# for Windows

@SET GOPATH=%~dp0..\
@ECHO Successfully change path to %GOPATH%

这样,每次打开新的终端/cmd后,就可以直接运行该脚本设置好环境变量了。

包的发布和可执行文件的生成

我想发布一个包让他人使用,怎么做?

这是我个人认为Go在包管理器上做的最出彩的一点,当maven需要XML进行包信息说明和引入、pip和npm自己维护服务器进行包托管时,go已经支持git协议了! go get支持将任意git仓库克隆并视作包来管理,我们作为包的发布者,只需要将该包的源码托管在github上即可。

例如,Gurling的git仓库结构如下

--- gitroot for "https://github.com/levythu/Gurgling" /
 └---- midwares/
    └---- ...
 └---- routers/
    └---- ...
 └---- utils/
    └---- ...
 └---- LICENSE
 └---- readme.md
 └---- router.go
 └---- response.go
 └---- request.go
 └---- configure.go

则通过以下语句即可下载该包(需要 GOPATH设置好)

# 获取路径为github.com/levythu/gurgling的包且置于$GOPATH/src/github.com/levythu/gurgling/下
go get github.com/levythu/gurgling    

而后在自己的源码中可以直接使用

package main

import (
    . "github.com/levythu/gurgling"
)

func main() {
    // Create a root router.
    var router=ARouter()

    // Mount one handler
    router.Get(func(res Response) {
        res.Send("Hello, World!")
    })

    // Launch the server
    fmt.Println("Running...")
    router.Launch(":8080")
}

总而言之,Go的后发优势允许其提供极其开发者友好的包发布流程。大大节省了各类开发者的时间。

生成可执行文件

说到可执行文件,就不得不提到包名为 main的包(注意是包名,而非包路径)。Go编译器约定包名为 main的包拥有主函数 func main()并能编译成可执行文件。

继续以上文的包目录为例,假如 package1目录下的包包名为 main,则执行

go install package1

将会生成 bin/package1.exe(Windows)或 bin/package1(Linux),而非 pkg/package1.a。而生成的可执行文件正是以该包中的 main函数为程序入口的。

总结

Go的包管理和其他语言有所相同也有所不同,但总体来说几乎比其他所有语言都友好。一旦掌握,对开发效率的提升是很大的。而掌握其的最快方式,个人认为就是赶紧自己写个几百行的小工程,毕竟实践出真知。

最后,欢迎大家试用和fork我的web框架Gurgling~