本文永久链接 – https://tonybai.com/go-course-faq

《Go语言第一课》专栏正式上线后收到了很多读者的留言反馈,很多留言中的问题显然都是大家认真思考过提出的,在专栏后台我也尽可能地做出认真细致的回答。这些问题以及我的回答也算是我和专栏学习者基于专栏的二次创作,于是我有了将这些问题作为FAQ集中记录起来的想法,这就是这篇文章的由来。

本页面内容将持续更新!请持续关注本FAQ永久链接 – https://tonybai.com/go-course-faq。

一. 本人相关

  • 关于音频中带有地方特色的口音^_^

这也是我第一次录带有音频的专栏,虽然音频老师给与了多次耐心的讲解,但毕竟不是专业的,在音频技巧方面还有提高空间。

有些童鞋听出了我的地方特色的口音,这个我得承认,而且这个不是短期能修正的^_^。我是辽宁人,辽宁一些地区的地方特色口音就是平翘舌界限不清晰,这里还希望大家海涵。

二. Go专栏

  • 为什么要出这个Go专栏?

为此,我特意写了一篇简短的文章,叙述了这门Go专栏诞生幕后的那些事,感兴趣的朋友可以去看看。

  • 专栏的更新节奏

根据极客时间要求,专栏每周更新三篇。希望我能保持生产力,争取不断更,压力山大啊!

  • 是否有针对该专栏的交流群

目前暂没有。作者精力有限,能力有限,不适合维护这样的一个群,希望大家体谅。欢迎大家在专栏积极留言,我会认真解答大家问题的。

  • 阅读完该专栏,我是否可以得到地道的Go代码编写风格、优雅的Go编程姿势呢?

虽然这门课的定位是入门课,而并非进阶课,但我在课程讲解以及Go示例代码中都会尽力以native的Go代码去呈现。并且课程讲解穿插着一些关于Go编码的最佳实践建议,希望大家在阅读后能有收获。

btw,要写出native 的Go代码,一定要多读高质量Go代码,Go标准库是一个最好的选择。俗话说:”熟读唐诗三百首,不会作诗也会吟”,多读高质量代码,与此有异曲同工之妙。

  • 专栏讲解使用的是Go最新稳定版本吗?

专栏写作开始于Go 1.17正式发布之前,因此早期的一些篇章使用的可能是Go 1.16.x版本,后期使用的几乎都是最新的Go 1.17.x。即便有一些引用的标准库或运行时的代码是Go 1.17之前的某个版本的,我这里也可以保证这些代码的引用仅是为了说明某个具体知识点,不会影响到大家的理解。

一个可能影响大家实践的问题就是从Go 1.17版本开始,go get不再用于安装某个Go版本或工具,我们要用Go install。Go install命令在之前的版本中几乎被弱化到“基本不用”的尴尬境地,其角色都被go get的光环所掩盖。

但从Go 1.17开始,Go install又恢复了其本职工作。

  • 老师你好,我想咨询个问题,在听您课的过程中还可以和哪些资料一起参考学习,学习进度可能快些的。

如果富有余力,可以采用“同主题阅读”方法,即确定一个主题,可以以专栏某一讲为主题,然后找几本相关内容的书籍同时阅读,这样可以对这个主题有更深入的了解和认识。建议看一下Go语言的规范、《Go程序设计语言》这样的权威资料。

如果想加快学习和理解的节奏,建议通过预习方式来进行。本专栏的大纲已经公开,可根据大量中的题目,预习一下后续几节课的
内容,带着问题去阅读专栏,可能学习效果更好。

三. 入坑Go与Go前景

  • 什么样的人适合学Go语言?

Go设计之初,其目标是成为一门通用的系统编程语言。这一目标基本上就将Go划分到后端编程语言行列。虽然Go社区在前端、移动端编程的支持上面都做了很多尝试,比如:Gopherjs项目以及Go支持编译为webassembly来应对前端开发,再比如Gomobile项目(https://pkg.go.dev/Golang.org/x/mobile)让Go也可以在移动端编程占有一席之地,但这么多年下来,Go的主力战场还是云原生基础设施、中间件、web/api服务、微服务、命令行应用等等。因此如果你的目标与这些领域重合,那么Go是一个很有竞争力的选择。

  • 请问中小公司中的Go语言技术栈的岗位多吗?

Go是生产力与执行效率两方面都有突出表现的语言。这两方面都能给中小公司省下不少money。一线城市接纳新语言的开发者较多,招聘也不再是问题了。因此我觉得一线城市应该不少,这方面具体数据还得看招聘网站。二三线城市这些年Go也在拓展地盘。在我地处的东北地区,越来越多小公司选用了Go,趋势是好的。

  • 希望老师能说一下Java和Go的区别?

这是很大的话题,也是一个极容易“引战”的话题。

看待这个问题有多种维度,比如从语言语法、生产力、性能、社区活跃度,生态成熟度、发展前景等等。

语言语法见仁见智,java是不折不扣的面向对象编程语言,就像“java编程思想”一书中说:“一切都是对象”。而Go是传统的命令式编程语言,按照Go语言family图谱,它的先祖来自C、Pascal、Newsqueak等。语法简单,但谈不上“领先”,就像很多人说的在最近10年出品的编程语言中,Go的语法显得有些“土气”,我更喜欢称之为朴实无华。很多人就像我,就是喜欢这种朴实。虽然朴实,但Go的表达力并不差哦。

在生产力方面,目前来看Go是要高于java的。

性能方面,同资源消耗下,Go也是要高于java的。另外一点就是即便是新手写Go,性能也不会很差。

社区活跃度方面,两者都是主流语言,java诞生年头多,且是目前企业应用领域的第一语言,其社区自然更好一些。生态成熟度也是如此,现在很难找到一个领域没有java的开源实现。实话说,Go在这方面规模还不及java,但是增长速度要更快。

至于,发展前景,两者都是自己擅长领域的佼佼者,都有不错的前景。Go由于处于成长期,蓝海属性更强一些。

  • Go在机器学习算法包括工程这一块前景如何?

这个要实话实说。在机器学习领域,python是当之无愧的老大。但python也有自己的瓶颈,主要是性能相较于静态语言有数量级差距。各个编程语言也都试图争抢python在机器学习领域的份额,包括julia、c++、rust,Go也不例外。但与在云原生领域的投入相比,Go社区在机器学习算法库方向上的投入还不够,但也有一些成果,比较知名的项目包括GonumGorGoniaonnx-Go等。在帮助构建机器学习/深度学习平台层面上,Go也发挥了很大的作用,比如:kubeflow的部分实现。

机器学习算法上,python已经形成一家独大之势,其他语言,包括Go都会在自己擅长的领域一起助力机器学习的发展了。

  • Go在区块链方向应用广吗?

Go在区块链领域应用应该很多啊,至少区块链刚刚起步时,很多都是用Go开发的。比如联盟链fabric。以太坊已经足够Gopher学习好长时间了。另外像ipfs、filecoin等项目虽然不是典型区块链项目,但很多技术点都很相似,也可以了解一下。

四. Go的历史与哲学

  • 有人吐槽 Go 核心人员不想做的东西,就是 Less is more,自己想做就是各种哲学,这个问题,老师怎么看?

Go语言的简单或者说功能特性少,的确来自于less is more的理念。保持一门小语言,让语言更容易学习与理解。同时每个特性都是经过精心打磨与实现,不能再少了。上周我看了Rob Pike最新一期的talk,他还在说 “Go语言中变量声明的方式有些多了”,这也是我在实际编码过程中的体会。如果重新来过,我想Rob Pike会更彻底的执行less is more,将变量声明方式再减少一种。所以说,特性少不是不想做,而是经过深思熟虑,那个特性的确没必要加入到语言中。

  • Go的异常处理,使用起来简单,但是不方便,请问老师这是在践行Go的简单设计哲学吗?

从Go设计者的初衷来看(https://golang.google.cn/doc/faq#exceptions),Go没有采用像java那样的结构化异常处理的确是出于对“简单”原则的考虑。

在java中错误处理与真正的“异常”是混杂在Try-catch机制中的,并没有明显的界限,无论是错误还是异常,一旦throw,方法的调用者就得负责处理它。

但在Go中,错误处理与真正的异常处理是严格分开的,也就是说不要将panic掺和到错误处理中

错误处理是常态,Go中只有错误是返回给上层的。一旦出现panic,这意味着整个程序处于即将崩溃的状态,返回给上层几乎也是“无济于事”,所以在Go中,一个常见的api设计思路是:不要向外部抛出panic(don’t panic!)。如果api中存在panic的可能性,那么api自己要负责处理panic,并通过error将状态返回给上层。如果api无法处理panic,那程序就很大可能是要崩溃了,这种panic多是因为程序bug导致的。

  • Go的统一代码有利的地方是:保证了开发者的编码风格是一致的,增加了代码的可读性。但这会不会对一些高手来说是一个限制呢?

Go面向工程的设计哲学鼓励复杂软件开发的大协作,Go不鼓励“奇技淫巧”,在Go中做一件事一般只有一种方法。所以我们看到的高手编写的开源项目或是标准库,代码绝大多数都是很容易读懂的。

  • 什么是Go的自举?

和很多主流语言一样,Go语言编译器最初都是由C语言和汇编语言实现的。C语言和汇编实现的Go编译器(记作A)用来编译Go源文件。那么问题来了?是否可以用Go语言自身实现一个Go编译器B,用编译器A来编译Go编译器B工程的源码并链接成最终的Go编译器B呢?这就是Go核心团队在Go 1.5版本时做的事情。

他们将绝大多数原来用C和汇编编写的Go编译器以及运行时实现改为使用Go语言编写,并用Go 1.4.x编译器(C与汇编实现的,相当于A)编译出Go 1.5编译器。这样自Go 1.5版本开始,Go编译器就是用Go语言实现的了,这就是所谓的自举。即用要编译的目标编程语言(Go语言)编写其(Go)编译器。

这之后,Go核心团队基本就告别C代码了,可以专心写Go代码了。这可以让Go核心团队积累更为丰富的Go语言编码经验,也算是一种“吃狗粮”。同时Go语言自身就是站在C语言等的肩膀上,修正了C语言等的缺陷并加入创新机制而形成的,用Go编码效率高,还可避面C语言的很多坑。

在这个基础上,使用Go语言实现编译器和runtime还利于Go编译器以及运行时的优化,Go 1.5及后续版本GC延迟大幅降低以及性能的大幅提升都说明了这一点。这就是自举的重要之处。

五. Go开发环境安装

  • 关于gotip版本

很多初学者不知道gotip版本的存在,gotip指代就是目前Go核心团队正在积极开发的项目最新提交版本,因此gotip时刻在变化。当我们通过go get/Go install(Go 1.17及以后版本)方式安装go-tip版本时,go get其实也是下载Go项目最新源码,然后编译这份源码。如果某个Go核心开发者提交一次代码恰好导致Go tip源码编译不过去,而你下载的恰恰是这个时刻的Go tip源码,那你的Go tip安装自然就会因build失败而失败。这也是我提到的gotip版本不是每次都能安装成功的原因。

  • Go env里面的配置项究竟是存储在哪儿的?网上有说是生成Go 命令(Go语言的的编译工具)时,直接包含在其中了,也有说是在一个和用户相关的配置文件夹里面,还有的说是来自系统环境变量,那这三种来源的优先级是怎么样的?

Go env的确会综合多个数据源。优先级最高的是用户级环境变量。以linux为例,你的用户下的.profile文件中的环境变量优先级最高。然后是系统级环境变量(但我们很少在linux下用系统级环境变量),最后是Go自带的默认值。

六. Go程序构建

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

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

require github.com/user/b v1.0.0

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

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

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

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

  • Go应用项目源码还需要放在gopath的src下么?

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

  • go.mod中的module path必须是github.com/user/repo这样的形式么?

专栏例子中使用github.com/user/repo这个样式作为module path是因为多数实用级module多是要上传到github上的。用这种示例便于后续与真实生产接驳。但对于本地开发使用的简单示例程序而言,module path可以任意选用,比如:

// go.mod
module demo1

Go 1.17

也是ok的。

  • go get或go mod tidy下载的go module缓存在哪里了?

go mod tidy下载的第三方module一般在$GOPATH[0]/pkg/mod下面。这里的GOPATH[0]指的是GOPATH环境变量设置的多个路径中的第一个路径,比如说:如果GOPATH的设置如下:

export GOPATH=path1:path2:path3

那么下载的第三方module就会被缓存在path1/pkg/mod下面。

如果你没有设置GOPATH环境变量也没关系,而且这不是必须的步骤。gopath的默认值为你的home路径下的Go文件夹。这样第三方包就在$HOME/Go文件夹的pkg/mod下面。

Go 1.15版本开始,Go提供了GOMODCACHE环境变量用于自定义module cache的存放位置。如果没有显式设置GOMODCACHE,那么module cache的默认存储路径依然是$GOPATH[0]/pkg/mod。

  • 是否需要深入了解gopath

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

  • 有没有推荐的免费好用的国内module proxy服务?

我个人最常用的是下面这个proxy服务:

export GOPROXY=https://goproxy.cn,direct

其他的几个proxy服务也应该都很好用:

export GOPROXY=https://goproxy.io,direct
export GOPROXY=https://mirrors.aliyun.com/goproxy,direct
export GOPROXY=https://goproxy.baidu.com,direct

以上代理除了通过环境变量配置外,还可以用go env命令写入,以阿里的module proxy为例:

$Go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/
  • 如何拉取私有go module?

这个属于稍高级一些话题,这门课尚不会涉及。之前写过一篇有关拉取私有module的文章可以参考。

  • 什么是可重现构建(Reproducible Build)?

可重现构建,顾名思义,就是针对同一份go module的源码进行构建,不同人,在不同机器(同一架构,比如都是x86-64),相同os上,在不同时间点都能得到相同的二进制文件。

  • go.mod文件中能表述依赖的module信息吗?go.mod文件中的内容一般不都是依赖的第三方包和版本吗?

在go module机制进入Go之前,也就是gopath构建模式时代,我们谈到的所有依赖都是包与包的版本;但go module引入后,所有的版本信息都绑定在module上,所以你在go.mod中看到的require块中的依赖都是module与module的版本,不再是包。

  • Go团队认为“最小版本选择”为Go程序实现持久的和可重现的构建提供了最佳的方案” 这句话能展开讲讲吗?

对开发者而言,更易于理解和预测,就像课程中例子那样,我们根据依赖图可以很容易确定程序构建最终使用的依赖版本。

对Go核心团队来说,更容易实现,据说实现最小选择的代码也就几十行。

更重要的是最小版本选择更容易实现可重现构建。试想一下,如果选择的是最大最新版本,那么针对同一份代码,其依赖包的最新最大版本在不同时刻可能是不同的,那么在不同时刻的构建,产生的最终文件就是不同的。

当然这一切的前提都是基于语义版本规范,对于不符合规范的module,相当于没有遵守契约,这套规则也就失效。这对任何语言来说都是一样的。

  • 在包依赖引用那一节,您说的是A和B依赖C的v1.1.0和v1.3.0版本,这种版本依赖很好理解。但是按照上述的V1和V2不同的原则,如果现在B依赖的不是v1.3.0而是v2.3.0,那我现在项目里引用的C到底是哪个版本?

如果B依赖的是C v2.3.0,那么B导入C的语句就是 import c/v2,而A依赖的是v1.1.0,那么A导入c的语句就是import c,这两个是可以共存的啊。于是你会在go.mod中既看到c v1.1.0,也有c/v2 v2.3.0,它们可以理解为两个不同的module。

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

Go默认是开启CGO_ENABLED的,即CGO_ENABLED=1(可通过go env命令查看)。但编译出来的二进制程序究竟有无动态链接,取决于你的程序使用了什么包。

如果就是一个打印“hello,world”的Go程序,那么你编译出来的将是一个纯静态链接程序,启动时无需动态链接。

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

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

更多关于Go静态编译还是动态编译的内容,可以参考我的Go进阶专栏中的文章《与C互操作不是免费的!一文了解cgo的使用成本》

七. Go语法

  • 什么是Go运行时?

Go 运行时,也称为Go runtime。

它在那里?其本身就是每个Go程序的一部分,它会跟你的源码一起编译并连接到目标程序中。即便你只是写了一个hello world程序,这个程序中也包含了runtime的实现。

它在我的程序中具体负责什么?runtime负责实现Go的垃圾收集、并发、内存堆栈管理以及Go语言的其他关键功能。

它的代码在哪里?它大部分以标准库的形式存放在每个Go发布版的源码中。

  • 包的空导入有什么作用?

像下面代码这样的包导入方式被称为“空导入”:

import _ "foo"

空导入也是导入,意味着我们将依赖foo这个路径下的包。但由于是空导入,我们并没有显式使用这个包中的任何语法元素。那么空导入的意义是什么呢?由于依赖foo包,程序初始化的时候会沿着包的依赖链初始化foo包,我们在08里会讲到包的初始化会按照常量->变量->init函数的次序进行。通常实践中空导入意味着期望依赖包的init函数得到执行,这个init函数中有我们需要的逻辑。

  • 老师,每个文件的包名怎么命名?根据目录来吗?

包名可以任意命名,没有限制,但社区公认的好的包名通常为一个小写单词。一个目录下仅允许存放一个包。通常一个优秀的实践
是包名与目录名相同。但也有很多项目没有遵守这个约定俗成的规则。

  • 专栏中经常提到“字面值”?怎么理解字面值的含义呢?

字面值(literal)就是源码中的一个固定的值,它直接写在源码中,不可变,且不需经过任何计算我们就能从字面上看出其“值”。在编程语言中,通常一个字面值都可以从其字面上大致推断出其类型。另外字面值可以用于初始化变量,也可以作为常量的值。

  • 老师,同一个包内有多个源文件的话,这个包是将所有源文件的常量、变量、init函数汇集到一起,然后按常量->变量->init这样的顺序进行初始化,还是按每个源文件走一遍“常量->变量->init”这样的顺序?

我在macbook pro go1.17版本下的实测情况是Go会先按文件传入的顺序,分别初始化每个文件常量与变量,然后再分别调用各个文件中的init函数。比如说:如果一个包pkg1有两个文件file1.go和file2.go,那么实测的初始化顺序就是:“file1中的常量 -> file1中的变量 -> file2中常量 -> file2中变量 -> file1中init函数 -> file2中init函数”。

八. Go程序设计

  • 专栏中提到的“一动一静共同构成了Go 应用程序的骨架”中的一动一静指的是什么?该如何理解

关于“一动一静”,“动”主要指程序的并发设计层面,如何设计去管理和控制Goroutine。当程序运行起来后,真正“动”的是一个一个Goroutine。而“静”,则是Go源码中的实体以及它们之间的耦合关系。

九. Go标准库

  • printf 能格式化字符串,换行就要手动添加 “\n”,println 又不能格式化字符串。我想知道为什么要这样的设计?在看我来这就是特别反人类的设定,Rust 的 println!(“{}”, a); 才是符合直觉的。

这个问题我是这么看的,printf是go提供的标准格式化io的函数,它能实现你所期望的所有功能。与c语言的printf是对等的。但println这个函数你可以看成是一种“语法糖”,它本身就是一个特例,你可以用go doc看看println的manual,println原语义就是使用一种默认的格式输出数据到stdout上。你认同这种默认格式,你就使用println,简化你的代码。否则,你就用printf就好了

十. Go工具链与工程实践

  • 谈谈支持Go的VS Code的Copilot插件

copilot插件我还没体验过,如果真的如你所言那么强大,那也是Go语言和Go开发者的一大幸事

十一. 其他

暂无。