作为系列的第一篇,本文会介绍 Golang 的基本概念和语法,后续会逐渐引入更多更深入的内容。
更新历史
- 2020.09.25: 开始撰写
- 2020.10.10: 完成第一版
Golang 简介
- 2007 年末,由Robert Griesemer, Rob Pike, Ken Thompson 主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人
- 2009 年 11 月开源
- 2010 年谷歌投入使用
- 2011 年被 Google AppEngine 支持
- 2012 年正式版 Go 1.0
- 2015 年发布 Go 1.5,移除了“最后残余的C代码”。这个版本被认为是历史性的。完全移除 C 语言部分,使用 Go 编译 Go,少量代码使用汇编实现。GC 也重新设计,支持并发GC,解决了一直以来广为诟病的GC时延(STW)问题。
- 最新版本 Go 1.15 (2020.08.11),具体的发布历史查看 这里
开发者基本都是神级人物
- 肯·汤普逊(Ken Thompson):设计了 B 语言和 C 语言,创建了 Unix 和 Plan 9 操作系统,1983 年图灵奖得主,Go 语言的共同作者。
- 罗布·派克(Rob Pike): Unix 小组的成员,参与 Plan9 和 Inferno 操作系统,参与 Limbo 和 Go 语言的研发,《Unix编程环境》作者之一。
- 罗伯特·格里泽默(Robert Griesemer):曾协助制作 Java 的 HotSpot 编译器和 Chrome 浏览器的 JavaScript 引擎V8。
- 布拉德·菲茨帕特里克(Brad Fitzpatrick):LiveJournal 的创始人,著名开源项目 memcached 的作者。
核心设计思想
- Less is More,大道至简
- 专注于工程需要而非语言研究,为软件工程服务
- 完整性、可读性和简单性
- 如果说 C++/Java 是关于类型继承和类型分类的,Go 就是关于组合的
优势和劣势见仁见智,这里我就挑一些个人最在意的点简单说明一下:
- 优势
- 语言层面天生支持并发
- 丰富的标准库 + 完整的工具链,简单直观,没有那么多套路
- 部署简单,直接就是一个二进制文件
- 劣势
- 依赖管理和包管理
- 不支持很多在其他语言中习惯的用法,如泛型、静态变量等
适用场景
- 服务器编程
- 分布式系统
- 网络编程
- 内存数据库
- 云平台
其他更多资讯请参考 Documentation,这里不再赘述。
思考题合集
这里整理了整篇文章中的各个思考题,方便大家查漏补缺,对应实验代码放在这里,大家可以自行查看:
- 没有初始化的变量,默认值是什么?
- 能否准确给出包含 iota 的代码的执行结果?
- 如何通过 switch 语句来判断变量类型?
- 如果 switch 后没有表达式,具体执行哪个 case?
- 你能准确给出包含 select 语句的代码执行结果吗?
- 你能准确给出包含 select 语句的代码执行结果吗?(powered by jiasen)
- 传引用和引用类型一样吗?你能说出下面代码的执行结果吗?
- 如何通过传入函数实现回调?
- 你能准确说出以下包含闭包的函数执行结果吗?
- 你能说出切片的扩容逻辑吗?
- 你能说出上面类型转换的最终结果吗?
- 你能给出下面并发代码的执行结果吗?
- 给你一个数组,你能通过 channel 用 2 个 goroutine 完成求和计算吗?
- 你能通过 range 和 channel 打印出斐波那契数列吗?
基础语法
这里简单整理 Golang 的基础语法,可以当做一个简单的手册快速查询。这部分以实战为主,会省略很多具体的概念,建议大家先找个教程过一遍,实际编码的时候再看这部分。
数据类型
对应类型会在后面详细介绍,这里只列举与简单说明:
- 布尔型
true/false
,例如bool := false
- 数字类型
- 整型 int:
unit8/uint16/uint32/uint64/int8/int16/int32/int64
- 浮点型:
float32/float64/complex64/complex128
- 其他类型:
byte
(类似 uint8),rune
(类似 int32),int/uint
(32 或 64 位),uintptr
(无符号整型,用于存放指针)
- 整型 int:
- 字符串类型:由单个字节链接起来,默认为 UTF-8 Unicode
- 派生类型:
指针/数组/函数/struct/channel/slice/interface/map
变量与常量
直接看代码
1 | // 声明变量 |
思考题 1:没有初始化的变量,默认值是什么?
对应代码为 1.go
,实验结论如下:
- 字符串默认值为空
- 整型、浮点型默认值为 0
- 布尔型默认值为 false
- 指针、channel、func、接口都为 nil
- 数组默认为
[]
- 字典默认为
map[]
思考题 2:能否准确给出包含 iota 的代码的执行结果?
对应代码为 2.go
,实验结论结论如下,你答对了吗:
0 1 2 wdxtub wdxtub wdxtub 10086 10086 10086 9 10 11
1 2 4 16 32 64
分别对应1 << 0, 1 << 1, 1 << 2, 2 << 3, 2 << 4, 2 << 5
运算符
运算这部分比较常规,简单列举一下,本节没有思考题:
- 算数运算符:
+
-
*
/
%
++
--
- 关系运算符:
==
!=
>
<
>=
<=
- 逻辑运算符:
&&
||
!
- 位运算符:
&
|
^
<<
>>
- 赋值运算符:
=
+=
-=
*=
/=
%=
<<=
>>=
&=
^=
|=
- 其他运算符:
&
(取地址)*
(指针)
条件语句与循环
if 语句
1 | a := 10 |
switch 语句
1 | a := 10 |
思考题 3:如何通过 switch 语句来判断变量类型?
对应代码为 3.go
思考题 4:如果 switch 后没有表达式,具体执行哪个 case?
对应代码为 4.go
,结果为
1 | 2 - true |
select 语句
select 语句主要用于控制通讯,要么是发送要么是接收,如果没有条件满足,则会一直阻塞(特定情况可以利用这样的特性,但一定要小心),具体语法为:
- 每个 case 都必须是一个通信
- 所有 channel 表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意一个通信可以进行,就会执行并忽略其他
- 如果多个 case 都可以运行,select 会随机选出一个执行
思考题 5:你能准确给出包含 select 语句的代码执行结果吗?
对应代码为 5.go
,从结果中可以看到,在所有满足条件的 case 中会随机选一个执行。
思考题 6:你能准确给出包含 select 语句的代码执行结果吗?(来自小伙伴 jiasen 的一道题目)
对应代码为 6.go
,这题综合了循环和 channel,是很好的题目
for 语句
这部分比较简单,直接上代码
1 | // 正常循环 |
函数
函数本身的定义比较简单,直接看代码就可以,重点在于把函数当做变量来处理的时候,这个后面会介绍。我们先来看看基本的函数,下面的函数会把两个数字加起来
1 | // 函数定义 |
有且仅有值传递
首先要强调的是,Golang 中只有值传递,但是具体传的值,可以分为引用类型(指针/map/slice/chan/等)和非引用类型(int/string/struct/array/等)
对于函数的进一步了解,需要掌握的是值传递和引用传递,简单来说就是值传递的变量在函数中被修改后,原变量不变,而传引用的变量,原变量也会改变。
思考题 7:传引用和引用类型一样吗?你能说出下面代码的执行结果吗?
对应代码为 7.go
,从结果中可以明确看到引用类型和非引用类型的差别
函数作为参数
我们可以把函数直接传给另一个函数,以实现更加灵活的代码编写,比如我可以写这样一个自定义打印函数:
1 | func main() { |
思考题 8:如何通过传入函数实现回调?
对应代码为 8.go
,掌握了这个部分之后,就可以开始继续学习闭包了。
闭包
熟悉了如何把函数当做参数传入,就可以通过闭包(匿名函数)的方式实现一些特定的功能,闭包给访问外部函数定义的内部变量创造了条件,将关于的函数的一切封闭到了函数内部,减少了全局变量。
比如,我们可以通过闭包非常方便地实现不同等级的日志打印,具体如下:
1 | type logFunc func(format string, v...interface{}) |
思考题 9:你能准确说出以下包含闭包的函数执行结果吗?
对应代码为 9.go
结构体
主要用于面向对象的封装,比较灵活,直接看代码
1 | type Book struct { |
数组与切片
Golang 中的数组是有固定长度的,一旦创建了就不允许改变,空余位置用 0 填补并且不允许越界。但是实际使用的时候,固定长度的数组不是很方便,所以我们一般都用切片来操作。关于数组我们来看一段代码,然后主要来介绍切片。
1 | // 一维数组 |
切片的底层结构如下,是一个包含指向数组的地址和数据元素的结构体:
1 | type slice struct { |
因为切片的底层是数组,当容量不够的时候,是需要进行扩容的。
思考题 10:你能说出切片的扩容逻辑吗?
对应代码为 10.go
通过 append
扩容的时候,指针会发生变化,原切片不受影响。在 1024 大小之前每次扩容会翻倍,之后会按照 1.25 倍扩容。
Range 与 Map
range
可以用于遍历 array/slice/channel/map,在 array/slice 中返回索引和对应值,在 map 中返回 key-value 对。具体代码如下:
1 | names := []string{"wdx", "tub", "wdxtub"} |
range
相对来说比较清晰,这里我们重点看一下 map
及其底层实现。map 的基础用法如下:
1 | nameAgeMap := map[string]int{} |
在目前最新的 go 1.15 中,map 的实现在 src/runtime/map.go
文件中,这里我们只简单了解一下 map 的底层结构
1 | // Go map 类型的结构定义 |
更加具体的说明可以参考 这里,虽然代码版本较老,但是核心思想没有变化。
类型转换与错误处理
类型转换和其他强类型的语言非常类似,直接加类型,具体如下:
1 | oneInt := 17 |
思考题 11:你能说出上面类型转换的最终结果吗?
对应代码为 11.go
,答案是 17 17 17,并没有四舍五入。
错误处理是 Golang 里我最喜欢的部分,简单直接,不搞什么花里胡哨的,通过内置的错误接口来处理,不搞什么 try catch,定义如下:
1 | // 只要返回 string 就可以,后面可以根据 string 来判断状态 |
并发与通道
Golang 在语言层面支持并发,使用也非常简单,只需要在函数前加上 go
关键字,就会开启 goroutine 执行,同一个程序中的所有 goroutine 共享同一个地址空间。
思考题 12:你能给出下面并发代码的执行结果吗?
对应代码为 12.go
,答案是什么都不输出,因为需主进程在 goroutine 执行之前就结束了,所以我们一般会通过 channel 来控制并发!
思考题 13:给你一个数组,你能通过 channel 用 2 个 goroutine 完成求和计算吗?
对应代码为 13.go
,我们可以利用两个 goroutine 分别计算前半和后半部分,并最终加起来。
通道还有一个用法就是和 range 搭配进行遍历,可以实现类似 python 中 yield 的效果,大家可以通过下面的思考题尝试一下
思考题 14:你能通过 range 和 channel 打印出斐波那契数列吗?
对应代码为 14.go
写在最后
至此,Golang 语言层面相关的内容基本介绍完毕,接下来会基于这些基础知识,深入剖析经典的项目与设计。