【Go语言学习】01 | 初窥门径

创建hello, world程序

创建hello, world文件夹

1
2
3
4
5

$mkdir ~/goprojects // 创建一个可以集合所有专栏项目的根文件夹
$cd ~/goprojects
$mkdir helloworld // 创建存储helloworld示例的文件夹
$cd helloworld

编写和运行第一个Go程序

首先,我们需要创建一个名为 main.go 的源文件。

Go 源文件总是用全小写字母形式的短小单词命名,并且以.go 扩展名结尾。

如果要在源文件的名字中使用多个单词,我们通常直接是将多个单词连接起来作为源文件名,而不是使用其他分隔符,比如下划线。也就是说,我们通常使用 helloworld.go 作为文件名而不是 hello_world.go。

1
2
3
4
5
6
7
8

package main

import "fmt"

func main() {
fmt.Println("hello, world")
}

保存文件,编译和运行

1
2
3
4

$go build main.go
$./main
hello, world

hello, world程序的结构

上述代码可以分成三部分剖析:

  1. package main

    这一行代码定义了 Go 中的一个包 package。包是 Go 语言的基本组成单元,通常使用单个的小写单词命名,一个 Go 程序本质上就是一组包的集合。所有 Go 代码都有自己隶属的包,在这里我们的“hello,world”示例的所有代码都在一个名为 main 的包中。main 包在 Go 中是一个特殊的包,整个 Go 程序中仅允许存在一个名为 main 的包

  2. main 函数

    1
    2
    3
    4

    func main() {
    fmt.Println("hello, world")
    }

    这里的 main 函数会比较特殊:当你运行一个可执行的 Go 程序的时候,所有的代码都会从这个入口函数开始运行。

  3. import “fmt”

    • import “fmt” 一行中“fmt”代表的是包的导入路径(Import),它表示的是标准库下的 fmt 目录,整个 import 声明语句的含义是导入标准库 fmt 目录下的包;

    • fmt.Println 函数调用一行中的“fmt”代表的则是包名。

    • 导入多个包可以使用圆括号

      1
      2
      3
      4
      import (
      "fmt"
      "strings"
      )

Go语言中程序是怎么编译的

Go 是一种编译型语言,这意味着只有你编译完 Go 程序之后,才可以将生成的可执行文件交付于其他人,并运行在没有安装 Go 的环境中。

1
go build main.go

当然,Go 也借鉴了动态语言的一些对开发者体验较好的特性,比如基于源码文件的直接执行,Go 提供了 run 命令可以直接运行 Go 源码文件,比如我们也可以使用下面命令直接基于 main.go 运行:

1
go run main.go

复杂项目下Go程序的编译

我们新建一个项目:

1
2
3
4

$cd ~/goprojects
$mkdir hellomodule
$cd hellomodule

hellomodule文件夹中创建并编辑main.go文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

package main

import (
"github.com/valyala/fasthttp"
"go.uber.org/zap"
)

var logger *zap.Logger

func init() {
logger, _ = zap.NewProduction()
}

func fastHTTPHandler(ctx *fasthttp.RequestCtx) {
logger.Info("hello, go module", zap.ByteString("uri", ctx.RequestURI()))
}

func main() {
fasthttp.ListenAndServe(":9090", fastHTTPHandler)
}

对于复杂项目的构建,我们采用 Go module

Go module 构建模式是在 Go 1.11 版本正式引入的,为的是彻底解决 Go 项目复杂版本依赖的问题,在 Go 1.16 版本中,Go module 已经成为了 Go 默认的包依赖管理机制和 Go 源码构建机制。

Go Module 的核心是一个名为 go.mod 的文件,在这个文件中存储了这个 module 对第三方依赖的全部信息。

通过 init来创建go.mod文件:

1
2
3
4
5

$go mod init github.com/alexhuihui/hellomodule
go: creating new go.mod: module github.com/bigwhite/hellomodule
go: to add module requirements and sums:
go mod tidy

让我们看看生成的go.mod文件中有什么:

1
2
3
4
5

$cat go.mod
module github.com/alexhuihui/hellomodule

go 1.17

其实,一个 module 就是一个包的集合,这些包和 module 一起打版本、发布和分发。go.mod 所在的目录被我们称为它声明的 module 的根目录。不过呢,这个时候的 go.mod 文件内容还比较简单,第一行内容是用于声明 module 路径(module path)的。而且,module 隐含了一个命名空间的概念,module 下每个包的导入路径都是由 module path 和包所在子目录的名字结合在一起构成。比如,如果 hellomodule 下有子目录 pkg/pkg1,那么 pkg1 下面的包的导入路径就是由 module path(github.com/alexhuihui/hellomodule)和包所在子目录的名字(pkg/pkg1)结合而成,也就是 github.com/alexhuihui/hellomodule/pkg/pkg1。另外,go.mod 的最后一行是一个 Go 版本指示符,用于表示这个 module 是在某个特定的 Go 版本的 module 语义的基础上编写的。

通过 go mod tidy,让 Go工具自动添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ go mod tidy
go: finding module for package go.uber.org/zap
go: finding module for package github.com/valyala/fasthttp
go: downloading go.uber.org/zap v1.20.0
go: downloading github.com/valyala/fasthttp v1.32.0
go: found github.com/valyala/fasthttp in github.com/valyala/fasthttp v1.32.0
go: found go.uber.org/zap in go.uber.org/zap v1.20.0
go: downloading go.uber.org/atomic v1.7.0
go: downloading go.uber.org/multierr v1.6.0
go: downloading github.com/pkg/errors v0.8.1
go: downloading github.com/stretchr/testify v1.7.0
go: downloading go.uber.org/goleak v1.1.11
go: downloading github.com/andybalholm/brotli v1.0.2
go: downloading github.com/klauspost/compress v1.13.4
go: downloading github.com/valyala/bytebufferpool v1.0.0
go: downloading gopkg.in/yaml.v2 v2.2.8
go: downloading github.com/davecgh/go-spew v1.1.1
go: downloading github.com/pmezard/go-difflib v1.0.0
go: downloading gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
go: downloading github.com/benbjohnson/clock v1.1.0

下载依赖的时候你可能会碰上超时的问题,执行以下命令即可:

1
$ go env -w GOPROXY=https://goproxy.cn,direct

go mod tidy执行之后,go.mod就变成了这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module github.com/bigwhite/hellomodule

go 1.17

require (
github.com/valyala/fasthttp v1.32.0
go.uber.org/zap v1.20.0
)

require (
github.com/andybalholm/brotli v1.0.2 // indirect
github.com/klauspost/compress v1.13.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
)

这个时候,go.mod 已经记录了 hellomodule 直接依赖的包的信息。不仅如此,hellomodule 目录下还多了一个名为 go.sum 的文件,这个文件记录了 hellomodule 的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。在构建的时候,如果本地依赖包的 hash 值与 go.sum 文件中记录的不一致,就会被拒绝构建。

有了 go.mod 以及 hellomodule 依赖的包版本信息后,我们再来执行构建:

1
2
3
4

$go build main.go
$ls
go.mod go.sum main main.go

成功构建后,执行main命令,然后新开一个窗口执行curl localhost:9090/foo/bar

1
2
3
$ ./main 
{"level":"info","ts":1643097830.6936307,"caller":"hellomodule/main.go:16","msg":"hello, go module","uri":"/foo/bar"}

总结

  • Go 包是 Go 语言的基本组成单元。一个 Go 程序就是一组包的集合,所有 Go 代码都位于包中;

  • Go 源码可以导入其他 Go 包,并使用其中的导出语法元素,包括类型、变量、函数、方法等,而且,main 函数是整个 Go 应用的入口函数;

  • Go 源码需要先编译,再分发和运行。如果是单 Go 源文件的情况,我们可以直接使用 go build 命令 +Go 源文件名的方式编译。不过,对于复杂的 Go 项目,我们需要在 Go Module 的帮助下完成项目的构建。