Go语言36讲笔记--03库源码文件

145 阅读9分钟

what(库源码文件):

库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话)。
what(其他代码): 这里的其他代码可以与被使用的程序实体在同一个源码文件内,也可以在其他源码文件,甚至其他代码包中。
what(程序实体):
那么程序实体是什么呢?
在 Go 语言中,程序实体是变量、常量、函数、结构体和接口的统称
我们总是会先声明(或者说定义)程序实体,然后再去使用。
比如在上一篇的例子中,我们先定义了变量name,然后在main函数中调用fmt.Printf函数的时候用到了它。
再多说一点,程序实体的名字被统称为标识符。标识符可以是任何 Unicode 编码可以表示的字母字符、数字以及下划线“_”,但是其首字母不能是数字。从规则上说,我们可以用中文作为变量的名字。但是,我觉得这种命名方式非常不好,自己也会在开发团队中明令禁止这种做法。作为一名合格的程序员,我们应该向着编写国际水准的程序无限逼近。

今天的问题是:怎样把命令源码文件中的代码拆分到其他库源码文件?

举个栗子

package main
 
import (
	"flag"
)
 
var name string
 
func init() {
	flag.StringVar(&name, "name", "everyone", "The greeting object.")
}
 
func main() {
	flag.Parse()
	hello(name) //***
}

函数hello被声明在了另外一个源码文件中,我把它命名为 hello_lib.go,并且放在与 test.go 相同的目录下。如下:

// 需在此处添加代码。[1]
 
import "fmt"
 
func hello(name string) {
	fmt.Printf("Hello, %s!\n", name)
}

现在的问题是[1]这个位置需要添加什么包?

典型回答
答案很简单,填入代码包声明语句package main
为什么?我之前说过,在同一个目录下的源码文件都需要被声明为属于同一个代码包。

如果该目录下有一个命令源码文件,那么为了让同在一个目录下的文件都通过编译,其他源码文件应该也声明属于main包。

如此一来,我们就可以运行它们了。比如,我们可以在这些文件所在的目录下运行如下命令并得到相应的结果。

A:\goWorkAreas\src\test>go run test.go hello.go
Hello, everyone!

或者,像下面这样先构建当前的代码包再运行。

A:\goWorkAreas\src\test>go build test.go hello.go
//此时在test文件中生成了.exe文件
A:\goWorkAreas\src\test>test
Hello, everyone!

注意,test.go 和 hello.go 都声明自己属于main包。
在前面讲 Go 语言源码的组织方式的时候提到过这种用法,即:源码文件声明的包名可以与其所在目录的名称不同,只要这些文件声明的包名一致就可以。

问题解析(代码包声明的两个基本规则)

这个问题考察的是代码包声明的基本规则。
这里再总结一下。

  • 第一条规则,同目录下的源码文件的代码包声明语句要一致。也就是说,它们要同属于一个代码包。这对于所有源码文件都是适用的。如果目录中有命令源码文件,那么其他种类的源码文件也应该声明属于main包。这也是我们能够成功构建和运行它们的前提。

  • 第二条规则,源码文件声明的代码包的名称可以与其所在的目录的名称不同在针对代码包进行构建时,生成的结果文件的主名称与其父目录的名称一致。对于命令源码文件而言,构建生成的可执行文件的主名称会与其父目录的名称相同,这在我前面的回答中也验证过了。

下面的内容也将会与这两个规则相关。

在编写真正的程序时,我们仅仅把代码拆分到几个源码文件中是不够的。我们往往会用模块化编程的方式,根据代码的功能和用途把它们放置到不同的代码包中
不过,这又会牵扯进一些 Go 语言的代码组织规则。我们一起来往下看。

tips

用GOPATH安排新project的工作区,正确的用法是,你需要把该项目的打包文件下载到本地的任意目录下,然后经解压缩后把该目录加入到环境变量GOPATH中。还记得吗?这会使该目录成为工作区之一。(GOPATH中记录的就是各个工作区)


回答今天的问题

1. 怎样把命令源码文件中的代码拆分到其他代码包?

此时将test文件夹中的test.go放入q1文件夹,hello.go放入lib文件夹。为了让它们通过编译,我们应该怎样修改代码?

先修改hello.go

package lib5
 
import "fmt"
 
func Hello(name string) {
	fmt.Printf("Hello, %s!\n", name)
}

可以看到,我在这里修改了两个地方。第一个改动是,我把代码包声明语句由package main改为了package lib5。注意,我故意让声明的包名与其所在的目录的名称不同。第二个改动是,我把全小写的函数名hello改为首字母大写的Hello

基于以上改动,我们再来看下面的几个问题。

2. 代码包的导入路径总会与其所在目录的相对路径一致吗?

问题描述

库源码文件 hello.go 所在目录的相对路径是test/dummy/lib1,而它却声明自己属于lib5包。在这种情况下,该包的导入路径是test/dummy/lib1,还是test/dummy/lib5? 工程试验 首先,我们在构建或者安装这个代码包的时候,提供给go命令的路径应该是目录的相对路径,就像这样: go install test/dummy/lib1,该命令会成功完成。之后,当前工作区的 pkg 子目录下会产生相应的归档文件,具体的绝对路径是:A:\goWorkAreas\pkg\windows_amd64\test\dummy/lib1.a。其中的windows_amd64就是我在讲工作区时提到的平台相关目录。可以看到,这里与源码文件所在目录的相对路径是对应的。 继续 为了进一步说明问题,我需要先对 test.go 做两个改动。
第一个改动是,在以import为前导的代码包导入语句中加入test/dummy/lib1,也就是试图导入这个代码包。

第二个改动是,把对hello函数的调用改为对lib1.Hello函数的调用。其中的lib.叫做限定符,旨在指明右边的程序实体所在的代码包。不过这里与代码包导入路径的完整写法不同,只包含了路径中的最后一级lib,这与代码包声明语句中的规则一致。 此时还是会出现问题 A:\goWorkAreas\src\test\dummy\q1>go run test.go
#command-line-arguments
.\test.go:5:2: imported and not used: "test/dummy/q1/lib1" as lib5
.\test.go:16:2: undefined: lib1

第一个错误提示的意思是,我们导入了test/dummy/q1/lib1包,但没有实际使用其中的任何程序实体。这在 Go 语言中是不被允许的,在编译时就会导致失败。
这里的“as lib5”。说明虽然导入了代码包test/dummy/q1/lib1,但是使用其中的程序实体的时候应该以lib5.为限定符。这也就是第二个错误提示的原因了。Go 命令找不到lib1.这个限定符对应的代码包。

为什么会是这样?根本原因就是,我们在源码文件中声明所属的代码包与其所在目录的名称不同。请记住,源码文件所在的目录相对于 src 目录的相对路径就是它的代码包导入路径,而实际使用其程序实体时给定的限定符要与它声明所属的代码包名称对应

有两个方式可以使上述构建成功完成。我在这里选择把 hello.go 文件中的代码包声明语句改为package lib1。理由是,为了不让该代码包的使用者产生困惑,我们总是应该让声明的包名与其父目录的名称一致。

3. 什么样的程序实体才可以被当前包外的代码引用?

关于 hello.go 文件中的那个函数名称hello的首字母大写,实际上这涉及了 Go 语言中对于程序实体访问权限的规则。

超级简单,名称的首字母为大写的程序实体才可以被当前包外的代码引用,否则它就只能被当前包内的其他代码引用。

通过名称,Go 语言自然地把程序实体的访问权限划分为了包级私有的和公开的。对于包级私有的程序实体,即使你导入了它所在的代码包也无法引用到它。

4. 对于程序实体,还有其他的访问权限规则吗?

答案是肯定的。

在 Go 1.5 及后续版本中,我们可以通过创建internal代码包让一些程序实体仅仅能被当前模块中的其他代码引用。这被称为 Go 程序实体的第三种访问权限:模块级私有

具体规则是,internal代码包中声明的公开程序实体仅能被该代码包的直接父包及其子包中的代码引用。当然,引用前需要先导入这个internal包。对于其他代码包,导入该internal包都是非法的,无法通过编译。

总结

这里涉及了几条重要的 Go 语言基本编码规则

即:代码包声明规则、代码包导入规则以及程序实体的访问权限规则。在进行模块化编程时,你必须记住这些规则,否则你的代码很可能无法通过编译。

思考题

  1. 如果你需要导入两个代码包,而这两个代码包的导入路径的最后一级是相同的,比如:dep/lib/flagflag,那么会产生冲突吗?

这会产生冲突。因为代表两个代码包的标识符重复了,都是flag。

  1. 如果会产生冲突,那么怎样解决这种冲突?有几种方式?

接上一个问题。很简单,导入代码包的时候给它起一个别名就可以了,比如: import libflag "dep/lib/flag"。或者,以本地化的方式导入代码包,如:import . "dep/lib/flag"。