zhangxiang958 / Blog

阿翔的个人技术博客,博文写在 Issues 里,如有收获请 star 鼓励~
https://github.com/zhangxiang958/zhangxiang958.github.io/issues
152 stars 11 forks source link

Golang 包教不包会(二) Golang 中的包与作用域 #58

Open zhangxiang958 opened 4 years ago

zhangxiang958 commented 4 years ago

开始编写你的 golang 程序之前,你需要明白几个概念。

$GOPATH

什么是 $GOPATH? 它是一个环境变量,它指向的是下载的 golang 程序和一些你自己编写的 golang 程序的目录。

在 Mac 系统或者是 linux 系统可以使用命令来查看 $GOPATH 这个变量到底指向哪个目录:

go env | grep GOPATH

一般来说会在 /root/go 这个目录下。这个目录一般会有三个子目录:

如果你是使用 go1.11 以前的版本,go 是使用 $GOPATH 来管理依赖的,所以你的程序应该放在 src 目录下面,比如你的 github 账号是 zhangxiang958,那么你应该在 src 目录下面建一个 github.com 目录,然后再在其子目录下新建一个 zhangxiang958 的目录,然后假设你的代码文件夹是 demo,那么就将你编写的 go 文件放在这个目录下面(~/go/src/github.com/zhangxiang958/demo)。

Go Mod

go 在 1.11 版本之后,推出了 go mod 来管理依赖。

使用 go mod 的时候,不需要在 ~/go/src 下面放置我们的代码,我们只需要找到新建的目录,然后在该目录下执行:

go mod init xxxx # xxx 是你的项目名

然后你会得到一个文件 go.mod,里面的内容是:

module xxxx

go 1.13

新建了 go.mod 文件之后,你的依赖管理就使用 go mod 命令来管理,如果你新增一个依赖,那么在项目目录下还会出现 go.sum 文件

go get -v github.com/gammazero/deque

go.sum 文件内容类似如下,是用来记录包的依赖树的。

github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46 h1:iX4+rD9Fjdx8SkmSO/O5WAIX/j79ll3kuqv5VdYt9J8=
github.com/gammazero/deque v0.0.0-20200227231300-1e9af0e52b46/go.mod h1:D90+MBHVc9Sk1lJAbEVgws0eYEurY4mv2TDso3Nxh3w=

如果你熟悉 Node.js,这两者的关系就像在 Node 里面 package.json 和 package-lock.json 的关系一样。

编写年轻人的第一个 Golang 程序

这里我们默认使用 go1.11 以后版本,也就是使用 go mod 来管理我们的依赖,如上面篇章所说的,我们已经得到一个带有 go.mod 文件的目录了,接下来我们就在当前目录下新建一个 main.go 文件,我们现在尝试打印字符串 "Hello World":

// golang 里面注释可以使用 //
/**
    或者使用这个
    来表示多行注释
*/

// 文件一开头需要有一个 package 声明,这个 go 文件作为你新建的 xxx 这个包的入口文件,所以这里是 main
package main

// 这里是引入依赖包,fmt 是 golang 内置包,用来格式化处理输出的
import (
    "fmt"
)

// 定义 main 函数,golang 在执行的时候会自动找到 main 函数并执行。
func main() {
    // 使用 fmt.Println 方法,将 Hello World 字符串打印出来。
    fmt.Println("Hello World")
}

我们编写了上面的代码,接下来我们该怎么执行呢?我们有两种方式来执行:

  1. 使用 go run
go run main.go

执行上面这条命令,你可以直接在控制台看到打印出了 Hello World 字符串。

2.使用 go build

go build main.go
./xxxx # 执行二进制文件

你可以看到在 main.go 相对路径下,会多出一个二进制文件,二进制文件名一般和 go mod 定义的 package 名一致,然后即可执行这个二进制文件,得到的结果和 go run 一样。

go run 和 go build 有什么区别?go run 会将文件编译并且执行,而 go build 只是将文件进行编译。

为什么 package 要写 main?

就像 Node 里面,一个包会默认去找 index.js 一样,在 golang 里面,如果你想要一个可执行的二进制文件,那么你就需要将文件写在 main 包里面,并且命名一个 main 函数。

Go Package

上面说到 golang 的包,这里说的包不仅是指像 node 里面 npm 这样的包机制,还另指在 golang 里面怎么划分项目目录的。

使用 node 里面的 underscore 来举例,我们会有很多对于集合,array 做处理的文件,在 node 里面我们会这样:

- underscore
    - collection
        - each.js
        - find.js
    - array
        - first.js
        - flatten.js

文件的目录结构类似上面这样,在 golang 里面,我们是类似的:

- underscore
    - collection
        - each.go
        - find.go
    - array
        - first.go
        - flatten.go

每一个 .go 后缀文件,都需要属于某个包,也就是 package 名。而这里需要说明的是,你建立的包是一个库还是一个可执行函数?如果你建立的包是一个库,那么就可以不需要 main 包,而如果你需要的是一个可执行函数,需要打包构建出一个可执行二进制文件,那么就需要声明一个 main 包。库和可执行二进制包有什么区别?库是为了提高代码复用率,并且可以为任意合法的名字和可以被 import,二进制包是为了能够运行,并且必须提供 main 函数与 main 包,但不能被 import。

说到包名,一般来说就是 .go 文件所属的文件夹的名字,比如上面例子中的 each.go 和 find.go 都属于 collection 这个包,它们的 package 定义都是 package collection

一般来说,包名不使用下划线,中划线和大写字母,所以我们建目录的时候也需要注意这一点。

作用域

像 node 里面,变量定义了可以在哪里使用,a.js 能否调用 b.js 里面的方法或者变量都是通过作用域来决定的,golang 里面也有类似概念。

在同一个包里面,比如上面的 each.go 和 find.go,变量是可以不需要 import 就可以相互调用的。

比如在 each.go 定义一个 each 方法,在 find.go 中定义一个 find 方法:

package collection

import "fmt"

func each() {
    fmt.Println("each")
}
package collection

import "fmt"

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

然后在 find.go 中可以直接调用 each 方法:

func find() {
    each()
    fmt.Println("find")
}

这是因为这两个文件是属于同一个包的。

如果不在同一个包里面,比如上面目录结构中 array 目录与 collection 目录下的 go 文件怎么互相调用,下面 import 章节会讲到。

文件级作用域

像上面代码中,import 的 fmt,即

import "fmt"

这个引进来的 fmt 包,这个是文件级作用域,即只有当前这个文件可见,显然 each.go 和 find.go 都需要使用 fmt 这个包,但是他们需要每次都在自己文件引用一次,因为这种 import 是文件级的作用域。

包作用域

下面我们给 each.go 加一些代码:

package collection

import "fmt"

const isEachFile = true;

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

这里的 isEachFile 变量和 each 函数都是包作用域的,换言之,同一个包内的文件可以相互直接使用,比如 find.go 可以直接调用 each 函数,或者直接打印 isEachFile 这个变量。

块作用域

而在函数里面的就是块级作用域,如果在块级里面定义,那么只能在块级里面看到:

func each() {
    var isEach = true;
}

// 如果执行 forEach 函数会报错
func forEach() {
    // ERROR: undefined: isEach
    fmt.Println(isEach)
}

块级作用域的屏蔽

大家有可能会有疑问,如果包级的作用域定义了一个变量,然后块级里面也定义一个,那么会怎么样?

package collection

var isInEach = false

func each() {
    var isInEach = true
    fmt.Println("in each:", isInEach) // true
}

func forEach() {
    fmt.Println("in foreach:", isInEach) // false

    each()

    fmt.Println("in foreach:", isInEach) // false
}

块级定义的变量会屏蔽包级别的作用域定义的变量,并且只会影响块级的。当然如果你在块级里面不重新定义,也就是直接修改 isInEach 变量,是可以修改掉值的。

import

像上面的代码结构,collection 包的文件怎么引用 array 的包的文件呢?像 node 中,可以通过 require 解析相对路径来引用,但是在 golang 里面,会有一点特殊。

一般来说,如果我们想要自己定义的包里面的子包相互调用,也就是相当于上面 underscore 里面的 collection 引用 array 这样,underscore 根目录会定义一个 go.mod 文件,里面类似:

module github.com/xxx/underscore

这样相当于是包名,然后子包就使用这个前缀后面加上对应子包的路径即可,比如 array 使用 collection 包:

package array

import (
    // 这里 collection 是别名的意思,相当于给引入的包一个别名,下面的代码可以使用这个别名
    collection "github.com/xxx/underscore/collection"
)

func find() {
    collection.Find() // 注意,包如果需要暴露出方法或者变量,首字母要大写才能暴露(作用就像 node 里面的 exports)
}

这样就可以将 collection 包进入并使用了,可以使用 collection 暴露出来的方法或者变量。

Export

上面的代码例子里面,大部分代码都是没有暴露的,如果一个包里面的方法或者变量需要暴露给其他包使用,在 node 里面,我们会使用 module.exports 或者 exports,但是在 golang 里面,我们只需要将需要暴露出来的方法名或者变量名的首字母改写成大写,就可以被其他包使用了。

package collection

var IsEachFile = true
var privateVar = 1

// 包暴露的方法
func Each() {
    forEach();
}

// 包的私有方法
func forEach() {}