【Golang 之旅】01 语言简介
作为系列的第一篇,本文会介绍 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
变量与常量
直接看代码
// 声明变量
var item int
// 声明多个变量
var item1, item2 int
// 自动根据值生成变量
item1 := "wdxtub.com"
// 注:基本类型是值类型,指针等是引用类型
// 声明常量
const NAME string = "wdxtub"
// 常量枚举
const (
UNKNOWN = 0
SHENZHEN = 1
GUANGZHOU = 2
)
// iota 的用法,和前面是等价的
const (
UNKNOWN = iota
b
c
)
思考题 1:没有初始化的变量,默认值是什么?
对应代码为 1.go,实验结论如下:
- 字符串默认值为空
- 整型、浮点型默认值为 0
- 布尔型默认值为 false
- 指针、channel、func、接口都为 nil
- 数组默认为
[] - 字典默认为
map[]
思考题 2:能否准确给出包含 iota 的代码的执行结果?
对应代码为 2.go,实验结论结论如下,你答对了吗:
0 1 2 wdxtub wdxtub wdxtub 10086 10086 10086 9 10 111 2 4 16 32 64分别对应1 << 0, 1 << 1, 1 << 2, 2 << 3, 2 << 4, 2 << 5
运算符
运算这部分比较常规,简单列举一下,本节没有思考题:
- 算数运算符:
+-*/%++-- - 关系运算符:
==!=><>=<= - 逻辑运算符:
&&||! - 位运算符:
&|^<<>> - 赋值运算符:
=+=-=*=/=%=<<=>>=&=^=|= - 其他运算符:
&(取地址)*(指针)
条件语句与循环
if 语句
a := 10
if a % 3 == 0 {
fmt.Println('a 能被 3 整除')
} else if a % 3 == 1 {
fmt.Println('a-1 能被 3 整除')
} else {
fmt.Println('a-2 能被 3 整除')
}
switch 语句
a := 10
// 一种写法
switch a % 3 {
case 0:
fmt.Println('a 能被 3 整除')
case 1:
fmt.Println('a-1 能被 3 整除')
case 2:
fmt.Println('a-2 能被 3 整除')
}
// 也可以这么写
switch {
case a % 3 == 0:
fmt.Println('a 能被 3 整除')
case a % 3 == 1:
fmt.Println('a-1 能被 3 整除')
case a % 3 == 2:
fmt.Println('a-2 能被 3 整除')
fallthrough // 继续执行下一条语句
default:
fmt.Println('oh 到我了')
}
思考题 3:如何通过 switch 语句来判断变量类型?
对应代码为 3.go
思考题 4:如果 switch 后没有表达式,具体执行哪个 case?
对应代码为 4.go,结果为
2 - true
3 - false
4 - true
select 语句
select 语句主要用于控制通讯,要么是发送要么是接收,如果没有条件满足,则会一直阻塞(特定情况可以利用这样的特性,但一定要小心),具体语法为:
- 每个 case 都必须是一个通信
- 所有 channel 表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意一个通信可以进行,就会执行并忽略其他
- 如果多个 case 都可以运行,select 会随机选出一个执行
思考题 5:你能准确给出包含 select 语句的代码执行结果吗?
对应代码为 5.go,从结果中可以看到,在所有满足条件的 case 中会随机选一个执行。
思考题 6:你能准确给出包含 select 语句的代码执行结果吗?(来自小伙伴 jiasen 的一道题目)
对应代码为 6.go,这题综合了循环和 channel,是很好的题目
for 语句
这部分比较简单,直接上代码
// 正常循环
sum := 0
for i := 0; i <= 10; i++ {
sum += i
}
// 无限循环
sum = 0
for {
sum += 220
if sum > 1000 {
break
}
}
函数
函数本身的定义比较简单,直接看代码就可以,重点在于把函数当做变量来处理的时候,这个后面会介绍。我们先来看看基本的函数,下面的函数会把两个数字加起来
// 函数定义
func add(num1, num2 int) int {
return num1 + num2
}
// 函数调用
a, b := 220, 314
c := add(a, b)
有且仅有值传递
首先要强调的是,Golang 中只有值传递,但是具体传的值,可以分为引用类型(指针/map/slice/chan/等)和非引用类型(int/string/struct/array/等)
对于函数的进一步了解,需要掌握的是值传递和引用传递,简单来说就是值传递的变量在函数中被修改后,原变量不变,而传引用的变量,原变量也会改变。
思考题 7:传引用和引用类型一样吗?你能说出下面代码的执行结果吗?
对应代码为 7.go,从结果中可以明确看到引用类型和非引用类型的差别
函数作为参数
我们可以把函数直接传给另一个函数,以实现更加灵活的代码编写,比如我可以写这样一个自定义打印函数:
func main() {
// 函数变量
customStr := func(content string) {
return "[wdxtub.com]" + content
}
// 使用函数
fmt.Println(customStr("Hello World"))
}
思考题 8:如何通过传入函数实现回调?
对应代码为 8.go,掌握了这个部分之后,就可以开始继续学习闭包了。
闭包
熟悉了如何把函数当做参数传入,就可以通过闭包(匿名函数)的方式实现一些特定的功能,闭包给访问外部函数定义的内部变量创造了条件,将关于的函数的一切封闭到了函数内部,减少了全局变量。
比如,我们可以通过闭包非常方便地实现不同等级的日志打印,具体如下:
type logFunc func(format string, v...interface{})
// Logger 包装
func LoggerWrapper(logType string) logFunc {
return func(format string, v...interface{}) {
fmt.Printf(fmt.Sprintf("[%s] %s", logType, format), v...)
fmt.Println()
}
}
// 具体使用
func main() {
infoLogger = LoggerWrapper("INFO")
debugLogger = LoggerWrapper("DEBUG")
errorLogger = LoggerWrapper("ERROR")
infoLogger("Get a %s log", "info")
debugLogger("Get a %s log", "debug")
errorLogger("Get a %s log", "error")
}
思考题 9:你能准确说出以下包含闭包的函数执行结果吗?
对应代码为 9.go
结构体
主要用于面向对象的封装,比较灵活,直接看代码
type Book struct {
title string
author string
date string
}
// 使用
func main() {
book := Book{"《今天是周末吗》", "小土刀", "2020-09-29"}
fmt.Println(book.title)
}
数组与切片
Golang 中的数组是有固定长度的,一旦创建了就不允许改变,空余位置用 0 填补并且不允许越界。但是实际使用的时候,固定长度的数组不是很方便,所以我们一般都用切片来操作。关于数组我们来看一段代码,然后主要来介绍切片。
// 一维数组
a := [3]int{1,2,3}
b := [...]int{1,2,3,4}
// 二维数组
a := [2][3]int{
{1, 2, 3},
{2, 3, 4}
}
切片的底层结构如下,是一个包含指向数组的地址和数据元素的结构体:
type slice struct {
array unsafe.Pointer // 指向数组的指针
len int // 切片中的元素数量
cap int // array 数组的总容量
}
// 深浅拷贝的差别
copy(sliceA, sliceB) // 深拷贝
sliceA = sliceB // 浅拷贝
因为切片的底层是数组,当容量不够的时候,是需要进行扩容的。
思考题 10:你能说出切片的扩容逻辑吗?
对应代码为 10.go
通过 append 扩容的时候,指针会发生变化,原切片不受影响。在 1024 大小之前每次扩容会翻倍,之后会按照 1.25 倍扩容。
Range 与 Map
range 可以用于遍历 array/slice/channel/map,在 array/slice 中返回索引和对应值,在 map 中返回 key-value 对。具体代码如下:
names := []string{"wdx", "tub", "wdxtub"}
for _, name := range names {
fmt.Println(name)
}
nameDict := map[string]int{"wdx":1, "tub":2, "wdxtub":3}
for k, v := range nameDict {
fmt.Println(k, v)
}
range 相对来说比较清晰,这里我们重点看一下 map 及其底层实现。map 的基础用法如下:
nameAgeMap := map[string]int{}
nameAgeMap["wdxtub"] = 30
nameAgeMap["yiyi"] = 1
nameAgeMap["snail"] = 20000
// 删除 key
delete(nameAgeMap, "snail")
在目前最新的 go 1.15 中,map 的实现在 src/runtime/map.go 文件中,这里我们只简单了解一下 map 的底层结构
// Go map 类型的结构定义
type hmap struct {
count int // 包含元素的个数,调用 len() 会返回这个值
flags uint8 // 状态标识
B uint8 // 可以最多容纳 loadFactor * 2 ^ B 个元素,这里 B = log_2 buckect 数量
noverflow uint16 // 近似的溢出 bucket 个数
hash0 uint32 // 哈希种子
buckets unsafe.Pointer // 桶的地址 array of 2^B Buckets,count 为 0 时值可能为 nil
oldbuckets unsafe.Pointer // 旧桶的地址,用于扩容
nevacuate uintptr // 迁移进度,小于该值的 bucket 已经迁移成功
extra *mapextra // 可选,用于保存溢出桶的地址
}
更加具体的说明可以参考 这里,虽然代码版本较老,但是核心思想没有变化。
类型转换与错误处理
类型转换和其他强类型的语言非常类似,直接加类型,具体如下:
oneInt := 17
oneFloat := 17.2
int2Float := float32(oneInt)
float2Int := int(oneFloat)
fmt.Println(int2Float)
fmt.Println(float2Int)
oneFloat = 17.6
float2Int = int(oneFloat)
fmt.Println(float2Int)
思考题 11:你能说出上面类型转换的最终结果吗?
对应代码为 11.go,答案是 17 17 17,并没有四舍五入。
错误处理是 Golang 里我最喜欢的部分,简单直接,不搞什么花里胡哨的,通过内置的错误接口来处理,不搞什么 try catch,定义如下:
// 只要返回 string 就可以,后面可以根据 string 来判断状态
type error interface {
Error() 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 语言层面相关的内容基本介绍完毕,接下来会基于这些基础知识,深入剖析经典的项目与设计。