Skip to content

Latest commit

 

History

History

golang

Go 语言

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 环境变量

可通过 go env 查看所有 Go 环境变量。

  1. $GOPATH: Go 的工作目录,和 Python 的包导入路径 $PYTHONPATH 类似但含义有区别。
    • 它的默认值为 $HOME/go
    • $GOPATH 下通常有三个约定好的子目录:src pkg bin
    • 目前依赖管理已经改用 go modules 实现,依赖缓存都存放在 $GOPATH/pkg
    • 和系统的 $PATH 变量一样,$GOPATH 也支持使用同样的语法设置多个目录,靠前的目录将被优先检索。
  2. $GOROOT: Go SDK 的安装位置,通常安装在 /usr/local 里面
    1. 包含了标准库、编译器、链接器、pprof 性能测试工具等等

Go 语言代理

设置国内代理,否则无法拉取依赖:

# 设置使用七牛云代理地址
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GOPRIVATE=*.svc.local

依赖管理

Go 语言的依赖管理分成两种情况

1. 全局安装(安装到 GOPATH 下)

这个模式,通常用于安装系统全局的 tools,比如 golang/tools 中的官 方工具,或者别的二进制程序。

两个命令介绍如下:

  1. go install: 下载依赖源码到 $GOPATH/src 目录下。然后编译源码
    1. 编译出的静态链接库(xxx.a)将被放到 $GOPATH/pkg 目录下
    2. 编译得到的可执行文件将会被放到 $GOROOT/bin 目录下。
  2. go get -u: 首先从网络上下载源码,然后 install 它。
    1. -u 表示更新到最新版本

2. Module-aware 模式

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: 添加缺失的依赖项,删除未使用的依赖项

Go 基本概念

  • 每个 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 定义为值类型
    • 不可在指针类型上定义方法,也就是说上面的 P 不能是指针类型,因为不能对指针类型再次取指针。
    • 在调用类型上的方法时,不需要手动取指针,编译器会自动帮我们处理。
    • 不能在常量类型上调用 receiver 为指针类型的方法,因为常量类型不在堆栈上,无法取指针。
  • Go 的 OOP - 接口
    • 接口是一种特殊类型,用于解耦具体类型与代码逻辑。OOP 的一个口号就是「面向接口编程」
    • 在 Go 中,空接口 interface{} 一定程度上被用于实现类似泛型的功能。
    • 接口类型会保存两部分信息:
      • 动态类型:即值的真实类型
      • 动态值:具体的值
    • 在将一个值赋值给一个接口变量时,实际上会执行隐式类型转换,将值转换为接口类型的上述两部分信息
    • 接口的零值有两种情况:
      • 动态类型与动态值都为 nil, 比如 var w io.Writer
      • 动态类型不为 nil,但是动态值为 nil,比如 var w io.Writer = (*bytes.Buffer)(nil)
        • 这种情况下 w != 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」这样的能力,自定义函数无法做到同样的事 情,是妥妥的黑魔法。
  • Goroutines 与 Channel: 参见此文件夹的 并发编程.md
  • break with label
    • go 的 break label 表示的是,打破 label 标记的这个语句块,直接从该语句块的下一条命令开始执 行。
      • 这跟 c 语言或汇编的 goto label 是不一样的,它是让程序跳转到 label 处开始执行
  • go 工具链
    • 包管理
      • 空白导入语句 import _ "image/png": 仅执行包的 init 函数,将包的 Decoder 注册到另一个包中,常 用在编解码、数据库等场景中
    • go build 如果提供目标地址,必须以 . 或者 .. 开头!这是 go 的特殊规则
    • 包文档,go doc 可以从函数注释、方法注释、包注释等地方提取生成出对应的文档
    • 内部包:path 中带有 internal 段的包都是内部包,只可被 internal 父路径下的其他包导入
    • go list 可列出或查询所有可用的包

参考