Go 是 Google 开发的一门编程语言,其主要特点如下:
- 类似 C 的语法,但是有 GC,不需要自己管理内存
- GC 性能不如 JVM,这是 Go 目前较大的缺陷之一
- 类似 C 语言的错误处理机制:基于函数的多返回值实现
- Go 设计者认为 try-catch 这种处理逻辑,会使代码逻辑混乱
- Go 的错误处理哲学是:永远先处理出错的情况,再编写无错误的逻辑。
- 语言层面支持 goroutines 协程,从而提供了强大的并发编程能力
- 仅支持静态链接,生成单个二进制文件,外部只依赖 glibc/musl.
- 好处是部署、更新、维护方便(对运维很友好),但是二进制文件的体积会偏大
- 标准、统一、不易出错的规范
- Go 的设计哲学是:一件事情应该只存在一种最好的解决方法
- 这避免了规范层面一些无意义的争论,同时语言的静态分析、代码转换等工具实现起来也更简单
- 语言层面的规范举例如下
- 不需要使用分号结尾、左花括号
{
不允许换行 - 结构体的字面量,最后一行必须以逗号结尾,不允许省略逗号
- 不允许定义未使用的变量与 import 语句
- 通过变量首字母的大小写来区分其外部可见性:小写开头的是 package 的私有变量,大写开头的才外部可 见
- 不需要使用分号结尾、左花括号
- 完善的工具链:
- 官方的 tools 工具包:godocs 文档生成、gofmt 格式化、goimports 导入管理、pprof 定位性能问题
- 完善的依赖管理 go modules:完全基于 git 实现的依赖管理,一行命令安装所有依赖,一行命令完成编译
- 不过 go modules 也被很多人吐槽...曾经这一块也是比较混乱的,这里略过不提
- 丰富的官方标准库以及拓展库
golang.org/x/...
- 很多工作如网络编程、密码学编程、代码格式化与导入管理、文档生成、性能测试等,基本只用标准库或者其 他官方库就可以解决问题,相比之下其他语言对社区库的依赖要重得多
- 交叉编译:即跨平台编译,官方也提供了比较好的支持
- 其他特性:
- 不限制栈空间大小,支持无限递归调用。
- 对闭包的良好支持
- 所有变量默认都会被初始化为其 0 值,避免使用了错误的未定义值导致异常。基本所有现代化语言都是这么 干的,包括 rust
- 支持类型推导,很多场景下不需要手动指定变量类型。这也是现代化语言的一个通用特征,rust 也是这么干 的
- Go 1.18 支持了泛型,但是跟其他语言的泛型区别很大,有些情况有性能损失,官方建议谨慎使用。
- Go 通过空接口
interface{}
实现了与动态语言有些类似的特性,它也可以说是 Go 中泛型的替代品。 - 不支持宏
- 当然 Go 也存在许多收人诟病的缺陷,但是相比 C 问题应该是少了非常多。
- 你想想,C 甚至需要《C 与指针》、《C 缺陷》这样的书来专门分析语言的各种犄角旮旯...
Go 语言核心团队都是牛逼哄哄的大佬,不少人都曾经留名青史。Robe Pike 是 UTF-8 编码的主要设计者,Ken Thompson 是 Unix 的创造者,C 语言前身 B 语言的设计者,1983 年图灵奖得主,著名的《C 程序设计语言》一 书的作者之一,同时他也与人合著了《Go 程序设计语言》一书,广受好评。Robert Griesemer 是 JVM HotSpot 编译器及 JavaScript V8 引擎的开发者之一。
Go 被认为是现代化的 C 语言,但是它的应用场景跟 C 只有部分交叉。因为在底层引入了 GC 以及 goroutines, 它并不适合系统层面的编程。目前广泛认为系统编程领域真正的继任者应该是 Rust.
Go 语言目前被大量应用在云原生领域,比如容器化工具 Docker、容器集群 Kubernetes、监控系统 Prometheus、 分布式对象存储 MinIO 等等... 其次 Go 也被大量应用在现代微服务开发、命令行工具、网络代理工具等领域。
可通过 go env
查看所有 Go 环境变量。
$GOPATH
: Go 的工作目录,和 Python 的包导入路径$PYTHONPATH
类似但含义有区别。- 它的默认值为
$HOME/go
- $GOPATH 下通常有三个约定好的子目录:src pkg bin
- 目前依赖管理已经改用 go modules 实现,依赖缓存都存放在
$GOPATH/pkg
中 - 和系统的
$PATH
变量一样,$GOPATH
也支持使用同样的语法设置多个目录,靠前的目录将被优先检索。
- 它的默认值为
$GOROOT
: Go SDK 的安装位置,通常安装在/usr/local
里面- 包含了标准库、编译器、链接器、pprof 性能测试工具等等
设置国内代理,否则无法拉取依赖:
# 设置使用七牛云代理地址
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOPRIVATE=*.svc.local
Go 语言的依赖管理分成两种情况
这个模式,通常用于安装系统全局的 tools,比如 golang/tools 中的官 方工具,或者别的二进制程序。
两个命令介绍如下:
go install
: 下载依赖源码到$GOPATH/src
目录下。然后编译源码- 编译出的静态链接库(
xxx.a
)将被放到$GOPATH/pkg
目录下 - 编译得到的可执行文件将会被放到
$GOROOT/bin
目录下。
- 编译出的静态链接库(
go get -u
: 首先从网络上下载源码,然后install
它。-u
表示更新到最新版本
在 GOMODULE111=on
时,如果当前工作目录或者任意父文件夹中存在 go.mod
, Module-aware 模式就会自动
激活。
这个模式下,go get
有不同的功用,它首先解析并添加依赖到 go.mod
,然后再 install
这些依赖。
go get
支持版本号或者 commit,几种示例用法如下:
go get github.com/gorilla/mux@latest # same (@latest is default for 'go get')
go get github.com/gorilla/[email protected] # records v1.6.2
go get github.com/gorilla/mux@e3702bed2 # records v1.6.2
go get github.com/gorilla/mux@c856192 # records v0.0.0-20180517173623-c85619274f5d
go get github.com/gorilla/mux@master # records current meaning of master
此外还有一系列 go mod 相关命令:
go mod download
: 下载项目的所有依赖go mod tidy
: 添加缺失的依赖项,删除未使用的依赖项
- 每个 package 可以由多个文件组成,可以有
init()
与main()
两个特殊函数init()
会在包被初始化时调用main()
是程序的入口
- 数组与 slice 切片
- 常量
- 枚举类型是基于常量 + 自增器
iota
实现的 - 无类型常量(untyped constants)是一种特殊的常量,在使用时才会被确定类型,可以提供最高的精度
- 枚举类型是基于常量 + 自增器
new
为某个类型分配空间,并返回其指针- 用得很少,因为定义一个类型通常就自动分配空间了,而引用类型通常使用
make
- 用得很少,因为定义一个类型通常就自动分配空间了,而引用类型通常使用
make
为引用类型分配空间,并返回该类型的值。- go 中大量使用了
nil
以及与nil
相等的其他类型的空值。- 比如,就像茴字有四中写法,空切片也有四种定义方式:
- ``var s []int`
var s []int = nil
s := []int{nil}
s := []int{}
: 只有这个!= nil
成立,前面三个跟 nil 完全相等
- 比如,就像茴字有四中写法,空切片也有四种定义方式:
- map 与 set
set
通过map[string]bool
实现- 也有人用
map[string]interface{}
实现set
,能省一点存bool
的空间,但是官方觉得这点空间微 不足道,没必要这么干。
- 也有人用
struct
结构体- 匿名结构体,其实只一个语法糖
- 匿名结构体实际上会有一个与其类型同名的名称
struct
的每个字段都可以定义一个metadata tag
,通常用于json
的序列化与反序列化
- Go 官方提供了一个模板语言包
text/template
,它被大量应用在helm
等配置文件渲染、网页渲染、代码 生成等场景中。 - Go 的 OOP - 方法定义
- Go 可以在命名类型上定义方法,方法有一个
receiver
- 如果类型比较大,或者需要修改类型的值,就需要将 receiver 定义为指针类型,如
func (p *P) xxx () {}
- 对于较小的类型,而且不需要修改类型的值,可以直接将 receiver 定义为值类型
- 如果类型比较大,或者需要修改类型的值,就需要将 receiver 定义为指针类型,如
- 不可在指针类型上定义方法,也就是说上面的
P
不能是指针类型,因为不能对指针类型再次取指针。 - 在调用类型上的方法时,不需要手动取指针,编译器会自动帮我们处理。
- 不能在常量类型上调用 receiver 为指针类型的方法,因为常量类型不在堆栈上,无法取指针。
- Go 可以在命名类型上定义方法,方法有一个
- Go 的 OOP - 接口
- 接口是一种特殊类型,用于解耦具体类型与代码逻辑。OOP 的一个口号就是「面向接口编程」
- 在 Go 中,空接口
interface{}
一定程度上被用于实现类似泛型的功能。 - 接口类型会保存两部分信息:
- 动态类型:即值的真实类型
- 动态值:具体的值
- 在将一个值赋值给一个接口变量时,实际上会执行隐式类型转换,将值转换为接口类型的上述两部分信息
- 接口的零值有两种情况:
- 动态类型与动态值都为 nil, 比如
var w io.Writer
- 动态类型不为 nil,但是动态值为 nil,比如
var w io.Writer = (*bytes.Buffer)(nil)
- 这种情况下
w != nil
- 因此要尤其注意这种情况,绝对不要把一个空指针赋值给一个接口变量!!!
- 这种情况下
- 动态类型与动态值都为 nil, 比如
- 由于接口变量底层的实际类型是多变的,因此编译器无法在编译器检测它的实际类型,这导致运行时如果发现
类型错误,就会触发 panic!
- 比如,将一个无法进行比较的类型赋值给某个接口类型,然后作为 map 的 key 使用,就会触发 panic
- 或者直接比较两个包含接口变量的值,也可能会因为接口底层的值无法比较,触发 panic
- 总之如果你要比较某些元素,一定要确保其中不包含接口变量,或者确保其中的值都是可比较的。
- 接口存在运行时开销,因此不应该滥用它。
- 只有在需要对多个实际类型进行统一的处理时(比如统一的 IO 接口、sort 接口),才应该使用接口。
- 唯一的例外:接口可用于解耦两个 packages.
- 接口应该尽可能设计得小一些 - 只包含你需要用到的东西
- 类型断言
xxx.(T)
:- 与类型转换的区别在于,它只用于将一个接口类型转换成底层真正的类型,或者另一个接口类型,不涉及 copy 操作
- 而类型转换如
[]byte("xxx")
,会将一个字符串转换成一个 bytes slice,会 copy 底层的所有数据。
- 类型切换
switch x.(T) {}
,通过 switch-case 来针对 x 的类型做各种操作,是简化的 if-else 语法。 - 可选的第二个返回值
ok
- Go 的内置类型与类型断言,有提供一些黑魔法——可选的返回值
ok
- 如果调用方没接收第二个返回值
ok
,结果依具体操作的不同而不同- 如果是一个类型断言
val := xxx.(T)
,失败将会导致 panic - 如果是从 map 中取值,比如
val := map[key]
,调用将直接返回对应类型的零值
- 如果是一个类型断言
- 如果调用方接收了第二个返回值
ok
,这个值将用于表示调用是否成功。- 这种情况下,即使是类型断言
val, ok := xxx.(T)
,也不会触发 panic
- 这种情况下,即使是类型断言
- 只有部分内置类型或表达式才提供「可选的第二个返回值
ok
」这样的能力,自定义函数无法做到同样的事 情,是妥妥的黑魔法。
- Go 的内置类型与类型断言,有提供一些黑魔法——可选的返回值
- Goroutines 与 Channel: 参见此文件夹的
并发编程.md
- break with label
- go 的
break label
表示的是,打破label
标记的这个语句块,直接从该语句块的下一条命令开始执 行。- 这跟 c 语言或汇编的
goto label
是不一样的,它是让程序跳转到label
处开始执行
- 这跟 c 语言或汇编的
- go 的
- go 工具链
- 包管理
- 空白导入语句
import _ "image/png"
: 仅执行包的 init 函数,将包的 Decoder 注册到另一个包中,常 用在编解码、数据库等场景中
- 空白导入语句
- go build 如果提供目标地址,必须以
.
或者..
开头!这是 go 的特殊规则 - 包文档,
go doc
可以从函数注释、方法注释、包注释等地方提取生成出对应的文档 - 内部包:path 中带有
internal
段的包都是内部包,只可被internal
父路径下的其他包导入 go list
可列出或查询所有可用的包
- 包管理