目录 开篇词 | 跟着学,你也能成为Go语言高手 导读 | 写给0基础入门的Go语言学习者 导读 | 学习专栏的正确姿势 开篇词 | 跟着学,你也能成为Go语言高手 Go 语言是由 Google 出品的一门通用型计算机编程语言 Go 语言拾遗:这部分将会讲述一些我们使用 Go 语言做软件项目的过程中很可能会遇到的问题。 导读 | 写给0基础入门的Go语言学习者 1. 你需要遵循怎样的学习路径来学习 Go 语言? 从 2018 年开始,随着 Google 逐渐重回中国,Go 语言的官方网站在 Google 中国的域名下也有了镜像,毕竟中国是 Go 语言爱好者最多的国家,同时也是 Go 语言使用最广泛的一片土地。 如果你在国内,可以敲入这个网址 https://golang.google.cn/ 来访问 Go 语言的官网。 这个专栏专注于 Go 语言的核心知识,因此我并不会深入说明所有关于语法和命令的细枝末节。 如果你想去全面了解 Go 语言的所有语法,那么可以去 Go 语言官网的语言规范页面 https://golang.google.cn/ref/spec 仔细查阅。
Go 语言摄取了面向对象编程中的很多优秀特性,同时也推荐这种封装的做法。从这方面看,Go 语言其实是支持面向对象编程的,但它选择摒弃了一些在实际运用过程中容易引起程序开发者困惑的特性和规则。 这里强调一下,Go 语言中根本没有继承的概念,它所做的是通过嵌入字段的方式实现了类型之间的组合。 在我面试过的众多 Go 工程师中,有很多人都在说“Go 语言用嵌入字段实现了继承”,而且深信不疑。 要么是他们还在用其他编程语言的视角和理念来看待 Go 语言,要么就是受到了某些所谓的“Go 语言教程”的误导。每当这时,我都忍不住当场纠正他们,并建议他们去看看官网上的解答。 但是,Go 语言会适时地为我们进行自动地转译,使得我们在这样的值上也能调用到它的指针方法。
我今天要讲的if语句、for语句和switch语句都属于 Go 语言的基本流程控制语句。它们的语法看起来很朴素,但实际上也会有一些使用技巧和注意事项。 更宽泛地讲,当只有一个迭代变量的时候,数组、数组的指针、切片和字符串的元素值都是无处安放的,我们只能拿到按照从小到大顺序给出的一个个索引值。 因此,这里的迭代变量i的值会依次是从0到5的整数。 毕竟,在 Go 语言中,只有类型相同的值之间才有可能被允许进行判等操作。 我们需要多加注意的往往是那些隐藏在 Go 语言规范和最佳实践里的细节。 这些细节其实就是我们很多技术初学者所谓的“坑”。 比如,我在讲for语句的时候交代了携带range子句时只有一个迭代变量意味着什么。
从今天开始,我会开始向你介绍使用 Go 语言进行模块化编程时,必须了解的知识,这包括几个重要的数据类型以及一些模块化编程的技巧。首先我们需要了解的是 Go 语言的函数以及函数类型。 前导内容:函数是一等的公民 在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型。这是什么意思呢? Go 语言在语言层面支持了函数式编程。我们下面的问题就与此有关。 今天的问题是:怎样编写高阶函数? 先来说说什么是高阶函数?简单地说,高阶函数可以满足下面的两个条件: 1. 即使对于像 Go 语言这种静态类型的编程语言而言,我们在定义闭包函数的时候最多也只能知道自由变量的类型。 你需要记住 Go 语言是怎样鉴别一个函数的,函数的签名在这里起到了至关重要的作用。 函数是 Go 语言支持函数式编程的主要体现。
Go语言核心36讲(Go语言进阶技术八)--学习笔记 14 | 接口类型的合理运用 前导内容:正确使用接口的基础知识 在 Go 语言的语境中,当我们在谈论“接口”的时候,一定指的是接口类型。 然后,Go 语言会用我上面提到的那个专用数据结构iface的实例包装这个dog2的值的副本,这里是nil。 在 Go 语言中,我们把由字面量nil表示的值叫做无类型的nil。这是真正的nil,因为它的类型也是nil的。 虽然dog2的值是真正的nil,但是当我们把这个变量赋给pet的时候,Go 语言会把它的类型和值放在一起考虑。 也就是说,这时 Go 语言会识别出赋予pet的值是一个*Dog类型的nil。 Go 语言团队鼓励我们声明体量较小的接口,并建议我们通过这种接口间的组合来扩展程序、增加程序的灵活性。
新年彩蛋 | 完整版思考题答案 基础概念篇 Go 语言在多个工作区中查找依赖包的时候是以怎样的顺序进行的? 答:你设置的环境变量GOPATH的值决定了这个顺序。 答:狭义上讲是不可以的,但是广义上讲是可以的。这需要一些定制化的工作,并且被给定的参数值只能是序列化的。具体可参见flag代码包文档中的例子。 XXX这种方式导入的代码包中的变量与当前代码包中的变量重名了,那么 Go 语言是会把它们当做“可重名变量”看待还是会报错呢? 答:这两个变量会成为“可重名变量”。 答:其实这个事情可以让 Go 语言自己来做,例如: switch t := x. 有了runtime/trace代码包,我们就可以为 Go 程序加装上可以满足个性化需求的跟踪器了。Go 语言标准库中有的代码包正是通过使用该包实现了自身的功能,例如net/http/pprof包。
10 | 通道的基本操作 作为 Go 语言最有特色的数据类型,通道(channel)完全可以与 goroutine(也可称为 go 程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学。 这是作为 Go 语言的主要创造者之一的 Rob Pike 的至理名言,这也充分体现了 Go 语言最重要的编程理念。 前导内容:通道的基础知识 通道类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类型。它使用起来十分简单,并不会徒增我们的心智负担。 在声明并初始化一个通道的时候,我们需要用到 Go 语言的内建函数make。就像用make初始化切片那样,我们传给这个函数的第一个参数应该是代表了通道的具体类型的类型字面量。 最后别忘了,通道也是 Go 语言的并发编程模式中重要的一员。 思考题 我希望你能通过试验获得下述问题的答案。 通道的长度代表着什么?它在什么时候会通道的容量相同?
19 | 错误处理(上) 提到 Go 语言中的错误处理,我们其实已经在前面接触过几次了。 比如,我们声明过error类型的变量err,也调用过errors包中的New函数。 我在前面讲函数用法的时候也提到过卫述语句。简单地讲,它就是被用来检查后续操作的前置条件并进行相应处理的语句。 对于echo函数来说,它进行常规操作的前提是:传入的参数值一定要符合要求。 问题解析 如果你看过一些 Go 语言标准库的源代码,那么对这几种情况应该都不陌生。我下面分别对它们做个说明。 类型在已知范围内的错误值其实是最容易分辨的。 在 Go 语言的标准库中也有不少以相同方式创建的同类型的错误值。 paths2 := []string{ runtime.GOROOT(), // 当前环境下的Go语言根目录。
然后,Go 语言会用我上面提到的那个专用数据结构iface的实例包装这个dog2的值的副本,这里是nil。 在 Go 语言中,我们把由字面量nil表示的值叫做无类型的nil。这是真正的nil,因为它的类型也是nil的。 虽然dog2的值是真正的nil,但是当我们把这个变量赋给pet的时候,Go 语言会把它的类型和值放在一起考虑。 也就是说,这时 Go 语言会识别出赋予pet的值是一个*Dog类型的nil。 Go 语言团队鼓励我们声明体量较小的接口,并建议我们通过这种接口间的组合来扩展程序、增加程序的灵活性。 Go 语言标准库代码包io中的ReadWriteCloser接口和ReadWriter接口就是这样的例子,它们都是由若干个小接口组合而成的。
由于在 Go 语言中实现接口是非侵入式的,所以我们可以做得很灵活。比如,在标准库的net代码包中,有一个名为Error的接口类型。 这是 Go 语言标准库给予我们的优秀范本,非常有借鉴意义。 不过要注意,如果你不想让包外代码改动你返回的错误值的话,一定要小写其中字段的名称首字母。 // 并且,这会影响到当前Go程序中所有的此类判断。 // 所以,一定要避免这样做! 我们先一起看了一下 Go 语言中处理错误的最基本方式,这涉及了函数结果列表设计、errors.New函数、卫述语句以及使用打印函数输出错误值。 接下来,我提出的第一个问题是关于错误判断的。 我在这里提出了两个在 Go 语言标准库中使用很广泛的方案,即:立体的错误类型体系和扁平的错误值列表。 之所以说错误类型体系是立体的,是因为从整体上看它往往呈现出树形的结构。
问题解析 这需要从两个方面讲,都跟函数的声明有些关系。 顺便说一下,我们在调用SendInt函数的时候,只需要把一个元素类型匹配的双向通道传给它就行了,没必要用发送通道,因为 Go 语言在这种情况下会自动地把双向通道转换为函数所需的单向通道。 另外,我们在 Go 语言中还可以声明函数类型,如果我们在函数类型中使用了单向通道,那么就相等于在约束所有实现了这个函数类型的函数。 它的用法我在后面讲for语句的时候专门说明。现在你只需要知道关于它的三件事: 上述for语句会不断地尝试从通道intChan2中取出元素值。 除此之外,Go 语言还有一种专门为了操作通道而存在的语句:select语句。 知识扩展 问题 1:select语句与通道怎样联用,应该注意些什么?
这个问题你可以在 Go 语言规范中找到答案,但却没那么简单。它的典型回答是:Go 语言字典的键类型不可以是函数类型、字典类型和切片类型。 这样的键值也不会让 Go 语言编译器报错,因为从语法上说,这样做是可以的。 Go 语言会用被查找键的哈希值与这些哈希值逐个对比,看看是否有相等的。如果一个相等的都没有,那么就说明这个桶中没有要查找的键值,这时 Go 语言就会立刻返回结果了。 当我们试图在一个值为nil的字典中添加键 - 元素对的时候,Go 语言的运行时系统就会立即抛出一个 panic。你可以运行一下 demo19.go 文件试试看。 我以 Go 语言规范为起始,并以 Go 语言源码为依据回答了这些问题。认真看了这篇文章之后,你应该对字典中的映射过程有了一定的理解。
那么 Go 语言的链表是什么样的呢? Go 语言的链表实现在标准库的container/list代码包中。 Go 语言标准库中很多结构体类型的程序实体都做到了开箱即用。这也是在编写可供别人使用的代码包(或者说程序库)时,我们推荐遵循的最佳实践之一。 实际上,Go 语言的切片就起到了延迟初始化其底层数组的作用,你可以想一想为什么会这么说的理由。延迟初始化的缺点恰恰也在于“延后”。 它们都是 Go 语言原生的数据结构,使用起来也都很方便. 不过,你的集合类工具箱中不应该只有它们。这就是我们使用链表的原因。
不过,我们那时大多指的是指针类型及其对应的指针值,今天我们讲的则是更为深入的内容。 让我们先来复习一下。 我们刚刚只提到了其中的一种情况,在 Go 语言中还有其他几样东西可以代表“指针”。其中最贴近传统意义的当属uintptr类型了。该类型实际上是一个数值类型,也是 Go 语言内建的数据类型之一。 再来看 Go 语言标准库中的unsafe包。unsafe包中有一个类型叫做Pointer,也代表了“指针”。 别忘了,我在讲结构体类型及其方法的时候还说过,我们可以在一个基本类型的值上调用它的指针方法,这是因为 Go 语言会自动地帮我们转译。 除此之外,我们都知道,Go 语言中的++和--并不属于操作符,而分别是自增语句和自减语句的重要组成部分。
21 | panic函数、recover函数以及defer语句 (上) 在本篇,我要给你展示 Go 语言的另外一种错误处理方式。 package main func main() { s1 := []int{0, 1, 2, 3, 4} e5 := s1[5] _ = e5 } Go 程序,确切地说是程序内嵌的 Go 语言运行时系统 注意,这里的 ID 其实并不重要,因为它只是 Go 语言运行时系统内部给予的一个 goroutine 编号,我们在程序中是无法获取和更改的。 在 Go 语言中,因 panic 导致程序结束运行的退出状态码一般都会是2。 这里的最外层函数指的是go函数,对于主 goroutine 来说就是main函数。但是控制权也不会停留在那里,而是被 Go 语言运行时系统收回。
Go 语言的内建函数recover专用于恢复 panic,或者说平息运行时恐慌。recover函数无需任何参数,并且会返回一个空接口类型的值。 这里存在一些限制,有一些调用表达式是不能出现在这里的,包括:针对 Go 语言内建函数的调用表达式,以及针对unsafe包中的函数的调用表达式。 顺便说一下,对于go语句中的调用表达式,限制也是一样的。 其实也并不复杂,在defer语句每次执行的时候,Go 语言会把它携带的defer函数及其参数值另行存储到一个链表中。 下面该你出场了,我在 demo51.go 文件中编写了一个与本问题有关的示例,其中的核心代码很简单,只有几行而已。 以上这些,就是关于 Go 语言中特殊的程序异常,及其处理方式的核心知识。这里边可以衍生出很多面试题目。 思考题 我们可以在defer函数中恢复 panic,那么可以在其中引发 panic 吗?
07 | 数组和切片 我们这次主要讨论 Go 语言的数组(array)类型和切片(slice)类型。 它们的共同点是都属于集合类的类型,并且,它们的值也都可以用来存储某一种类型的值(或者说元素)。 也正因为如此,Go 语言的切片类型属于引用类型,同属引用类型的还有字典类型、通道类型、函数类型等;而 Go 语言的数组类型则属于值类型,同属值类型的有基础数据类型以及结构体类型。 注意,Go 语言里不存在像 Java 等编程语言中令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。 一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。 但是,当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的1.25倍作为新容量的基准(以下新容量基准)。
不过,在 Go 程序当中,Go 语言的运行时(runtime)系统会帮助我们自动地创建和销毁系统级的线程。这里的系统级线程指的就是我们刚刚说过的操作系统提供的线程。 M、P、G 之间的关系(简化版) 由于篇幅原因,关于 Go 语言内部的调度器和运行时系统的更多细节,我在这里就不再深入讲述了。 你需要知道,Go 语言实现了一套非常完善的运行时系统,保证了我们的程序在高并发的情况下依旧能够稳定、高效地运行。 严谨地讲,Go 语言并不会去保证这些 goroutine 会以怎样的顺序运行。 总结 今天,我描述了 goroutine 在操作系统的并发编程体系,以及在 Go 语言并发编程模型中的地位和作用。
顺便说一句,我在讲“结构体及其方法的使用法门”的时候留过一道与此相关的思考题,你可以返回去看一看。 再说回当下的问题,有没有比使用通道更好的方法? for i := 0; i < 10; i++ { go func(i int) { fmt.Println(i) }(i) } 只有这样,Go 语言才能保证每个 goroutine 都可以拿到一个唯一的整数 其原因与go函数的执行时机有关。 我在前面已经讲过了。在go语句被执行时,我们传给go函数的参数i会先被求值,如此就得到了当次迭代的序号。之后,无论go函数会在什么时候执行,这个参数值都不会变。 也就是说,go函数中调用的fmt.Println函数打印的一定会是那个当次迭代的序号。 然后,我们在着手改造for语句中的go函数。 纵观count变量、trigger函数以及改造后的for语句和go函数,我要做的是,让count变量成为一个信号,它的值总是下一个可以调用打印函数的go函数的序号。
Go 语言中的程序实体包括变量、常量、函数、结构体和接口。 Go 语言是静态类型的编程语言,所以我们在声明变量或常量的时候,都需要指定它们的类型,或者给予足够的信息,这样才可以让 Go 语言能够推导出它们的类型。 问题:声明变量有几种方式? 这里利用了 Go 语言自身的类型推断,而省去了对该变量的类型的声明。 你可以认为,表达式类型就是对表达式进行求值后得到结果的类型。Go 语言中的类型推断是很简约的,这也是 Go 语言整体的风格。 Go 语言的类型推断可以带来哪些好处? 当然,在写代码时,我们通过使用 Go 语言的类型推断,而节省下来的键盘敲击次数几乎可以忽略不计。 在 Go 语言中,代码块一般就是一个由花括号括起来的区域,里面可以包含表达式和语句。Go 语言本身以及我们编写的代码共同形成了一个非常大的代码块,也叫全域代码块。 回到变量重声明的问题上。