Go 基础速描
Go 基础速描,非教程.
资料来源:
https://tour.go-zh.org/list
更新
1
2022.03.21 初始
导语
开始尝试在接下来的一段时间内以兴趣为主,过渡一段时间,找一下抽象的欲望是什么东西.
转行到 CS 到现在,似乎只是抽象的对计算机感兴趣,但是对计算机领域那一块最想当作事业,却非常不明确,这样也导致了很多问题.因此从计算机网络入手,开始找找抽象的欲望: 最想折腾什么?
这不是教程,不是教程,不是教程.
语法1
go 初看的感觉非常像替换 c 语言,然后夹杂了 kotlin/python 语法,又删掉了一些符号.
包管理
1 | package main |
- 总是从 main 包开始运行
- 包名与导入路径的最后一个元素一致,这里是
rand
import (**)
是分组导入,当然可以是单独写多个 import,不过分组导入更实用.- go 包中以 大写字母开头,那就是已导出的,可以直接访问.如果是小写字母那包外无法直接访问.类似于
math.Pi
与math.pi
.
函数
1 | func add(x int, y int) int { |
int x
->x:int
->x int
: 类型在后,没有冒号.初上手最奇怪的一点.- 多个类型相同时,可只保留最后一个.
x,y int
- 可以返回任意数量返回值
return
可用没有参数,直接返回已命名的参数.- 第二个示例实在有点雷人,直接返回 xy.
- xy 写在了函数返回值,相当于已经声明了 xy.
变量
1 | var i int |
- 与函数参数类似,没有
:
,定义的位置可以随意. - 提供初始值则类型可省略
- 简便写法
:=
只能用于函数.- 简便写法的类型会是自动推导的,数字与其输入的精度有关.
- 多个变量声明可用写成一个语法块
类型
- bool string
- int int8 16 32 64
- unit unit8 16 32 64 unitstr
- float32 float64
- complex 32 complex128
- byte = unit8 rune = int32(unicode码点)
- int unit uintpter 32位默认是 32,64 位默认是 64.
- 没有初始值时,数字 = 0,bool = false,字符串 = “”. ^761ff9
- go 没有隐藏的类型转换, 全部需要显式转换,即使是数字.
i := 42 , f := float64(i)
常量
1 | const Pi = 3.14 |
const
声明,包括类似const(xx)
语法块的写法.- 不支持
:=
简便写法. - 未指定类型常量由上下文确定
- needInt 和 needFloat 都可以调用 Small
循环-for(有且仅有 for)
1 | for i := 0; i < 10; i++ { |
- 感觉与 c/java 的 for 去掉小括号
- 没有小括号! 所以大括号必须的.
- 初始化和后置语句可以省略 or 放在别的地方
- 写法类似 第二/三. while 的代替
条件分支-if
1 | if x < 0 { |
- 与 c/java 类似,还是没有小括号(杠上了是吗?),大括号必须.
- if 条件处可以执行简单语句,这一点和其他语言非常不一样
- 但这个变量的作用域仅在 if/else 内部
练习: 牛顿法求平方根
1 | func sqrt(x float64, n int) float64 { |
条件分支-switch
1 | switch os := runtime.GOOS; os { |
- 与 c/java 语法 去掉小括号.
- go switch 只能匹配一个,除非
fallthrough
.- 相当于 go 为每个分支自动添加了
break
- 相当于 go 为每个分支自动添加了
case
无需是常量.- 执行顺序从上到下
- 没有条件的 switch 相当于一串更简洁的 if/else
defer 延迟?
1 | // 先执行 xx2 然后才执行 xx1 |
- defer 这个作用有点凌乱,属于完全没见过…
- defer 会依次将 xx1 xx2 xx3 推到一个堆栈里,在外层的 xx4 执行了 return 后才出栈,因此执行顺序会是 xx4 xx3 xx2 xx1.
- 若 xx 是个函数,那么函数参数在被压入堆栈时,值就确定了.
- 闭包就没这个问题
语法2
逃了 c/c++ 还是没能逃过结构体和指针…
指针
1 | var p *int //int 类型指针 |
- 谢天谢地没有指针运算.
- 未初始化默认值:
nil
&i
取地址*p
取值,还是熟悉的味道.
结构体
1 | type Vertex struct { |
- 语法与 c 类似.
- 结构体定义后,与普通类型使用基本相同.
- 初始化时
{Name:xx}
,其他成员 !默认初始值
数组
1 | var a [10]int //创建数组,必须指定长度 |
- 声明时类型在后, 必须指定长度,长度是数组的一部分
- 数组大小不可变 -> 切片(变相的动态数组)
切片
1 | var s []int = a[1:4] // 切片 |
- 切片
- 定义时 类型在后, 切片范围不包括下界.
- 取切片时默认动作与 py 相同,
a[:]
a[:10]
a[2:]
- 任何对切片元素的修改会直接反映在原数组上.
- 切片与 py 最大的区别是有长度和容量的概率
- 长度: 元素的个数
len()
- 容量: 第一个切片元素到数组最后一个元素的长度
cap()
- 长度: 元素的个数
- 空切片: 直接指向
nil
,长度 0 容量 0,字符串输出是[]
make
创建新的切片,可指定长度/容量.- 切片算是简化版指针? 指向切片的切片也是可以的.
append
向切片添加元素,底层数组容量不足,会直接创建新的底层数组 -> 虚拟的动态数组- 循环
- 默认 for 循环是循环 下标+元素副本
- 类似 py 可用
_
忽略 下标 or 元素副本. - 仅用索引可省略
_
练习:
- 应当返回一个长度为 dy 的切片,其中每个元素是一个长度为 dx,元素类型为 uint8 的切片。
import "golang.org/x/tour/pic"
pic.Show(Pic)
每个整数解释为蓝度值并显示它所对应的图像。- 图像的选择由你来定: (x+y)/2, x_y, x^y, x_log(y) 和 x%(y+1)。
- 提示:需要使用循环来分配 [][]uint8 中的每个 []uint8;请使用 uint8(intValue) 在类型之间转换;你可能会用到 math 包中的函数。
1 | func Pic(dx, dy int) [][]uint8 { |
- 逻辑上并不复杂
- 使用切片实现二维数组基本没这么干过,但这可能是 go 的常态
映射: 怎么感觉这么像字典的变体?
1 | var m map[string]Vertex |
- 映射:
[类型]类型
- 键映射到值,未初始化是
nil
,make
可建立映射. - 直接初始化时,顶级类型可省略.
- 访问和字典一样
- 双赋值
- key 存在,ok true,不存在,ok false.
- elem 要不然是真正的值,要不然是 对应的零值
练习: 单词统计
1 | //strings.Fields 是以空格分割字符串的 go 标准库函数 |
函数值: go 自带高阶函数
1 | func compute(fn func(float64, float64) float64) float64 { |
- 函数也可以当作值传递,函数参数/函数返回值都行.
func(float64, float64) float64
类似这样的声明
闭包: 匿名函数
1 | func adder() func(int) int { |
- 闭包是一个函数,
- 能引用定义在闭包以外的函数体的变量,访问/修改,有些类似内部类/内部方法的意思.
练习: 斐波那契额闭包
1 | func fibonacci() func() int { |
- 有类似 py yiled 的感觉
方法和接口
方法 (接收者 ≈ 调用者)
- go 没有类,而是与 c 一样用结构体一定程度代替.
- 指定接收者的特殊函数
func (接收者) 方法名 函数参数{}
1 | type Vertex struct { |
- 值接收者,方法内部访问的是实例的副本,修改无效.
- 接收者定义与使用其定义的方法必须在同一个包
- 不能为内建类型声明方法 (
type
可解)
- 不能为内建类型声明方法 (
1 | // 传入指针,可以修改结构体值 |
- 指针接收者
- 能修改指向结构体实例的成员,非常常用.
- 无论是值接收者 or 指针接收者, 都能透过结构体 实例 or 指针调用方法.
- 两者语法相同,使用时不区分.
go 所有数据都是值传递
- 指针到真实值的映射, c 语言中是 p->X, 而 go 直接将这个映射简化为了 p.X. 与 a.X 形式相同.也就说 go 中 真实值 和 指向真实值的指针 可以等同.(暂时理解)
- a -> p 不能自动实现,原因是 一个指针肯定对应一个真实值,但反过来真实值可能对应多个指针.
接口:
- 一组方法的集合; Go 的类型系统核心: 根据行为设计抽象,而不是根据类型设计抽象.
- 实现了接口的所有方法 = 实现了接口; 没有显式的
implements
非常像鸭子类型. - 接口是一种类型 ; 参数;返回;赋值;xxx; 都行
1 | type Abser interface { |
- 接口内部实现是两个指针
(value,type)
- 一个指向真实值
- 一个指向类型
空接口:
- 因为是空,相当于所有类型都实现了空接口.
- 空接口类型可以存放任意类型的实例, 有些类似 java 的 object 类型.
1 | var i interface{} // 空接口 |
- 可以承载任意类型,其他任意 = 空接口, 但这个特质并不能延续到 空接口的数组/切片/xxx
- 到底空接口也是接口,是一种类型;
- 两个指针,一个指向类型,一个指向真实值,虽然这里默认情况都是
nil
.
- 两个指针,一个指向类型,一个指向真实值,虽然这里默认情况都是
- 其他类型的数组切片 = 空接口的数组/切片,底层存储方式完全不同,肯定报错.
- 除非先行把数组类所有元素都 = 空接口了,这样存储方式就一样了.
1 | var i interface{} = "hello" |
- 类型断言: 从接口访问指向的实例.
- 虽然常常和空接口联合使用.
- 映射同款判断是否取值成功语法.
- 取值不成功,则为传入类型的默认值.
- 有的时候类型太多,一个一个断言太麻烦 -> 类型选择: 以 switch 形式的多组类型断言
常用接口
1 | type Stringer interface { |
- Stringer 是
fmt
包定义的最常见的接口: 类型如何以字符串描述自己. - 基本上很多包都是以这个接口打印输出.
练习: 输出 IPAddr{1, 2, 3, 4}
类型实现 Stringer
接口,输出 1.2.3.4
1 | type IPAddr [4]byte |
Go 最让人崩溃的地方就是繁琐的错误处理
1 | type error interface { |
- 所谓错误,也只是一个
Error
类型的接口. - 默认的错误处理就是: 有错误发生时,返回的 err 不为空.
- 没有错误堆栈追踪,基本都靠第三方框架
练习: 开方 Sqrt 接受到一个负数 or 复数 返回一个非 nil 的错误值.
type
创建 float64 新类型- 实现
func (e ErrNegativeSqrt) Error() string
1 | type ErrNegativeSqrt float64 |
io.Reader 接口: 从数据流末尾读取.
- 非常多的数据流的地方有这个接口的实现,网络/文件/加解密等等
io.Reader
接口的Read
方法Read
用数据填充给定的字节切片并返回填充的字节数和错误值- 在遇到数据流的结尾时,它会返回一个
io.EOF
错误。
1 | import ( |
练习: 实现一个 Reader 类型,它产生一个 ASCII 字符 ‘A’ 的无限流.
1 | import "golang.org/x/tour/reader" |
练习: 对流的修改,编写一个实现了 io.Reader
并从另一个 io.Reader
中读取数据的 rot13Reader
,通过应用 rot13 代换密码对数据流进行修改。
1 | type rot13Reader struct { |
Image
接口
1 | package image |
Bounds
方法的返回值Rectangle
实际上是一个image.Rectangle
,它在image
包中声明.color.Color
和color.Model
类型也是接口,但常用image.RGBA
和image.RGBAModel
. ->image/color
练习: 定义你自己的 Image
类型,实现必要的方法并调用 pic.ShowImage
。
- Bounds 应当返回一个 image.Rectangle ,例如 image.Rect(0, 0, w, h)。
ColorModel
应当返回color.RGBAModel
。At
应当返回一个颜色。上一个图片生成器的值v
对应于此次的color.RGBA{v, v, 255, 255}
。
1 | type nsImage struct{} |
并发
来到 go 协程的主场了,似乎与 kotlin 协程在概念上有很多相似.
- 也别纠结在概念,就当作用户控制的轻量级线程罢了.
1 | go f(x, y, z) // 启动一个协程 |
协程直接交换信息: chan or sync
chan 信道
1 | ch := make(chan int) //初始化信道 |
- 箭头是数据流向,非常直观.
- 默认是发送后就阻塞,直到有接受才能再次发送,创建时手动设置缓存,一直发直到彻底灌满.
- 只有发送方才能
close
关闭信道,一般无需关闭信道,除非是需要显示通知接收方,例如在循环中,跳出循环. select
配合 chan 倒是头一次见,阻塞一直到某个 case 可以执行,多个随机选一个- 不想要阻塞 -> 增加 default 分支.
sync 互斥锁
1 | mux := sync.Mutex |
- 与 c 的使用基本相同,上锁解锁.
defer
可以保证一定会解锁
练习: 检查两个二叉树保存的序列是否相同,二叉树前序遍历.
使用 tree 定义了下面的二叉树类型
1 | type Tree struct { |
1 | import ( |
- 利用 chan 的队列性质很有意思,无需维护额外的数据结构.
- 配合 go 协程,原本联系紧密的 读取 -> 验证过程 被拆开在 3 个协程中.
- 合理的并发会降低代码复杂度.
练习: Web 爬虫
- 在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。
- 修改
Crawl
函数来并行地抓取 URL,并且保证不重复。 - 提示:可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!