🌝

2022 年怎么开始学 Go

Posted at — Apr 13, 2022
#Go

写给可能因国内教程走弯路的 Go 初学者

纠结了很久是否要水这篇文章,因为实在是太没技术含量了,但最近实在是看不下去了,身边的同学越来越多开始学 Go 但又被国内的教程带偏,加上想到之前的文章不也没技术含量么,所以还是决定动笔。 从我开始接触 Go 的 2019 年到现在,Go 共发生了 3 次重大更新(包括上个月发布的 1.18 版本中正式加入的 Go 泛型)。这是像 Go, Rust 这种比较年轻的语言的特点,特别是 Rust 这种由社区驱动的语言更是更新频繁得离谱。

所以唯一靠谱的只有官方文档。相比 Rust,由于由 Google 一个团队维护,加上对简洁高效的追求,Go 并不太迎合社区需求,没有拼命增加新特性,从今年才发布泛型就可看出 Go 的相对保守,我记得从我开始接触社区就有大量提议泛型的声音。

其实 Go 的定位就是高性能网络编程,而这块往往让使用传统编程语言的程序员头疼,也是 Go 团队发明 Go 的初衷。在高效和简洁间寻找平衡,答案就是 Go。

但不要误会,这篇水文题目叫怎么「开始」学,而不是怎么学,我也不会介绍 Go 的历史。意在讲下目前 2022 年 Go 的正确打开方式,包括正确安装环境、工具链使用、代码组织、命名规范、包管理、常用生产发布方式、文档阅读和目前的生态状况。本文也会跟随 Go 的更新进行内容的修改。

第一原则

任何领域都有所谓的 first principal,而编程界的第一原则就是「先看官方文档」没有什么比官方文档更准确。要是你嫌官方文档都是英文看不懂,那你可能不适合研究这行。目前 Go 的国内技术沉淀还不像 Java 那样,充分得可以自给,能给初学者足够多的正确信息,毕竟 Java 主宰市场那么多年。想要学会一门还在发展中的新语言,你不得不直接阅读英文文档和开源项目源码,后者是因为 Go 的模式和传统设计模式不太一样,更何况像模式这种东西也只能靠大量写项目、看大量项目源码才能掌握和正确使用,不是在课上靠老师念念 PPT,学生上讲台耍耍嘴皮子就能学会的 /doge。

另外,技术类的英文(任何语言)都写的很直接,不拐弯抹角,读不懂往往是因为里面的技术名词不熟悉,还有就是计算机的基础知识不扎实,很多文档里一句话带过的内容可能需要深刻的计算机知识才能理解。所以我一直说基础很重要,基础教学很重要,但实际上国内高校的操作。。。不说也罢!

不多说,进入正题。

动手前

你得知道 Go 之前的官网是 golang.google.com,所以严格来讲,Go 是谷歌的编程语言而不是程序员的,我们能用完全是因为谷歌的用爱发电,这么说可能有点过,因为站在理想主义角度,当一个项目变成开源项目后,他本身的所属性质可能就会改变,但不管怎么说,当你在使用(白嫖)一个东西的时候不要忘记它来自哪里。

现在 Go 官网是 go.dev,这似乎是现在的趋势,像 svelte.dev, micro.dev 等比较新的项目都是用 .dev 顶域,语义更符合「开发」另外该域名在 Firefox, Chrome, Edge 上会被强制重定向为安全协议1,所以若使用该域名,你的服务必须要支持 https 连接和有效的证书。

目前 Go 的官网局部相比之前更加规范和友好,这里主要讲 4 点。首先是 文档 部分,Go 的大部分说明包括各平台上的安装和各个特性的入门教程都在这里面,其中 Effective Go,是官方给的深入入门教程,包括编程规范和最佳实践,以及对 Go 的一些特性或理念做的详细的讲解和举例。可以把它当作 Go 版本的 Effective Java

其次是 Tour of Go,这是官方给的针对基础概念、语法和 Go 并发模式的在线学习环境,适合对这些 Go 特性进行初步了解。然后是官方博客 The Go Blog,里面是官方收录的一些 Go 相关文章,包括新特性的讨论、官方通知等等。

最后是 官方包管理在线文档,注意是包管理「文档」而不是像 Python, NodeJS, Rust 那样的中心化包管理平台。Go 采用的是分布式包管理,代码由作者或组织自己存储管理,一般是在 GitHub 上(后面会讲到 Go 的包编写和发布方法)你在代码中每个全局变量、类型定义、函数或方法上写的注释,会被自动转换为文档显示在该在线文档中。所以,当你需要知道哪个模块如何使用或想研究它的原理时,先看该文档。另外如果你想发布自己写的模块,请确保完整地写好了注释。

关于注释

有人说好的代码是不需要注释的,完全是放屁。好的代码更需要注释,因为就算你的代码写的再清晰,在复杂业务面前,其他人不可能仅通过一段段语义上代码看出整个系统的样子,所以注释不仅应该写「是什么」更多地,要写「为什么」。比如一个函数的注释不光是这个函数里每行代码在干什么,还应该为什么要这么干,写注释的人应该站在全局视角来写。另外 Go 的注释风格是第三人称描述,比如你有一个函数叫 Retrieve,那它的注释应该是「Retrieve does something」这种格式。

动手时

在正式写代码前,需要正确的安装环境,这部分也是 Go 较之前有很大变化的地方,也是国内很多教程将初学者带入歧途的原因。

正确安装

官方下载地址 里根据你的操作系统和机器架构选择一个下载,如果因为网络原因访问不了,可以尝试之前的 国内的镜像地址。Windows 上就是点击下载的 msi 安装程序,一直默认下一步就好,Go 会被默认放到 C:\Program Files\Go 路径下,我的建议是不要更改这个默认路径,不要给自己增加麻烦,因为你其实不会点开这个目录,Go 的所有东西包括编译器源码、标准库、单元测试等等都在该目录里。除此之外,该安装器会为你添加两条 PATH 环境变量:C:\Program Files\Go\bin%USERPROFILE%\go\bin,前者用来让操作系统找到 gogofmt 命令,后者用来为找到通过 go getgo install 安装的第三方 Go 编写的软件,所以你自己编写的 Go 程序编译后的可执行文件也建议放到 %USERPROFILE%\go\bin 中。其中 %USERPROFILE%\go 也就是你用户目录下的 go 目录为 GOPATH 的默认路径。

Linux 和 macOS 上也很简单,将解压出的东西放到 /usr/local/go 目录里并写好环境变量即可,具体见 官方文档(因为这种步骤写起来真的很搞笑)

当然可以直接使用 macOS 或 Linux 各个发行版的软件管理工具安装,如对于 Arch Linux

1
pacman -S go

另外使用 macOS 的同学也可以下载官网给的 pkg 格式的安装包来安装,安装过程就像在 Windows 上那样。

GOPATH vs Go Module

在 Go 1.13 版本之前,默认使用 GOPATH 来进行包管理。在该模式下,你的所有 Go 代码都要放到 $GOPATH/src 目录下,并且该目录下的目录结构必须和远程地址一致。比如你的项目的远程地址为 github.com/mivinci/web,且通过 go get 获取了第三方模块 github.com/grpc/grpc-go 那么你的目录结构会长这样:

$GOPATH
├── bin
└── src
    └── github.com
        ├── grpc
        │   └── grpc-go
        └── mivinci
            └── web

这样当你在项目里使用 import "github.com/grpc/grpc-go" 时,Go 就会从 GOPATH 开始往下查找。我个人挺喜欢这种包管理方式的,简单直接,但弱点显而易见,就是无法区分包的版本。若你的本地几个项目分别依赖了同一个包的不同版本,那么这种包管理方式是不可行的。针对此问题,官方给出了 vendor 方案,即若项目根目录有 vendor 目录,那 Go 就会去该 vendor 目录里按照 import 的路径寻找。就跟 NodeJS 的 node_modules 一样,这里对该方法不做描述,也不建议使用。

从 Go 1.13 开始,正式推出 Go Module 模式,在该模式下,你的 Go 项目源码可以放在任何一个地方,不在需要放到 $GOPATH/src 中,你下的包也被 Go 统一维护在目录 $GOPATH/pkg 中,你可以看到该位置中每个包都有独立的存放不同版本的目录。

在项目根目录运行 go mod init <项目名> 即可将该项目置为 Module 模式,如

1
go mod init github.com/mivinci/web

这样 Go 就会自动为你在项目根目录生成一个 go.mod 文件,里面记录了项目名、使用的 Go 版本和第三方依赖及其版本信息。当在 Module 模式中使用 go get 获取第三方包时,Go 会自动为你将该包的信息写进 go.mod 文件中。这里我推荐无论是不是写给别人调用的包,项目名都填远程仓库的路径。

你还需要知道,Go 模块的最小单位是「目录」也就是说,一个目录下的所有文件在一个命名空间里,通俗的讲就是一个目录里的所有文件里的函数、变量、类型不能重名。而不是像 Python, JavaScript 那样以单个文件作为最小单位。所以若你的项目中有多个 main 函数,那需要将它们放到不同的目录中。

Go 的这种设计我认为是正确的,因为它不仅方便的开发者还减小了使用者的负担,当开发者想要更改某个 Go 文件名时,其他调用了该文件的地方不需要跟着改,虽然现在的 IDE 已经能自动为你修改受改名影响的地方,但这样对 commit 的简洁性是灾难。对于包的使用者,他们也无须知道哪个函数、类型等在那个文件,只需要 import 到文件所在的目录就行,剩下的交给编辑器或 IDE 来提示。要是你没懂这段话也没关系,写多了就能体会到了。

另外,若将 Go 的 GO111MODULE 环境变量设置为 auto,则除了在 GOPATH 目录下 go getgo install 按照 GOPATH 上述模式进行外,其他目录都自动按照 Go Module 模式进行。

go env -w GO111MODULE="auto"

Go Workspace

Go Workspace 是上个月 Go 1.18 推出的新特性,是对 Go Module 的补充,为了方便进行多 Module 同时开发和构建。我项目里基本都用了多包同时开发构建这种模式,并且这些包之间存在调用链,但尝试了 workspace 之后感觉有点失望,我以为不用再在 go.mod 里写 replace 之类的,结果还是要写,所以先不讲这个特性,且对于初学者还暂时用不到这些。

编辑器 vs IDE

世界上只有两种程序员,一种是使用 Vim 写代码的程序员,另一种是其他程序员。不过我并不是要说都应该使用 Vim 写代码,也不是要推荐使用集成开发环境(IDE)。事实上,我反对在初学阶段使用 IDE 这种点运行按钮运行代码的模式。

就像最开始学习 C 一样,一来就用 IDE 的话,到最后你什么都学不到,甚至大多数人连 IDE 是啥都不知道,把编译器和 IDE 混为一谈,高校和教材编写者应该为此反思。C 语言只应该在类 Unix 操作系统下学习,无论是入门还是深入研究,都应该使用编辑器 + 编译器(如 gcc)这种手动编译学习的模式,因为这样你才能亲身接触到编程语言背后的原理和编译器的历史。你可以通过编译器生成的汇编指令,真正理解到指针是如何工作,参数、局部变量和返回值在函数调用栈里是怎么被维护,不同机器架构下的指令集又有什么不同,系统调用等这些计算机最基础的东西。这些是今后在这个领域能轻松学习、快速理解新事物和创新的必要条件。

对于 Go 的入门学习,同样推荐这种模式,不要一来就使用 Goland 这种高度集成的 IDE,你可以使用一些你喜欢的轻量的编辑器,如 Sublime Text, Atom,但考虑到对代码提示和语法检测插件的支持,我只推荐使用 VSCode。其实严格来说,VSCode 也不算太轻量,但比 Jetbrain 系列好太多,也不会因为看到满篇波浪线心烦。另外,VSCode 的 debug 功能我甚至觉得比 JB 系列的更好用,速度更快。

虽然我是「All in VSCode」哲学的践行者,但我想大家意识到,VSCode,也就是 Microsoft Visual Studio Code,并不是一个开源软件,开源的是一个叫做 Code OSS 的东西,即 VSCode 的内核,你可以把这两者看成是 Linux 和 Ubuntu 的关系,也就是说你如果自己用官方给的 VSCode 代码仓库的源码来构建,并不能得到一个和你用的 VSCode 一摸一样的 VSCode,少了一些跟微软相关的模块,比如遥测模块,你应该明白这意味着什么。另外,还有一个叫 VSCodium 的编辑器,是基于 Code OSS 构建,由社区维护的 VSCode。如果你在乎数据保密性,那么请使用 Code OSS 或 VSCodium 这种真正的开源软件。

就我个人写代码来说,但我不在乎,所以我还是用的大家熟知的这个 VSCode,这三者大致功能没什么区别,界面也长得一模一样。

好的,接着上节内容,安装好 Go 后,请务必设置国内 Go 代理地址,不然安装不了第三方 Go 包,因为默认的 Go 代理地址 proxy.golang.org 在墙外。让当你也可以直接把 Go 代理关掉,但为了更好的编程体验,请务必设置国内代理。运行命令:

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

其中,goproxy.cn七牛云 维护的 Go 模块镜像地址,当然你也可以换成其他地址,就目前来说,这个是最好用的。

然后在 VSCode 扩展市场搜索「Go」下载 Go 的插件,这一个就够了!这个插件等会儿帮你通过 go install 安装一些 Go 代码检查工具,然后在你写代码时通过这些工具为你提供友好的懒人服务(我只能说,比 JB 公司的好用)

现在我们新建一个目录,就叫 dev 吧,作为我们第一个 Go 项目的根目录,随便放哪儿都行,不用管 GOPATH,因为我们打算使用(建议以后都使用)Go Module 模式进行开发。然后在该目录下运行:

1
go mod init github.com/mivinci/dev

当然你得把 mivinci 换成你自己的 GitHub 账户名。就像上节所说,你也可以不向我这样填写远程仓库的地址,光填个 dev 就行。但我的建议是养成习惯,况且这样在 import 时也比较美观,因为只有在 import 标准库时才只有个名称,其他的都应该为远程仓库地址。你问我啥是标准库?那你别干这行了 /doge

用 VSCode 将该目录打开,然后在根目录新建一个 main.go 文件,此时 VSCode 应该会提醒你是否要安装一些必要的 package,点击 Install All 即可,然后等待它显示 You are ready to Go :) 就好了,一语双关。这些工具都是用 Go 实现的,非常好用,如果去他们仓库研究下源码,应该能学到很多编译原理的东西。另外正如上节所说,这些通过 go install 安装的软件被自动放在了 $GOPATH/bin 中,不用管它们。

好了,此时你的目录结构应该长这样:

dev
├── go.mod
└── main.go

最后你就可以愉快地开始学习了。

不知道你有没有晕,还是总结一下步骤:

  1. 从官网下载安装 Go
  2. 设置国内 Go 代理地址
  3. 从官网下载安装 VSCode
  4. 安装 VSCode 的 Go 插件
  5. 新建一个目录作为第一个项目,同时也为了触发 Go 插件安装一些工具链
  6. 运行 go mod init <项目名> 将该目录置为 Go Module 模式
  7. 新建 main.go 文件
  8. 等待 VSCode Go 插件安装好对应工具链
  9. You’re ready to Go :)

国内有效资料

若想更深入的了解 Go 的原理,可以看看这本《Go 语言高级编程》这是 在线阅读地址,特别是第二、三章对 CGo 和 Go 汇编的解释。若你实在看不下去官方文档,可以参考 Go 语言中文网 这个由国内社区维护的网站给的 在线书单。但是我还是建议多研究下官方给的东西。

这里推荐三篇并发相关的经典文章,学到 Go 并发模式的时候最好看看:

关于网络编程

Go 主要为网络、IO 编程服务,但其实这两者是一回事,都是 IO,因为一切皆文件 233。

和大多数入门教程不同,我不建议先学 Go Web,而应该按照《Go 语言高级编程》里的顺序先了解 RPC,即微服务的核心:远程调用。原因很简单,因为 Go 的网络库 net 很强大,配合 Goroutine 能很快写出质量还可以的服务程序,所以如果先接触那些 Go Web 的东西的话,可能对网络编程的理解比较肤浅,所以我建议先通过 RPC 来熟悉下所谓的 Socket 编程,写点 demo 程序,如多人聊天室、各种代理什么的。你会发现网络编程这块 Go 和 C 如出一辙。只不过 Go 为你封装好了事件循环、IO 复用什么的,如果你写过 C 的话应该能理解。

然后对于 Web 编程,也就是我们熟知的传统后端,我仍建议先熟悉 Go 的 HTTP 标准库 net/http,大多数 Go Web 框架,其实都不算是框架,只是个 router 路由器,有时也叫 multiplexer 多路复用器,因为 Go 已经提供了比较完整易用的封装(除了动态路由),能满足你大部分需求。等熟悉了再去玩各种第三方库,你会发现他们都大同小异。

同理,对关系型数据库,Go 也提供了十分完整的封装 database/sql,所以不建议一来就使用 ORM 这种东西,如 gorm。不过这块 Go 只提供了接口,并没有具体实现,写代码的时候你需要像 go-sql-driver/mysql 这种第三方库。从这点就可以看出 Go interface 的简洁强大,这才是真正的多态,多态的本质就是同类事物的归一化。

IO,即输入输出,是网络编程的核心。Go 标准库大量依赖了其 io.Reader, io.Writer 接口,包括文件操作,数据流读写。很多编程实现都能归一化为这两个接口,所以我建议在实践过程中多留意一下当前模块本质上是否为对某个对象的读写,可能会对简化逻辑有帮助,也更符合计算机的工作过程。

后面可能会写写微服务的东西,特别是当前 service mesh 服务网格的趋势。To be continued…


  1. https://ma.ttias.be/chrome-force-dev-domains-https-via-preloaded-hsts/ ↩︎

评论插件加载中 OvO