04|初窥门径:一个Go程序的结构是怎样的?

你好,我是Tony Bai。

经过上一讲的学习,我想现在你已经成功安装好至少一个Go开发环境了,是时候撸起袖子开始写Go代码了!

程序员这个历史并不算悠久的行当,却有着一个历史悠久的传统,那就是每种编程语言都将一个名为“hello, world”的示例作为这门语言学习的第一个例子,这个传统始于20世纪70年代那本大名鼎鼎的由布莱恩·科尼根(Brian W. Kernighan)与C语言之父丹尼斯·里奇(Dennis M. Ritchie)合著的《C程序设计语言》。

图片

在这一讲中,我们也将遵从传统,从编写一个可以打印出“hello, world”的Go示例程序开始我们正式的Go编码之旅。我希望通过这个示例程序你能够对Go程序结构有一个直观且清晰的认识。

在正式开始之前,我要说明一下,我们这节课对你开发Go程序时所使用的编辑器工具没有任何具体的要求。

如果你喜欢使用某个集成开发环境(Integrated Development Environment,IDE),那么就用你喜欢的IDE好了。如果你希望我给你推荐一些好用的IDE,我建议你试试 GoLandVisual Studio Code(简称VS Code)。GoLand是知名IDE出品公司JetBrains针对Go语言推出的IDE产品,也是目前市面上最好用的Go IDE;VS Code则是微软开源的跨语言源码编辑器,通过集成语言插件(Go开发者可以使用Go官方维护的 vscode-go插件),可以让它变成类IDE的工具。

如果你有黑客情怀,喜欢像黑客一样优雅高效地使用命令行,那么像Vim、Emacs这样的基于终端的编辑器同样可以用于编写Go源码。以Vim为例,结合 vim-gococ.nvim(代码补全)以及Go官方维护的gopls 语言服务器,你在编写Go代码时同样可以体会到“飞一般”的感觉。但在我们这门课中,我们将尽量使用与编辑器或IDE无关的说明。

好,我们正式开始吧。

创建“hello,world”示例程序

在Go语言中编写一个可以打印出“hello,world”的示例程序,我们只需要简单两步,一是创建文件夹,二是开始编写和运行。首先,我们来创建一个文件夹存储编写的Go代码。

创建“hello,world”文件夹

通常来说,Go不会限制我们存储代码的位置(Go 1.11之前的版本另当别论)。但是针对我们这门课里的各种练习和项目,我还是建议你创建一个可以集合所有项目的根文件夹(比如:~/goprojects),然后将我们这门课中所有的项目都放在里面。

现在,你可以打开终端并输入相应命令,来创建我们用于储存“hello,world”示例的文件夹helloworld了。对于Linux系统、macOS系统,以及Windows系统的PowerShell终端来说,用下面这个命令就可以建立helloworld文件夹了:

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

建好文件夹后,我们就要开始编写我们第一个Go程序了。

编写并运行第一个Go程序

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

这里,我需要跟你啰嗦一下Go的命名规则。Go源文件总是用全小写字母形式的短小单词命名,并且以.go扩展名结尾。

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

这是因为下划线这种分隔符,在Go源文件命名中有特殊作用,这个我们会在以后的讲解中详细说明。总的来说,我们尽量不要用两个以上的单词组合作为文件名,否则就很难分辨了。

现在,你可以打开刚刚创建的main.go文件,键入下面这些代码:

package main

import "fmt"

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

写完后,我们保存文件并回到终端窗口,然后在Linux或macOS系统中,你就可以通过输入下面这个命令来编译和运行这个文件了:

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

如果是在Windows系统中呢,你需要把上面命令中的./main替换为.\main.exe。

>go build main.go
>.\main.exe
hello, world

不过,无论你使用哪种操作系统,到这里你都应该能看到终端输出的“hello, world”字符串了。如果你没有看到这个输出结果,要么是Go安装过程的问题,要么是源文件编辑出现了问题,需要你再次认真地确认。如果一切顺利,那么恭喜你!你已经完成了第一个Go程序,并正式成为了Go开发者!欢迎来到Go语言的世界!

“hello,world”示例程序的结构

现在,让我们回过头来仔细看看“hello,world”示例程序中到底发生了什么。第一个值得注意的部分是这个:

package main

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

main包中的主要代码是一个名为main的函数:

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

这里的main函数会比较特殊:当你运行一个可执行的Go程序的时候,所有的代码都会从这个入口函数开始运行。这段代码的第一行声明了一个名为main的、没有任何参数和返回值的函数。如果某天你需要给函数声明参数的话,那么就必须把它们放置在圆括号()中。

另外,那对花括号{}被用来标记函数体,Go要求所有的函数体都要被花括号包裹起来。按照惯例,我们推荐把左花括号与函数声明置于同一行并以空格分隔。Go语言内置了一套Go社区约定俗称的代码风格,并随安装包提供了一个名为Gofmt的工具,这个工具可以帮助你将代码自动格式化为约定的风格。

Gofmt是Go语言在解决规模化(scale)问题上的一个最佳实践,并成为了Go语言吸引其他语言开发者的一大卖点。很多其他主流语言也在效仿Go语言推出自己的format工具,比如:Java formatter、Clang formatter、Dartfmt等。因此,作为Go开发人员,请在提交你的代码前使用Gofmt格式化你的Go源码。

好,回到正题,我们再来看一看main函数体中的代码:

fmt.Println("hello, world")

这一行代码已经完成了整个示例程序的所有工作了:将字符串输出到终端的标准输出(stdout)上。不过这里还有几个需要你注意的细节。

注意点1:标准Go代码风格使用Tab而不是空格来实现缩进的,当然这个代码风格的格式化工作也可以交由gofmt完成。

注意点2:我们调用了一个名为Println的函数,这个函数位于Go标准库的fmt包中。为了在我们的示例程序中使用fmt包定义的Println函数,我们其实做了两步操作。

第一步是在源文件的开始处通过import声明导入fmt包的包路径:

import "fmt"

第二步则是在main函数体中,通过fmt这个限定标识符(Qualified Identifier)调用Println函数。虽然两处都使用了“fmt”这个字面值,但在这两处“fmt”字面值所代表的含义却是不一样的:

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

通常导入路径的最后一个分段名与包名是相同的,这也很容易让人误解import声明语句中的“fmt”指的是包名,其实并不是这样的。

main函数体中之所以可以调用fmt包的Println函数,还有最后一个原因,那就是Println函数名的首字母是大写的。在Go语言中,只有首字母为大写的标识符才是导出的(Exported),才能对包外的代码可见;如果首字母是小写的,那么就说明这个标识符仅限于在声明它的包内可见。

另外,在Go语言中,main包是不可以像标准库fmt包那样被导入(Import)的,如果导入main包,在代码编译阶段你会收到一个Go编译器错误:import “xx/main” is a program, not an importable package。

注意点3:我们还是回到main函数体实现上,把关注点放在传入到Println函数的字符串“hello, world”上面。你会发现,我们传入的字符串也就是我们执行程序后在终端的标准输出上看到的字符串

这种“所见即所得”得益于Go源码文件本身采用的是Unicode字符集,而且用的是UTF-8标准的字符编码方式,这与编译后的程序所运行的环境所使用的字符集和字符编码方式是一致的。

这里,即便我们将代码中的"hello, world"换成中文字符串“你好,世界”,像下面这样:

package main

import "fmt"

func main() {
    fmt.Println("你好,世界")
}

我们依旧可以在终端的标准输出上看到正确的输出。

最后,不知道你有没有发现,我们整个示例程序源码中,都没有使用过分号来标识语句的结束,这与C、C++、Java那些传统编译型语言好像不太一样呀?

不过,其实Go语言的正式语法规范是使用分号“;”来做结尾标识符的。那为什么我们很少在Go代码中使用和看到分号呢?这是因为,大多数分号都是可选的,常常被省略,不过在源码编译时,Go编译器会自动插入这些被省略的分号。

我们给上面的“hello,world”示例程序加上分号也是完全合法的,是可以直接通过Go编译器编译并正常运行的。不过,gofmt在按约定格式化代码时,会自动删除这些被我们手工加入的分号的。

在分析完这段代码结构后,我们来讲一下Go语言的编译。虽然刚刚你应该已经运行过“hello, world”这个示例程序了,在这过程中,有一个重要的步骤——编译,现在我就带你来看看Go语言中程序是怎么进行编译的。

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

你应该也注意到了,刚刚我在运行"hello, world"程序之前,输入了go build命令,还有它附带的源文件名参数来编译它:

$go build main.go

假如你曾经有过C/C++语言的开发背景,那么你就会发现这个步骤与gcc或clang编译十分相似。一旦编译成功,我们就会获得一个二进制的可执行文件。在Linux系统、macOS系统,以及Windows系统的PowerShell中,我们可以通过输入下面这个ls命令看到刚刚生成的可执行文件:

$ls
main*		main.go

上面显示的文件里面有我们刚刚创建的、以.go为后缀的源代码文件,还有刚生成的可执行文件(Windows系统下为main.exe,其余系统下为main)。

如果你之前更熟悉某种类似于Ruby、Python或JavaScript之类的动态语言,你可能还不太习惯在运行之前需要先进行编译的情况。Go是一种编译型语言,这意味着只有你编译完Go程序之后,才可以将生成的可执行文件交付于其他人,并运行在没有安装Go的环境中。

而如果你交付给其他人的是一份.rb、.py或.js的动态语言的源文件,那么他们的目标环境中就必须要拥有对应的Ruby、Python或JavaScript实现才能解释执行这些源文件。

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

$go run main.go
hello, world

当然像go run这类命令更多用于开发调试阶段,真正的交付成果还是需要使用go build命令构建的。

但是在我们的生产环境里,Go程序的编译往往不会像我们前面,基于单个Go源文件构建类似“hello,world”这样的示例程序那么简单。越贴近真实的生产环境,也就意味着项目规模越大、协同人员越多,项目的依赖和依赖的版本都会变得复杂。

那在我们更复杂的生产环境中,go build命令也能圆满完成我们的编译任务吗?我们现在就来探讨一下。

复杂项目下Go程序的编译是怎样的

我们还是直接上项目吧,给go build 一个机会,看看它的复杂依赖管理到底怎么样。

现在我们创建一个新项目“hellomodule”,在新项目中我们将使用两个第三方库,zap和fasthttp,给go build的构建过程增加一些难度。和“hello,world”示例一样,我们通过下面命令创建“hellomodule”项目:

$cd ~/goprojects
$mkdir hellomodule
$cd hellomodule

接着,我们在“hellomodule“下创建并编辑我们的示例源码文件:

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(":8081", fastHTTPHandler)
}

这个示例创建了一个在8081端口监听的http服务,当我们向它发起请求后,这个服务会在终端标准输出上输出一段访问日志。

你会看到,和“hello,world“相比,这个示例显然要复杂许多。但不用担心,你现在大可不必知道每行代码的功用,你只需要我们在这个稍微有点复杂的示例中引入了两个第三方依赖库,zap和fasthttp就可以了。

我们尝试一下使用编译“hello,world”的方法来编译“hellomodule”中的main.go源文件,go编译器的输出结果是这样的:

$go build main.go
main.go:4:2: no required module provides package github.com/valyala/fasthttp: go.mod file not found in current directory or any parent directory; see 'go help modules'
main.go:5:2: no required module provides package go.uber.org/zap: go.mod file not found in current directory or any parent directory; see 'go help modules'

看这结果,这回我们运气似乎不佳,main.go的编译失败了!

从编译器的输出来看,go build似乎在找一个名为go.mod的文件,来解决程序对第三方包的依赖决策问题。

好了,我们也不打哑谜了,是时候让Go module登场了!

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

Go Module的核心是一个名为go.mod的文件,在这个文件中存储了这个module对第三方依赖的全部信息。接下来,我们就通过下面命令为“hello,module”这个示例程序添加go.mod文件:

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

你会看到,go mod init命令的执行结果是在当前目录下生成了一个go.mod文件:

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

go 1.16

其实,一个module就是一个包的集合,这些包和module一起打版本、发布和分发。go.mod所在的目录被我们称为它声明的module的根目录。

不过呢,这个时候的go.mod文件内容还比较简单,第一行内容是用于声明module路径(module path)的。而且,module隐含了一个命名空间的概念,module下每个包的导入路径都是由module path和包所在子目录的名字结合在一起构成。

比如,如果hellomodule下有子目录pkg/pkg1,那么pkg1下面的包的导入路径就是由module path(github.com/bigwhite/hellomodule)和包所在子目录的名字(pkg/pkg1)结合而成,也就是github.com/bigwhite/hellomodule/pkg/pkg1。

另外,go.mod的最后一行是一个Go版本指示符,用于表示这个module是在某个特定的Go版本的module语义的基础上编写的。

有了go.mod后,是不是我们就可以构建hellomodule示例了呢?

来试试看!我们执行一下构建,Go编译器输出结果是这样的:

$go build main.go
main.go:4:2: no required module provides package github.com/valyala/fasthttp; to add it:
	go get github.com/valyala/fasthttp
main.go:5:2: no required module provides package go.uber.org/zap; to add it:
	go get go.uber.org/zap

你会看到,Go编译器提示源码依赖fasthttp和zap两个第三方包,但是go.mod中没有这两个包的版本信息,我们需要按提示手工添加信息到go.mod中。

这个时候,除了按提示手动添加外,我们也可以使用go mod tidy命令,让Go工具自动添加:

$go mod tidy       
go: downloading go.uber.org/zap v1.18.1
go: downloading github.com/valyala/fasthttp v1.28.0
go: downloading github.com/andybalholm/brotli v1.0.2
... ...

从输出结果中,我们看到Go工具不仅下载并添加了hellomodule直接依赖的zap和fasthttp包的信息,还下载了这两个包的相关依赖包。go mod tidy执行后,我们go.mod的最新内容变成了这个样子:

module github.com/bigwhite/hellomodule

go 1.16

require (
	github.com/valyala/fasthttp v1.28.0
	go.uber.org/zap v1.18.1
)

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

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

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

这次我们成功构建出了可执行文件main,运行这个文件,新开一个终端窗口,在新窗口中使用curl命令访问该http服务:curl localhost:8081/foo/bar,我们就会看到服务端输出如下日志:

$./main
{"level":"info","ts":1626614126.9899719,"caller":"hellomodule/main.go:15","msg":"hello, go module","uri":"/foo/bar"}

这下,我们的“ hellomodule”程序可算创建成功了。我们也看到使用Go Module的构建模式,go build完全可以承担其构建规模较大、依赖复杂的Go项目的重任。还有更多关于Go Module的内容,我会在第7节课再详细跟你讲解。

小结

到这里,我们终于亲手编写完成了Go语言的第一个程序“hello, world”,我们终于知道一个Go程序长成啥样子了,这让我们在自己的Go旅程上迈出了坚实的一步!

在这一节课里,我们通过helloworld示例程序,了解了一个Go程序的源码结构与代码风格自动格式化的约定。

我希望你记住这几个要点:

  • Go包是Go语言的基本组成单元。一个Go程序就是一组包的集合,所有Go代码都位于包中;
  • Go源码可以导入其他Go包,并使用其中的导出语法元素,包括类型、变量、函数、方法等,而且,main函数是整个Go应用的入口函数;
  • Go源码需要先编译,再分发和运行。如果是单Go源文件的情况,我们可以直接使用go build命令+Go源文件名的方式编译。不过,对于复杂的Go项目,我们需要在Go Module的帮助下完成项目的构建。

最后,我们结合hellomodule示例初步学习了一个基于Go Module构建模式编写和构建更大规模Go程序的步骤并介绍了Go Module涉及到的各种概念。而且,Go Module机制日渐成熟,我希望你学会基于Go Module构建Go应用。关于Go Module构建模式,我们还会在后面的讲解中详细介绍。

思考题

今天我给你留了一道思考题,经过今天这节课,你喜欢Go统一的代码风格吗?你觉得Go这么做的利弊都有哪些呢?欢迎在留言区和我探讨。

欢迎你把这节课分享给更多对Go语言学习感兴趣的朋友。我是Tony Bai,我们下节课见。

精选留言

  • mazhen

    2021-10-20 11:16:42

    如何import自己在本地创建的module,在这个module还没有发布到GitHub的情况下?
    作者回复

    非常好的问题。go module机制在您提到的工作场景下目前的体验做的还不够好。在Go 1.17版本及之前版本的解决方法是使用go mod的replace指示符(directive)。假如你的module a要import的module b将发布到github.com/user/repo中,那么你可以手动在module的go.mod中的require块中手工加上一条:

    require github.com/user/repo v1.0.0

    注意v1.0.0这个版本号是一个临时的版本号。

    然后在module a的go.mod中使用replace将上面对module b的require替换为本地的module b:

    replace github.com/user/repo v1.0.0 => module b本地路径

    这样go命令就会使用你本地正在开发、尚未提交github的module b了。

    你可以试试。

    2021-10-21 06:00:35

  • return

    2021-10-20 10:42:54

    老师讲的太好了, 重点细节 都很到位,赞啊。
    请教老师:
    如果 路径和包名不一样, path: apath, package: apack
    那么使用的时候是这样吗?
    import "apath"
    apack.Print()
    作者回复

    你的理解非常正确,给你点个赞!

    2021-10-21 06:01:42

  • 多选参数

    2021-11-14 23:45:32

    go 引用了其他包的话,是将引用的包都编译进去。我用 ldd 才看几个 go 编译出来的二进制程序都是没有动态链接库的使用。但是,在看其他几个 go 编译出来的二进制程序时(比如 containerd、ctr,它们都是用 go 编写的),又有引用动态链接库,这个是为什么?
    作者回复

    go默认是开启CGO_ENABLED的,即CGO_ENABLED=1。但编译出来的二进制程序究竟有无动态链接,取决于你的程序使用了什么包。如果就是一个hello,world,那么你编译出来的将是一个纯静态程序。

    如果你依赖了网络包或一些系统包,比如用http包编写了一个web server(见第9讲示例),那么你编译出来的二进制程序又会是一个包含动态链接的程序。

    原因就在于目前的go标准库中,某些功能具有两份实现,一份是c语言实现的,一份是go语言实现的。在cgo_enable开启的情况下,go链接器会链接c语言的版本,于是就有了依赖动态链接库的情况。如果你将cgo_enabled置为0,你再重新编译链接,那么go链接器会使用go版本的实现,这样你将得到一个没有动态链接的纯静态二进制程序。

    2021-11-15 17:48:38

  • Hans

    2021-10-21 00:21:20

    配置国内镜像代理(使用阿里云镜像)
    go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/
    验证
    go env|grep GOPROXY
    作者回复

    嗯,国内的几大goproxy服务都是可以的。

    2021-10-21 05:19:24

  • william

    2021-10-20 23:40:38

    前面能看懂,后面就开始迷了。。。
    Go Module 的概念我还是不怎么理解,
    很多教程都用github.com... 做例子, 你说的是”路径“,路径不应该是
    (win10)"D:\goprojects\hellomodule" 吗?我自己自定义一个任意的名字也可以正常运行?我感觉很奇怪,为什么很多人用github.com/...做module呢?
    还有go mod tidy 命令后,下载的包在哪?在Go语言安装路径的src文件夹中?还是在哪?
    作者回复

    1. 使用github.com/...作为module path是因为多数实用级module多是上传到github上的。用这种示例便于后续与真实生产接驳。但对于本地开发使用的简单示例程序而言,你说的没错。module path可以任意起,比如:

    module demo1

    也是ok的。

    第二个问题,go mod tidy下载的第三方包一般在$GOPATH/pkg/mod下面。如果没有设置GOPATH环境变量,其默认值为你的home路径下的go文件夹。这样第三方包就在go文件夹的pkg/mod下面。

    2021-10-21 06:15:10

  • 自由

    2021-10-20 22:22:46

    关于思考题,我认为统一的代码风格是很有必要的。

    在多人协作时,我们的代码存放在 git 仓库中,git 的代码提交记录,文件中的缩进和折行也会被视为改动。假如两位同学的格式化风格不同,提交代码时,一个文件中的实际改动与 git 记录的改动将会相差甚远!100 行代码就会多出 100 行改动,而实际上,有效改动可能仅仅是几行代码,在追溯问题时,带来很多不必要的麻烦。
    作者回复

    作为一个多年gopher的心底话,现在go开发已经完全离不开gofmt或类似工具了。并且这些标准风格格式化工具与各种编辑器或IDE结合后,是在“quiet模式”下帮助开发者,并不需要开发者付出额外的工作量。

    2021-10-21 05:23:19

  • lesserror

    2021-10-20 23:51:13

    感谢Tony Bai老师这么细致的讲解。能够基于最新的Go Module来讲解,正好是我想要的。
    之前看了太多基于go path的教程。感觉Go Module比之前的go path清晰多了。

    有几个问题想要了解下:

    1. 老师这节课的源码目录为什么不是类似:src/github.com/xxx 这样的方式在本地构建呢? 我看很多地方,建议以这种方式来创建项目。

    2. go mod tidy 这个命令中的 tidy 该如何翻译比较准确呢? 这个命令平时还挺常用的,希望结合中文翻译去加深记忆。

    ps: 希望后面老师能够,结合Go项目的代码访问路径,来系统讲解一下,代码的访问流程。
    作者回复


    问题1:go module与gopath的一个重要区别就是可以将项目放在任意路径下,而无需局限在$GOPATH/src下面。我之所以将一个module放在一个任意路径下,就是故意要与GOPATH模式区分开的。

    问题2:有些时候,无论怎么翻译都不能很理想的呈现英文作者的原意,因此向go mod tidy我倾向于直接使用原文,而不要翻译为中文。

    关于你的建议,我后续会重点考虑的,感谢。

    2021-10-21 19:42:34

  • 隰有荷

    2021-11-05 02:19:37

    老师,我有些不理解的地方在于:

    1、
    1-1、go mod init 后面跟的路径规则是什么样的?
    1-2、这个命令本身是什么含义?
    1-3、文中所说的module,到底哪个是module,是go.mod文件是module,还是hellomodule这个文件夹是module?因为我看文章中说当前文件夹是module的根目录,所以这里不太理解。

    2、我在执行go mod tidy 之后出现报错:go: warning: "all" matched no packages。但是我每一步都和文章中的步骤一样,请问是哪里出现了问题?
    作者回复

    1-1. go mod init后面的路径就是go.mod中module后面的路径,代表的是module path。
    1-2. go mod init命令的实际行为就是在当前目录下创建一个go.mod,而这个go.mod将当前目录转换为一个go module。
    1-3. go module是一个逻辑概念。文中也说了,它更像一个命名空间的概念。它与文件夹名称无关。有了go.mod后,这个文件夹下的各个包就算是这个go module下面的包了。包的导入路径也是以module path为前缀的。

    2. 确认一下你本地的go编译器版本?确认go module构建模式已开启。确认一下是否在go.mod所在目录执行的go mod tidy?

    2021-11-09 09:32:14

  • Demon.Lee

    2021-10-26 14:08:30

    如果与面向对象语言进行类比,go 语言中的包,是否可以类比成Java中的类呢?
    作者回复

    如果从编译的角度来讲,go包是编译的基本单位,每个go包编译为一个.a文件,每个java public class编译为一个class文件。它们的确有些类似。但从内容结构上来说,go包是一个命名空间的概念,可以包含go所有语法元素。java class就是一个拥有多个属性+方法的抽象。

    2021-10-26 16:10:40

  • Aeins

    2022-05-24 14:42:40

    go.mod 文件中 module 指令设置模块的模块路径,可以是任意的,不需要和文件系统对应。建议和项目目录名相同

    包的导入路径为模块路径和相对模块根目录(go.mod所在目录)的相对路径组成

    包名由 package 语句指定,建议和包的目录名相同(也可以不同)

    模块和包都对应了一个文件系统目录
    作者回复

    👍

    2022-05-24 16:25:34

  • 张一驰

    2023-01-12 00:56:18

    文中提到“这是因为下划线这种分隔符,在 Go 源文件命名中有特殊作用,这个我们会在以后的讲解中详细说明。”
    请问老师下划线的特殊作用是用于测试文件(xxx_test.go)吗?我在后续的章节没看到这个特殊作用的讲解(可能是我没有注意到这个细节)。
    作者回复

    真是非常细心!真不好意思,这个你并没有看错。最初考虑将单元测试相关内容放在后面讲解中,后来取消了,于是这块"特殊作用"的讲解就“没能成行”。

    这里简单补充一下:

    在Go中我们针对包(package)编写测试代码。测试代码与包代码放在同一个包目录下,并且Go要求所有测试代码都存放在以*_test.go结尾的文件中。这使Go开发人员一眼就能分辨出哪些文件存放的是包代码,哪些文件存放的是针对该包的测试代码。

    执行单元测试时,go test命令会将所有包目录下的*_test.go文件编译成一个临时二进制文件(我们可以通过go test -c显式编译出该文件),并执行该文件,后者将执行各个测试源文件中的名字格式为TestXxx函数所代表的测试用例并输出测试执行结果。

    当然go不仅有_test,还有以os、cpu架构为特殊后缀的文件,比如:signal_linux_amd64.go(Go runtime包下的文件)。文件名中的linux、amd64用于限制该文件参与编译的os和平台。以signal_linux_amd64.go为例,该文件仅在linux x86-64平台上才会参与编译。

    2023-01-13 15:38:05

  • 进化菌

    2021-10-25 22:23:32

    Go 的文件名原来不偏好下划线连接,细节~
    Go 的编译,看起来比解析型的语言麻烦,但是跑起来之后,感觉像是打开了一扇大门。
  • aoe

    2021-10-20 20:53:45

    Go Module看起来很好用啊!
    作者回复

    go module已经是go官方标准包依赖管理和构建模式了,所以从go入门开始就建议直接使用go module。gopath模式可以简单了解一下,然后忽略掉就好了。

    2021-10-21 05:25:30

  • Rayjun

    2021-10-20 10:07:28

    老师,提个问题,有了 Go Module 之后, GoPath 还有存在的必要么?
    作者回复

    go官方有移除gopath的打算。目前这个时间点,学习go基本不需要了解太多gopath了。

    2021-10-21 06:33:49

  • GAC·DU

    2021-10-20 08:21:52

    Go编译模式很好的解决了环境问题,就像docker镜像一样,一包在手,天下可跑,不再需要考虑环境的问题,实现了宏观上的标准化。不过有一个疑惑,如果一个大项目需要引用很多外部项目,编译的包会不会过大,因此不利于项目的分发部署?
    作者回复

    无论任何静态编程语言,引用外部依赖越多,最终编译生成的可执行文件size都会越大。毕竟要把真正依赖的包的目标代码放入可执行文件中。这个没招啊。

    2021-10-21 06:36:48

  • holly

    2021-11-02 16:05:00

    老师,您好,我是本地构建的,在helloworld的上一级目录执行, go mod init helloworld,再执行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.19.1
    go: downloading github.com/valyala/fasthttp v1.31.0
    go: found github.com/valyala/fasthttp in github.com/valyala/fasthttp v1.31.0
    go: found go.uber.org/zap in go.uber.org/zap v1.19.1
    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 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-0.20210813005559-691160354723
    go: downloading github.com/benbjohnson/clock v1.1.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.mod 中内容也多出几个
    module helloworld

    go 1.17

    require (
    github.com/valyala/fasthttp v1.31.0
    go.uber.org/zap v1.19.1
    )

    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 1.17会将indirect依赖都列出来,与Go 1.16不同。

    2021-11-03 17:45:17

  • tequ1lAneio

    2021-10-20 10:25:12

    风格统一的确会给团队协作带来的极大的便利,但同时也会导致一部分人因为厌恶统一后的代码风格选择离开。
    作者回复

    感觉风格统一是未来编程语言的大趋势。很高兴你留在了go阵营:)

    2021-10-21 06:40:25

  • 马以

    2022-04-21 16:43:18

    执行 go mod tidy 被墙了同学请看这里
    $ go env -w GOPROXY=https://goproxy.cn

    执行一下上面的命令即可
    作者回复

    👍

    2022-04-21 22:26:47

  • William Ning

    2022-02-17 15:55:37

    文章与评论看完,感觉收获不少。
    作者回复

    👍

    2022-02-22 22:11:36

  • Vettel

    2021-12-30 10:30:25

    个人觉得代码格式的统一是很好的,就拿Java的import的来说,有人喜欢import * 有人不喜欢,就单纯一个import就可能浪费很多解决代码冲突的时间
    作者回复

    👍

    2022-01-12 22:20:51