专做写字楼出租的网站,网站域名实名制,山东电商网站建设,黄山网站建设哪家好前言
大家好#xff0c;这里是白泽#xff0c;我是21年8月接触的 Go 语言#xff0c;学习 Go 也正好两年半#xff0c;我决定重启我之前未完成的计划#xff0c;继续阅读《The Go Programing Language》#xff0c;一年多前我更新至第五章讲解的时候#xff0c;工作的忙…前言
大家好这里是白泽我是21年8月接触的 Go 语言学习 Go 也正好两年半我决定重启我之前未完成的计划继续阅读《The Go Programing Language》一年多前我更新至第五章讲解的时候工作的忙碌和 GPT 的出现让我搁置了这个计划。现在补上六至八章的讲解并整理成一个合集方便大家阅读。
我也开源了一个 Go 语言的学习仓库有需要的同学可以关注其中将整理往期精彩文章、以及 Go 相关电子书等资料。
仓库地址github.com/BaiZe1998/g…
本文的内容就是针对《The Go Programing Language》第一至八章的整理以及本人的领读涵盖 Go 语言的核心知识。
区别于连篇累牍我希望这份笔记是详略得当的可能更适合一些对Go有着一些使用经验但是由于是转语言或者速食主义者对 Go 的许多知识点并未理解深刻笔记中虽然会带有一些个人的色彩但是 Go 语言的重点我将悉数讲解。
再啰嗦一句笔记中讲述一个知识点的时候有时并非完全讲透或是浅尝辄止或是抛出疑问却未曾解答。希望你可以接受这种风格而有些知识点后续涉及到后续章节当前未过分剖析也会在后面进行更深入的讲解。
最后如果遇到错误或者你认为值得改进的地方也很欢迎你评论或者联系我进行更正又或者你也可以直接在仓库中提issue或者pr或许这也是小小的一次“开源”。 感兴趣可以关注公众号 「白泽talk」白泽目前也打算打造一个氛围良好的行业交流群文章的更新也会提前预告欢迎加入622383022。 一、综述
1.1 Hello Word
介绍包管理编译依赖运行代码的流程无需分号结尾以及严格的自动格式化
1.2 命令行参数
参数初始化获取命令行参数的方式给出了一个低效的循环获取命令行参数的代码在此基础上进行优化
关于字符串常量的累加是否是不断创建新值变量创建后如何存储结合Java堆栈
1.3 查找重复行
strings.join底层发生了什么
map乱序的原因
os.stdin Scan的终止条件
输出错误内容到标准错误
何时可以跳过error检查
1.4 GIF 动画
可以生成gif格式的图片
1.5 获取一个URL
resp.Body.Close()可以avoid leaking resources具体发生了什么
io.Copy(dst, src)与ioutil.ReadAll的工作模式区别
1.6 并发获取多个URL
当多个goroutine同时对一个channel进行输入输出的时候会发生阻塞
1.7 实现一个 Web 服务器
fmt.Fprintf(dir, src)可以将内容输出到指定输出web的response、标准错误因为dir实现了接口io.Writer
启动服务程序的时候maclinux为什么末尾要加
服务端handler路由匹配到前缀则可以触发并且开启不同goroutine处理request那么上限是多少高访问量会发生什么
1.8 杂项
switch在满足case之后不会继续下沉且default可以放置在任何位置
switch也可以以tarless的模式书写
goto语法不常用但是go也提供了
func也可以作为一种类型
结构、指针、方法、接口、包、注释
二、程序结构
2.1 名字
包名通常小写字母命名
通常来说对于作用域较短的变量名Go推荐短命名如i而不是index而对于全局变量则倾向于更长更凸显意义的命名
驼峰而非下划线命名
2.2 声明
注意全局变量的作用域最小也是整个包的所有文件大写则可以跨包
2.3 变量
引用类型slice、pointer、map、channel、function
可以同时初始化多种类型的变量并且Go没有未初始化的变量
var x float64 100 // 此时不使用短变量命名: 是声明而 是赋值
巧妙如果:左侧部分变量已经声明过作用域相同则只会对其进行赋值而只声明赋值未声明过的变量且左侧必须至少有一个未声明才能用:且declarations outer block are ignored
x : 1
p : x
*p 2 // 则 x 1
var x, y int
x x, x y, x nil // true false falseGo的flag包可以实现获取命令行参数的功能-help的来源
p : new(int) // p是int类型的指针或者某个类型的引用此时*p 0
*p 2 // new 并不常用垃圾回收一个变量如果不可达unreachable则会被回收
关于变量的生命周期全局变量在程序运行周期内一直存在而局部变量则会在unreachable时会被回收其生命周期从变量的声明开始到unreachable时结束
栈内存栈内存由编译器自动分配和释放开发者无法控制。栈内存一般存储函数中的局部变量、参数等函数创建的时候这些内存会被自动创建函数返回的时候这些内存会被自动释放栈可用于内存分配栈的分配和回收速度非常快
堆内存只要有对变量的引用变量就会存在而它存储的位置与语言的语义无关。如果可能变量会被分配到其函数的栈但如果编译器无法证明函数返回之后变量是否仍然被引用就必须在堆上分配该变量采用垃圾回收机制进行管理从而避免指针悬空。此外局部变量如果非常大也会存在堆上。
在编译器中如果变量具有地址就作为堆分配的候选但如果逃逸分析可以确定其生存周期不会超过函数返回就会分配在栈上。
总之分配在堆还是栈完全由编译器确定。而原本看起来应该分配在栈上的变量如果其生命周期获得了延长被分配在了堆上就说它发生了逃逸。编译器会自动地去判断变量的生命周期是否获得了延长整个判断的过程就叫逃逸分析。
/*
此时x虽然是局部变量但是被分配在堆内存在f()调用结束后依旧可以通过global获取x的内容我们称x从f当中escape了
逃逸并非是一件不好的事情但是需要注意对于那些需要被回收的短生命周期的变量不要在编程当中被长生命周期的变量全局变量引用否则会很大程度上影响Go的垃圾回收能力造成内存分配压力
*/
var global *int
func f() {var x intx 1global x
}
// 此时*y没有从g()当中escape因此是分配在栈内存当中调用结束变成unreachable需要被回收
fun g() {y : new(int)*y 1
}2.4 赋值
x, y y, x
a[i], a[j] a[j], a[i]
// 计算斐波那契数列赋值右侧的表达式会按照旧值先计算后赋值给左侧变量
func fib(n int) int {x, y : 0, 1for i : 0; i n; i {x, y y, xy}
}2.5 类型声明
type IntA int
type IntB int
var (x IntA 1 // 此时x和y是不同类型因此无法比较与一起运算y IntB 2
)T(x)将x转成T类型转换操作可以执行的前提是x和T在底层是相同的类型或者二者是未命名的指针类型底层指向相同的类型
这样的转换虽然转化了值的类型但是并没有改变其代表的值
当然数值类型的变量之间也允许这种转换损失精度或者将string转换成[]byte的切片等当然这些转化方式将改变值的内容
2.6 包和文件
包中.go文件的初始化流程
如果package p内部import了q则会先初始化package qmain package最后初始化可以确保main func在执行时所有的package已经完成初始化
2.7 作用域
变量的scope作用域是处于compile-time编译时的特征
变量的lifetime生命周期是处于run-time运行时的特征
if x : f(); x 0 {fmt.Println(x, y)
} else if y : g(x); x y {fmt.Println(x, y)
} else {fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here变量作用域的测试如下
func test() (int, error) {return 1, nil
}
func main() {x : 0
for i : 1; i 5; i {x : ifmt.Println(x, x)}fmt.Println(x, x) // 此时x依旧是0说明for内部的x是重新声明的x, err : test() // 此时x和err通过:声明赋值但是结合2.3节的内容此时x已经声明所以只对其进行赋值为1但是地址不变fmt.Println(x, x, err) // 此处打印的x 1时的地址与赋值前x 0地址相同
}
// 结果
1 0x1400012a010
2 0x1400012a030
3 0x1400012a038
4 0x1400012a040
5 0x1400012a048
0 0x1400012a008
1 0x1400012a008 nil三、基本数据类型
3.1 整数
负数的%运算
^位运算符and notx ^ y zy中1的位则z中对应为0否则z中对应为x中的位
00100010 ^ 00000110 00100000无符号整数通常不会用于只为了存放非负整数变量只有当涉及到位运算、特殊的算数运算、hash等需要利用无符号特性的场景下才会去选择使用
比如数组下标i用int存放而不是uint因为i–使得i -1时作为判断遍历结束的标志如果是uint则0减1则等于2^64-1而不是-1无法结束遍历
注意int的范围随着当前机器决定是32位还是64位
var x int32 1
var y int16 2
var z int x y // complie error
var z int int(x) int(y) // ok
// 大多数数值型的类型转换不会改变值的内容只会改变其类型编译器解释这个变量的方式但是当整数和浮点数以及大范围类型与小范围类型转换时可能会丢失精度或者出现意外的结果3.2 浮点数
math.MaxFloat32
math.MinFloat32
const x 6.2222334e30 // 科学计数法
// math包中有很多的使用浮点数的函数并且fmt包有很多适用于浮点数的格式化输出包括保留小数点的具体精度等float32精度大概6位
float64精度大概15位更常用因为单精度计算损失太快
// 直接用浮点数为返回值结果再二次用于其他的比较判断返回结果是否有效有时会有误差导致错误推荐额外增加一个bool参数
func compute() (value float64, ok bool) {if failed {return 0, false}return result, true
}3.3 复数
var x complex128 complex(1, 2) // 12i
var y complex128 complex(3, 4) // 34i
fmt.Println(x*y) // -510i
fmt.Println(real(x*y)) // -5
fmt.Println(imag(x*y)) // 10
// 两个复数相等当且仅当实部和虚部相当
fmt.Println(cmplx.Sqrt(-1)) // 01i3.4 布尔量
bool是if或者for的判断条件
s ! s[0] x // 当逻辑运算符左侧表达式可以决定操作结果则将放弃执行右侧表达式
// 的优先级高于||3.5 字符串
string在GO语言中是不可变的量
len获取的是字符串的字节数目而不是码点UTF-8 Unicode code point
字符串第i个字节并不一定是字符串的第i个字符因为UTF-8编码对于非ASCII的code point需要2个或更多字节
str : hello, world
fmt.Println(s[:5]) // hello
fmt.Println(s[7:]) // world
fmt.Println(s[:]) // hello world
s : left
t : s
s right // 此时s指向新创建的string ”left right“而t指向之前的s表示的“left”
a : x
b : x
a b // true且string可以按照字典序比较大小string是不可变的意味着同一个string的拷贝可以共享底层的内存使得拷贝变得很轻量比如s和s[:7]可以安全的共享相同的数据因此substring操作也很轻量。没有新的内存被分配。 反引号中的字符串表示其原生的意思内容可以多行定义不支持转义字符
func main() {a : hello worldlalalafmt.Println(a)
}Unicode
UTF-8使用码点描述字符Unicode code point在Go中对应术语runeGO中使用int32存储
可以使用一个int32的序列来代表rune序列固定长度带来了额外的开销因为大多数常用字符可以使用16bits描述
UTF-8
可变长编码使用1-4 bytes来代表一个rune1byte存储 ASCII 2 or 3 bytes 存储大多数常用字符 rune并且采用高位固定的方式来区分范围前缀编码无二义性编码更紧凑 s : Hello, 世界
fmt.Println(len(s)) // 13
fmt.Println(utf8.RuneCountInString(s)) // 9字符串和数组切片
字符串包含了一连串的字节byte创建后不可变。而[]byte内容是可变的
s : abc
b : []byte(s) // 分配新的字节数组内存
s2 : string(b) // 发生内存拷贝为了避免没有必要的转换和内存分配bytes包中提供了很多与string包中相同功能的方法更推荐使用共享内存
bytes.Buffer用于字符字符串的累加构造字符串操作很方便高效
// 一些操作Buffer的api
var buf bytes.Buffer
fmt.Fprintf(buf, %d, x)
buf.WriteString(abc)
buf.WriteByte(x)
buf.WriteRune(码点值)字符串和数值类型的转换
// 整数转string
x : 123
y : fmt.Sprintf(%d, x)
fmt.Println(y, strconv,Itoa(x)) // 123 123
// %b %d %u %x 用于进制转换
s : fmt.Sprintf(x%b, x) // x1111011
// string转整数
x, err : strconv.Atoi(123)
y, err : strconv.ParseInt(123, 10, 64)// base 10, up to 64 bits第三个参数表示转换的整型的范围为int64
// fmt.Scanf()可以用于读取混合数据整型、字符等3.6 常量
所有常量的底层都是由boolean、string、number组成在编译时确定不可变常量的运算结果依旧是常量
const a 2
const b 2*a // b 在编译时完成大多数常量的声明没有指定类型但是也可以指定没有类型的常量Go中称为无类型常量untyped constant具体的类型到使用到的时候确定
untyped constant
const a 10
fmt.Printf(%T\n, a) // int隐式类型
var b float64 4*a // 在需要的时候a转变成了float64
fmt.Printf(%T\n, b) // float64在默认情况下untyped constant 不是没有具体类型而是隐式转换成了如下类型因此上述a的类型可以打印为int
并且untyped constant拥有更高的精度可以认为至少有 256bit 的运算精度
untyped booleanuntyped integer 隐式转换成 intuntyped rune 隐式转换成 int32untyped floaing-point 隐式转换成 float64untyped complex (隐匿转换成 complex128)untyped string
常量生成器
可以参与计算且拥有增长属性
type Flags uint
const (a 1 iota // 1b // 2c // 4d // 8
)
const (_ 1 (10 * iota)KiB // 2^10MiB // 2^20GiB // 2^30TiB // PiB // EiB // ZiB // 2^70...
)四、复合类型
4.1 数组
长度不可变如果两个数组类型是相同的则可以进行比较且只有完全相等才会为true
a : [...]int{1, 2} // 数组的长度由内容长度确定
b : [2]int{1, 2}
c : [3]int{1, 2}4.2 切片
切片由三部分组成指针、长度len、容量cap
切片可以通过数组创建
// 创建月份数组
months : [...]string{1:January, 省略部分内容, 12: December}基于月份数组创建切片且不同切片底层可能共享一片数组空间 fmt.Println(summer[:20]) // panic: out of range
endlessSummer : summer[:5] // 如果未超过summer的cap则会扩展slice的len
fmt.Println(endlessSummer) // [June July August September October][]byte切片可以通过对字符串使用类似上述操作的方式获取
切片之间不可以使用进行比较只有当其判断是否为nil才可以使用
切片的zero value是nilnil切片底层没有分配数组nil切片的len和cap都为0但是非nil切片的len和cap也可以为0Go中len 0的切片处理方式基本相同
var s []int // len(s) 0, s nil
s nil // len(s) 0, s nil
s []int(nil) // len(s) 0, s nil
s []int{} // len(s) 0, s ! nilThe append Function
使用append为slice追加内容如果cap len则会触发slice扩容下面是一个帮助理解的例子使用了2倍扩容并非是Go内置的append处理流程那将会更加精细api也更加丰富 4.3 映射
map(hash table) — 无序集合key必须是可以比较的除了浮点数这不是一个好的选择
x : make(map[string]int)
y : map[string]int{alice: 12,tom: 34
}
z : map[string]int{}
// 内置函数
delete(y, alice)对map的元素进行取地址并不是一个好的注意因为map的扩容过程中可能伴随着rehash导致地址发生变化那么map的扩容规则
ages[carol] 21 // panic if assignment to entry in nil map
// 判断key-value是否存在的方式
age, ok : ages[alice]
if age, ok : ages[bob]; !ok {...
}4.4 结构体
type Point struct {x, y int
}
type Circle struct {center Pointradius int
}
type Wheel struct {circle Circlespokes int
}
w : Wheel{Circle{Point{8, 8}, 5}, 20}
w : Wheel{circle: Circle{center: Point{x: 8, y: 8},radius: 5,},spokes: 20,}4.5 JSON
// 将结构体转成存放json编码的byte切片
type Movie struct {Title stringYear int json:released // 重定义json属性名称Color bool json:color,omitempty // 如果是空值则转成json时忽略
}
data, err : json.Marshal(movie)
data2, err : json.MarshalIndent(movie, , )
// 输出结果
{Title:s,released:1,color:true}
{Title: s,released: 1,color: true
}
// json解码
content : Movie{}
json.Unmarshal(data, content)
fmt.Println(content)4.6 文本和HTML模板
略
五、方法
5.1 方法声明
// 可以提前声明返回值z
func add(x, y int) (z int) {z x-yreturn
}如果两个方法的参数列表和返回值列表相同则称之为拥有相同类型same type
参数是值拷贝但是如果传入的参数是slice、pointer、map、functionchannel虽然是值拷贝但是也是引用类型的值会对其指向的值做出相应变更
你可能会遇到查看某些go的内置func源码的时候它没有声明func的body部分例如append方法
// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
// slice append(slice, elem1, elem2)
// slice append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
// slice append([]byte(hello ), world...)
func append(slice []Type, elems ...Type) []Type事实上append在代码编译的时候被替换成runtime.growslice以及相关汇编指令了可以输出汇编代码查看细节你可以在go的runtime包中找到相关实现如下
// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slices length is set to the old slices length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slices length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {if raceenabled {callerpc : getcallerpc()racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, abi.FuncPCABIInternal(growslice))}if msanenabled {msanread(old.array, uintptr(old.len*int(et.size)))}if asanenabled {asanread(old.array, uintptr(old.len*int(et.size)))}// 省略...
}声明函数时指定返回值的名称可以在return时省略
func add(x, y int) (z int, err error) {data, err : deal(x, y)if err ! nil {return // 此时等价于return 0, nil}// 这里是赋值而不是声明因为在返回值列表中声明过了z xyreturn // 此时等价于return z, nil
}5.2 错误
error是一个接口因此可以自定义实现error
type error interface {Error() string
}如果一个函数执行失败时需要返回的行为很单一可以通过bool来控制
func test(a int) (y int, ok bool) {x, ok : test1(a)if !ok {return }y x*xreturn
}更多情况下函数处理时可能遇到多种类型的错误则使用error可以通过判断err是否为nil判断是否发生错误
func test(a int) (y int, err error) {x, err : test1(a)if err ! nil {return }y x*xreturn
}
// 打印错误的值
fmt.Println(err)
fmt.Printf(%v, err)Go通过if和return的机制手动返回错误使得错误的定位更加精确并且促使你更早的去处理这些错误而不是像其他语言一样选择抛出异常可能使得异常由于调用栈的深入导致最终处理不便
错误处理策略
一个func的调用返回了err则调用方有责任正确处理它下面介绍五种常见处理方式
传递
// 某func部分节选
resp, err : http,Get(url)
if err ! nil {// 将对Get返回的err处理交给当前func的调用方return nil, err
}fmt.Errorf()格式化添加更多描述信息并创建一个了新的error参考fmt.Sprintf的格式化 当error最终被处理的时候需要反映出其错误的调用链式关系
并且error的内容组织在一个项目中需要统一以便于后期借助工具统一分析
错误重试 优雅关闭
如果无法处理可以选择优雅关闭程序但是推荐将这步工作交给main包的程序而库函数则选择将error传递给其调用方。 使用log.Fatalf更加方便 会默认输出error的打印时间 选择将错误打印 或者输出到标准错误流 少数情况下可以选择忽略错误并且如果错误选择返回则正确情况下省略else保持代码整洁 EOFEnd of File
输入的时候没有更多内容则触发io.EOF并且这个error是提前定义好的 5.3 作为值的函数
函数是一种类型类型可以作为参数并且对应变量是“引用类型”其零值为nil相同类型可以赋值 函数作为参数的例子将一个引用类型的参数传递给多个func可以为这个参数多次赋值Hertz框架中使用了这种扩展性的思想 5.4 匿名函数
函数的显式声明需要在package层面但是在函数的内部也可以创建匿名函数 从上可以看出f存放着匿名函数的引用并且它是有状态的维护了一个递增的x
捕获迭代变量引发的问题
正确版本 错误版本 所有循环内创建的func捕获并共享了dir变量相当于引用类型所以创建后rmdirs切片内所有元素都有同一个dir而不是每个元素获得dir遍历时的中间状态
因此正确版本中dir : d的操作为遍历的dir申请了新的内存存放
func main() {arr : []int{1, 2, 3, 4, 5}temp : make([]func(), 0)for _, value : range arr {temp append(temp, func() {fmt.Println(value)})}for i : range temp {temp[i]()}
}
// 结果
5
5
5
5
5另一种错误版本i最终达到数组长度上界后结束循环并且导致dirs[i]发生越界 // 同样是越界的测试函数
func main() {arr : []int{1, 2, 3, 4, 5}temp : make([]func(), 0)for i : 0; i 5; i {temp append(temp, func() {fmt.Println(arr[i])})}for i : range temp {temp[i]()}
}
// 结果
panic: runtime error: index out of range [5] with length 5以上捕获迭代变量引发的问题容易出现在延迟了func执行的情况下先完成循环创建func、后执行func
5.5 变参函数 vals此时是一个int类型的切片下面是不同的调用方式 虽然…int参数的作用与[]int很相似但是其类型还是不同的变参函数经常用于字符串的格式化printf 测试
func test(arr ...int) int {arr[0] 5sum : 0for i : 0; i len(arr); i {sum arr[i]}return sum
}
func main() {arr : []int{1, 2, 3, 4, 5}fmt.Println(test(arr...))fmt.Println(arr)
}
// 切片确实被修改了
19
[5 2 3 4 5]5.6 延后函数调用
defer通常用于资源的释放对应于opencloseconnectdisconnectlockunlock
defer最佳实践是在资源申请的位置紧跟使用defer在当前函数return之前触发如果有多个defer声明则后进先出顺序触发 defer也可以用于调试复杂的函数通过return一个func的形式 测试1
func test() func() {fmt.Println(start)defer func() {fmt.Println(test-defer)}()return func() {fmt.Println(end)}
}
func main() {defer test()()fmt.Println(middle)
}
// 输出
start
test-defer
middle
end可以观察到test()()分为两步执行start在defer声明处打印end在main函数return前打印并且test内定义的defer也在test函数return前打印test-defer
此时start和end包围了main函数因此可以用这种方式调试一些复杂函数如统计执行时间
测试2
func test() func() {fmt.Println(start)defer func() {fmt.Println(test-defer)}()return func() {fmt.Println(end)}
}
func main() {defer test()fmt.Println(middle)
}
// 输出
middle
start
test-defer此时将test()()改为test()则未触发test打印end并且先执行了打印middle
另一个特性defer可以修改return返回值 此时double(x)的结果先计算出来后经过了defer内result x的赋值最后得到12
此外因为defer一般涉及到资源回收那么如果有循环形式的资源申请需要在循环内defer否则可能出现遗漏
5.7 panic崩溃
Go的编译器已经在编译时检测了许多错误如果Go在运行时触发如越界、空指针引用等问题会触发panic崩溃
panic也可以手动声明触发条件 发生panic时defer所定义的函数会触发逆序程序会在控制台打印panic的日志并且打印出panic发生时的函数调用栈用于定位错误出现的位置
func test() {fmt.Println(start)
}
func main() {defer test()panic(panic)
}
// 结果
start
panic: panicpanic不要随意使用虽然预检查是一个好的习惯但是大多数情况下你无法预估runtime时错误触发的原因 手动触发panic发生在一些重大的error出现时当然如果发生程序的崩溃应该优雅释放资源如文件io
关于panic发生时defer的逆序触发如下 5.8 recover恢复
panic发生时可以通过recover关键字进行接收有点像异常2捕获可以做一些资源释放或者错误报告工作因此可以优雅关闭系统而不是直接崩溃 如果recover()在defer中被调用则当前函数运行发生panic会触发defer中的recover()并且返回的是panic的相关信息否则在其他时刻调用recover()将返回nil没有发挥recover()作用
上图中的案例recover()接受到panic后选择打印panic内容将其看作是一个错误而不选择停止程序运行因此也就有了“恢复”的含义
但是recover()不能无端使用因为panic的发生只报告错误放任程序继续执行往往会使得程序后续的运行出现不可预计的问题即使是使用recover也只关注当前方法内的panic而不要去考虑处理其他包的方法调用可能产生的panic因为这更难把握程序运行的安全性
因此只有少数情况下使用recover并且确实是有这个需求否则还是建议触发panic的行为 六、方法
6.1 方法声明
Go的面向对象与传统意义上的不同
传统的方法声明和为Point结构声明方法 声明方法接收者的名称p通常可以选择对应类型的第一个小写字母声明方法接收者与声明参数列表很相似
两种方法调用方式如下 注意p的参数和方法不能同名否则编译时将出错但是不同类型的方法接收者可以拥有同名方法如下 Path是一个Point类型的切片而不是一个Point类型的结构
并且Go与其他面向对象语言不同它可以各种类型的变量数值型、string、slice、map、甚至function因为其也是一种类型声明方法为其添加额外的行为。
除了这个类型变量的底层不是一个指针point或者接口interface
type MyX *int
// 非法
func (m MyX) test(x int, y int) {fmt.Println(x, y)
}Go语言的方法没有重载功能同一个类型的方法不能出现同名及时参数列表不同 // 下面是一个错误的例子
type MyX int
func (m MyX) test(x int, y int) {fmt.Println(x, y)
}
func (m MyX) test(x int) {fmt.Println(x)
}6.2 带有指针接收器的方法 可以借助指针的特性实现对变量携带属性的修改 注意⚠️
如果声明的是带有指针接收器的方法但是创建的变量不是指针类型则方法调用时会将变量隐式转换成指针类型 如果声明的是普通类型接收器的方法但是创建的变量是指针类型则方法调用时会将变量隐式转换成原本的非指针类型 并且如果一个实例对应类型的方法都不是通过指针接收器声明的则对该实例进行copy操作则是安全的否则通过指针接收器调用的方法可能会涉及实例内部的状态向外泄露
nil作为有效的接收者 一个有趣的测试
type Test struct {x, y int
}
func (t *Test) Sum() int {if t nil {return 100}return t.x t.y
}
func main() {x : Test{1, 2}fmt.Println(x.Sum())x nilfmt.Println(x.Sum())
}
// 输出
3
100因此当x为nil的时候其函数调用触发行为是可以自定义的
6.3 嵌套结构体组成新类型
嵌套结构可以赋值可以简化相当于组合成的struct拥有内部struct的属性 组合成的struct拥有内部struct的方法因此一个需要拥有多个方法的结构可以通过拆解成多个结构各自拥有部分方法的组合 注意Go的结构的嵌套是“has a”的关系而不是“is a”的关系因此寻常语言的上下转型不适用
一个结构可以内嵌匿名属性 此时ColoredPoint拥有拥有自己声明的方法以及内嵌结构的所有方法如果出现相同名称的方法则外层拥有更高的优先级同名属性同理如果出现同层级相同优先级则会报错
./main.go:35:16: ambiguous selector xxx结构嵌套小试牛刀 6.4 方法调用的两种形式
寻常实例化后调用方法 先获取某个结构体的方法表达式传入需要触发方法的结构实例作为第一个参数方法的参数列表对应剩余参数 type A struct {y int
}
func (a *A) Sum(x int) {fmt.Println(a.y, x)
}
func main() {fun : (*A).Suma : A{1}fun(a, 2)
}使用场景 6.5 封装
Go语言只有一种控制变量或者方法的访问性的规则就是首字母大小写 这种写法则跨package的时候words字段不能被其他包内的实例访问一定程度上确保了slice的安全性
type A struct {y int
}
// 此时的sum也不能被跨包访问
func (a *A) sum(x int) {fmt.Println(a.y, x)
}七、接口
7.1 作为规约的接口 io.Writer也是一个接口所谓规约就是约束了行为但是不制定细节实现方式只要它实现了Write方法 比如可以自己定义一个结构并声明一个Write方法 另一个很重要的接口 7.2 接口类型 接口的嵌套 7.3 接口约定的达成
Go语言中使用“is a”可以表达一个type实现了某个接口声明的所有方法“has a”是用于结构的嵌套 赋值f右侧可以多于左侧只要覆盖左侧接口声明即可 之前提到过Go为一个type声明方法的时候可以使用指针接收器也可以不使用但是在方法调用的时候即使类型与声明时不对应也会隐式完成转换
⚠️但是要注意的是对于使用指针接收器声明的方法非指针接收器类型本身是不拥有这些方法的但是非指针接收器声明的方法指针类型是拥有声明的方法的
type IntSet struct {
}
func (*IntSet) String() string {fmt.Println(test)return test
}
func main() {t : IntSet{}var _ t.String() // okvar _ (t).String() // okvar _ fmt.Stringer t // okvar _ fmt.Stringer t // Cannot use t (type IntSet) as the type fmt.Stringer Type does not implement fmt.Stringer as the String method has a pointer receiver
}反之合法
type IntSet struct {
}
func (IntSet) String() string {fmt.Println(test)return test
}
func main() {t : IntSet{}var _ t.String() // okvar _ (t).String() // okvar _ fmt.Stringer t // okvar _ fmt.Stringer t // ok
}空接口没有声明任何方法因此可以接收任何类型的变量 但是因为interface{}没有任何方法当一个值赋值给它之后就失去了其原本的特性因此需要有一个机制去从interface{}中重新提取出原来的变量。断言机制后续7.10会提及
7.4 作为值的接口
一个interface的状态可以由type和value修饰称其为动态的type和动态的value这个type的概念并不是通常意义下的类型概念因为Type是一个静态的概念是编译时的特性
其type表示赋值给这个接口的变量对应的type
其value是一个指针指向赋值给这个接口的变量对应的value
对下面这四行代码进行分析 对于一个接口类型来说 一个接口的零值状态如下没有变量为其赋值这里的type也是一种“值”用于描述这个接口的特性 一个接口是否为nil是根据其动态类型typevalue决定的因此上述w是一个nil接口w nil true 第二行代码赋值后value是一个指针 第三行代码赋值后 第四行代码赋值后 接口是一个可以比较的类型两个接口类型都为nil时相等
或者其动态的type和value都相等要求可以比较如果是slice等不可比较的类型则会panic 因此interface可以作为hash的key也可以作为switch的操作数
打印接口的动态type 当interface的value为nil
当一个接口的变量的动态value指向nil因为它是一个指针则称这个变量为non-nil并且不等于nil因为其动态type不等于nil
func main() {var buf *bytes.Buffer// buf new(bytes.Buffer)// 将一个实现了Write方法的结构传递给io.Writer接口类型outf(buf)
}
func f(out io.Writer) {if out ! nil {out.Write([]byte(done))}
}
// 报错
panic: runtime error: invalid memory address or nil pointer dereference原因此时out是一个non-nil接口其动态的value指向nil但是其动态的type为*bytes.Buffer因此out ! nil确实为true 下面三节讲解interface如何应用在排序、web服务、错误处理当中
7.5 使用sort.Interface排序
一些常用的sort方法
func Ints(x []int) { Sort(IntSlice(x)) }
// 本质上IntSlice实现了sort.Interface定义的三个接口
type IntSlice []int
func (x IntSlice) Len() int { return len(x) }
func (x IntSlice) Less(i, j int) bool { return x[i] x[j] }
func (x IntSlice) Swap(i, j int) { x[i], x[j] x[j], x[i] }
// 下面两个用法等价
sort.Sort(sort.IntSlice(arr))
sort.Ints(arr)7.6 错误接口
error本质上也是实现了一个接口 并且返回的是一个结构的指针 fmt.Errorf()更加方便错误的格式化创建 7.7 类型断言
在前面我们说了一个接口底层有两个动态的值一个是type一个是指向value的指针
通过x.(T)的方式从接口实例x中提取出x接口实例底层动态的value底层type类型不对应T则panic断言操作之前一般有着接口赋值操作 如果T也是一个接口类型则断言机制会判断x的底层type是否满足这个T接口并且当成功时断言的结果依旧是一个接口只是底层type的值变成了T 通常通过断言是否成功来取值 并且可以选择复用原来的变量名称 7.8 使用类型断言分辨错误
定义一个封装的PathError 使用如下 os包提供了三个方法用于分辨Error 简易用法如下 这个机制的作用在于可以自定义一些错误并且在发生错误时动态判断错误的类型从而作出相应处理
7.9 使用接口类型断言查询行为
可以通过接口类型的断言将一个接口实例更具体化为携带某个功能的接口实例 7.10 按类型的程序分支
使用interface的两个场景
用接口去定义行为methods用接口去接收各个实现某个行为methods的类型实例配合断言进行动态处理
类型断言配合else-if 类型断言配合switch 关于变量的复用以x为例 switch块内部的x与sqlQuote方法的参数x并不会发生冲突
7.11 使用interface的建议
不要为每个单独的实例去事先定义一个接口接口的作用是抽象二个或更多
不一定要强行满足面向对象特性编码有时独立的function和变量也是好的选择
八、goroutine和信道
8.1 goroutine
程序入口main方法称为main goroutine当main方法结束return时所有的子goroutine终止
携程与线程的行为很像但是具体差别是由Go的语言层面的实现决定的这也是Go能支持更高并发性能的原因所在后续将进行更多讲解
8.2 示例并发时钟服务器 ListenIP地址和Port的监听
Accept阻塞直到有客户端的请求连接
Conn负责维护一个与客户端的连接循环并且在绝大多数情况下会由于client断开连接导致server端循环终止
此时时钟服务器是顺序提供服务的如果有多个客户端同时请求服务则同一时间只能有一个连接建立
使用go关键字开启goroutine则可以实现同时为多个客户端提供时间打印服务 8.3 channel 信道
作为goroutine之间的通信机制用于携程之间传递各种类型的value是引用类型的但channel可以用比较
比如创建一个用于收发int类型的信道 channel主要有两种操作方式send和receive一个goroutine为其存入value另一个取出value从而实现了跨goroutine通信的功能 channel的第三种操作方式close(ch)
对于关闭后的channel不能在往里面放入value会panic但是如果去reveive则会将其取出直到没有值在信道当中则不断显示该type对应的零值
channel有两种创建方式 unbuffered channel
对于无缓冲的信道sent和receive是相互阻塞的如果receive时信道中没有数据则会阻塞直到sent完成相反如果信道中已经有数据则sent会被阻塞直到receive完成
此时sent和receive操作是同步的通过这个机制可以实现控制程序执行的功能 此时main goroutine不会终止直到子goroutine执行完成为done存入值有时值是重要的有时存入值这个行为是重要的上图是后者。
判断channel是否关闭
如果是x : -natural的写法则natural在close之后x会因为外循环for的原因不断取出natural的type的对应0值 可以通过range的方式遍历channel并且在channel关闭时结束遍历并且channel的close只有当其确实有必要通知下游的接受者这个关闭消息才使用因为Go的垃圾回收机制会判断当一个变量不可达之后对其采取回收这与文件操作需要手动close不同 单向channel
双向channel可以赋值给单向channel隐式转换 带缓冲的channel 获取channel的缓冲长度 获取当前channel中缓冲的元素个数 总的来说无缓冲的channel倾向于实现同步的通信有缓冲的通信倾向于实现异步的通信
8.5 并行循环 下面是一个goroutine泄漏的例子当有一个非nil的error发生时程序return error但是与此同时相当数量的go协程因为errors信道里有信息而无法存入导致阻塞因为消费信道信息的函数已经return了
可能会导致程序的阻塞或者内存溢出 8.6 select多路复用 default是可选的如果没有可以执行的case则select会一直阻塞
如果有多个case同时可以执行则select随机选择一个 小结
以上就是《The Go Programing Language》一至八章的讲解希望能对你有所帮助。 感兴趣可以关注公众号 「白泽talk」白泽目前也打算打造一个氛围良好的行业交流群文章的更新也会提前预告欢迎加入622383022。