Golang学习笔记一
[TOC]
类型
- 类型决定变量的内存存储格式和长度。
-
编译后机器码不适用变量名,直接使用内存地址访问目标数据。
- 简短赋值定义变量, 简短模式退化为赋值
a := 1
// 简短模式退化为赋值
a := 100
a, b := 200, 300 // a为赋值操作,b为新定义变量
//退化赋值,需要至少有一个新定义变量,且作用域相同
func test() {
x := 100
println(&x, x)
{
x, y := 200, 300 //此时x外面和内部的是不同的变量
println(&x, x, y)
}
}
- 退化赋值,可以重复使用err变量
f, err := os.Open("./test.txt")
...
buf := make([]byte, 1024)
n, err := f.Read(buf)
- 编译器会将未使用的局部变量当做错误处理,全局变量没问题
var a int //全局变量
func test() {
x := 1 //局部变量x未使用报错
}
- 专有名词命名通常会全部大写,如
escapeHTML
局部变量有限用短名
func test() {
var c int //c代替count
for i := 1; i < 10; i++ { //i代替index
c++
}
}
-
常量是编译期间可以确定的字符,字符串,数字,布尔值。未使用的常量不会引发编译错误。
-
常量如果不赋值,则与上一行非空常量右值相同
//常量如果不赋值,则与上一行非空常量右值相同
func constDemo1() {
const (
x uint = 12
y
s = "ass"
z
)
fmt.Printf("%T, %v\n", y, y) //uint, 12
fmt.Printf("%T, %v\n", z, z) //string, ass
}
- go没有enum类型,但是借助iota来实现自增常量来实现枚举
func iotaDemo() {
const (
x = iota
y
z
)
println(x, y, z)//0 1 2
}
func constDemo2() {
const (
_ = iota
KB = 1 << (10 * iota) //左移10相当于乘以2^10
MB
GB
)
println(KB, MB, GB) //1024 1048576 1073741824
}
- iota,iota是golang语言的常量计数器,只能在常量的表达式中使用。iota在const关键字出现时将被重置为0(const内部的第一行之前),const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。
//跳值
type AudioOutput int //实际使用中,使用自定义类型来定义枚举
const (
OutMute AudioOutput = iota // 0
OutMono // 1
OutStereo // 2
_
_
OutSurround // 5
)
//定义数量级
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
MB // 1 << (10*2)
GB // 1 << (10*3)
TB // 1 << (10*4)
)
const (
_,_ = iota, iota*10
a,b //1, 1*10
c,d //2, 2*10
)
const (
a = iota //0
b //1
c = 100 //100
d //100
e = iota //4 (恢复iota自增,计数包括c,d行)
f //5
)
-
常量与变量的区别:变量在运行期间分配内存(未优化时),而常量会被编译器在预处理阶段直接展开,当做指令使用。
-
别名,别名类型无需类型转换,可直接赋值
//byte alias for uint8
//rune alias for int32
func aliasDemo() {
var a byte = 0x11
var b uint8 = a
var c uint8 = a + b
fmt.Println(c)
}
-
引用类型特指slice、map、channel这三种的预定义类型。与数字,数组不同的是,引用类型,除了分配内存外,还必须初始化一些属性。 引用类型通常用make函数创建,make确保内存的分配以及属性的初始化。
-
类型转换,隐式类型转换造成的问题远大于其带来的好处。除了常量,别名及未命名类型外,GO强制要求使用显示类型转换。
a := 10
b := byte(a)
c := a + int(b)
- var, type, const, import多个同类型的可以合并
type
用于定义用户自定义类型- 自定义类型的底层基础类型相同,也属于完全不同的类型
//自定义类型的底层基础类型相同,也属于完全不同的类型
func customTypeDemo() {
type (
red int
blue int
)
var r red = 1
var b blue = 2
var c int = 3
fmt.Printf("%T, %v\n", r, r) // red, 1
fmt.Printf("%T, %v\n", b, b) // blue, 2
fmt.Printf("%T, %v\n", c, c) // int, 3
}
未命名类型
,类似int,bool,string等类型有明确的标识符。数组,切片,字典,通道等的类型还与具体存储的元素类型和存储的长度有关,即未命名类型。- 具有相同声明的未命名类型视为同一种类型。
- 具有相同基类型的指针
- 具有相同类型元素和长度的数组
- 具有相同类型元素的切片
- 有相同键值类型的字典
- 有相同键值类型元素及相同操作方向的通道
- 具有相同字段序列(字段名,字段类型,字段顺序,标签)的结构体
- 具有相同签名(参数,返回值列表,不包括参数名)的函数
- 具有相同方法集合的(方法,方法签名,不包括顺序)接口
- struct tag 也属于类型的组成部分, 函数的签名顺序也类型的组成部分
- 如何判断一个 channel 是否已经被关闭?在读取的时候使用多重返回值的方式
x, ok := <-ch
单向管道
,发送值的通道类型chan<- T
, 接收值的通道类型<-chan T
var ch = make(chan string, 3) //定义一个普通的(双向)通道
var sendCh chan <- string = ch //双向通道转为发送通道
var recvCh <- chan string = ch //双向通道转为接收通道
类型可比较 Comparable
可比较又可以分为两个小类:
- 可比较,包括相等(==),和不相等(!=)
- 可排序,包括大于(>),大于等于(>=),小于(>),小于等于(<=)
可排序的一定是可比较的,反之不成立,即可比较的不一定是可排序的,例如struct类型就是可比较的,但不可排序。
可排序的数据类型有三种:整型,浮点型,字符串
可比较的数据类型:整型,浮点型,字符串,布尔, 复数,指针,通道,接口,结构体,数组
不可比较的数据类型
:Slice, Map, 和Function
是否可比较通过反射方法Comparable判断。
可赋值
类型需要满足某种条件如实现接口方法,或者类型相同,或者底层类型(underlying types)相同。
通过反射包AssignableTo方法判断是否可赋值。
表达式
- 更简洁的语言设计,更多的功能通过扩展类库或其他非侵入的方式实现。更丰富的语言特征会提升学习门槛,降低一致性和可维护性。
- 位运算符
按位与 (都是1) 0101 & 0011 == 0001 按位或 (至少一个1) 0101 | 0011 == 0111 按位亦或 (只有一个1)0101^0011 == 0110 按位取反 ^0111 = 1000 按位清除 (bit clear) 0110 &^1011 == 0100 位左移 0001 « 3 == 1000 位右移 1010 » 2 == 0010
- 指针,会分配专门的内存空间,相当于专门保存内存地址的整型变量
- 若两个指针指向同一个地址或者都为nil,则它们相等
- 指针支持相等运算,不支持加减运算和类型转换。
- 复合类型变量限制
- 初始化必须含类型标签
- 做花括号必须在类型尾部,不能另起一行
- 多个成员之间用逗号隔开
- 允许多行,但每行必须逗号或者花括号结尾
- 复杂的条件组合可以重构为独立的函数,调用函数虽会略有性能损失,但让主流程更简洁。
- fallthrough:Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的一个case代码。
func fallthroughDemo() {
x := 5
switch x {
case 4:
fmt.Println("****")
case 5:
fmt.Println("$$$$$")
fallthrough//必须是case语块的最后一句
case 6, 7:
fmt.Println("######")
case 8:
fmt.Println("&&&&&&&&")
default:
fmt.Println("^^^^^^^^^^")
}
//输出
//$$$$$
//######
}
- 有时,switch语句可以用来代替if语句
- 循环语句只有for循环一种, 初始化语句只执行一次,条件判断会多次执行
for i := 0; i < 10; i++ {
}
for x < 10 {
x++
}
for {
break
}
- for…range 用作数据迭代,支持字符串,数组,切片,字典,通道类型,返回键值,索引数据。
func (n *Note1) forRangeDemo() {
str := "hello"
//字符串
for k, v := range str {
fmt.Println(k, string(v))
}
//数组
arr := []int {1,2,3,4}
for k,v := range arr {
fmt.Println(k, v)
}
//字典
m1 := map[string]interface{}{
"name":"Sword",
"id":123,
"height":175,
}
for k, v := range m1 {
fmt.Println(k, v)
}
//通道
var ch = make(chan int, 1)
var sig = make(chan bool, 1)
go func() {
for i := 1; i < 5; i++ {
ch <- i
}
close(ch)
sig <- true
}()
for v := range ch {
fmt.Println(v)
}
<- sig
}
- range 会复制目标数据,尤其是对数组,可用数组指针或切片。字符串,切片基本结构体很小?而字典,通道本身就是指针封装复制成本较小,无需专门优化。
//数组
data := [3]int{1,2,3}
//切片
data := []int{1,2,3}
//range 迭代会复制数据
func forRangeDemo2() {
data := [3]int{1,2,3}
for k, v := range data {
if k == 0 {
data[0] += 100
data[1] += 100
data[2] += 100
}
fmt.Println(v, data[k])
}
dd := [3]int{5,6,7}
for k, v := range dd[:] {
if k == 0 {
dd[0] += 100
dd[1] += 100
dd[2] += 100
}
fmt.Println(v, dd[k])
}
//1 101
//2 102
//3 103
//5 105
//106 106
//107 107
}
- range的目标表达式是函数调用,也只会执行一次
func getData() []int {
fmt.Println("origin data")
return []int{1,2,3}
}
func test() {
for k, v := range getData() {
fmt.Println(k, v)
}
}
- 在一些性能优先的场景goto依然能发挥积极的作用,go的源代码中依然后很多goto的使用
- 数组转切片
a := [3]int{1,2,3} //数组
b := a[:] //数组转切片
函数
- go语言函数
- 无需前置声明
- 不支持命名嵌套定义
- 不支持同名函数重载
- 不支持默认参数
- 支持不定长变参
- 支持多返回值
- 支持命名返回值
- 支持匿名函数和闭包
- 函数是第一类对象,具有相同签名(参数及返回值列表)的视为同一种类型
- 第一类对象:指可在运行期间创建,可用作函数的参数和返回值,可存入变量的实体。例如匿名函数的使用。
- 命名函数更加易于维护。
- 函数只能判断是否为nil,不支持其他比较操作
参数
- 形参是函数中定义的参数,实参则是函数调用所传递的参数
- 函数调用都是传递参数的拷贝,无论是指针,引用类型或其他类型,区别是拷贝的目标对象还是指针
- 函数调用前,会为形参和返回值分配内存空间,并将实参拷贝到形参内存空间
- 表面上看指针参数性能更好一些,实际需具体分析。被复制的对象会延长目标对象的声明周期,还可能导致它被分配到堆上,那么性能消耗还得加上堆内存分配和垃圾回收的成本。在栈上复制小对象只需很少的指令即可完成,远比运行堆内存分配要快的多。
- 如果复制成本很高,自然使用指针传参更好。
- 如果函数的参数过多,建议将参数改为一个复合类型,如结构体。
- 将过多的参数独立成option struct, 便于扩展参数,也方便通过newOption函数设置默认参数配置。
type serverOption struct {
address string
port int
path string
timeout time.Duration
log *log.Logger
}
func newOption() *serverOption {
return &serverOption{
address: "0.0.0.0",
port: 8080,
path: "/var/www",
timeout: time.Second * 5,
log: nil,
}
}
func server(opt *serverOption) {
}
func run() {
opt := newOption()
opt.port = 88 //命名参数设置
server(opt)
}
- 变参,本质就是一个切片,可以接收一个或多个同类型的参数,把切片作为可变参数时,需要展开,若是数组需先转为切片
//a其实就是个切片
func paramsDemo(s string, a ...int) {
fmt.Printf("%T, %v\n", s, s) //string, hxx
fmt.Printf("%T, %v\n", a, a) //[]int, [1 2 3 111]
}
func demo5() {
paramsDemo("hxx", 1, 2, 3, 111)
a := [3]int{11,22,33} //数组
b := a[:] //数组转切片
paramsDemo("hxx", a[:]...)//把数组作为可变参数时需先转为切片,再展开
paramsDemo("hxx", b...)//把切片作为可变参数时,需要展开后面三个点...
}
- 有返回值的函数必须用return结束,除非有panic或者是无break的死循环无须return来终止
匿名函数
- 匿名函数,可在函数内部定义形成嵌套,可作为参数,返回值,赋值给变量,直接执行。编译器会为匿名函数生成随机符号名。
- 普通函数,匿名函数都可以作为结构体字段
//函数作为结构体字段
func testStruct() {
type calc struct {
mul func (x, y int) int
}
x := calc{
mul: func(x, y int) int {
return x * y
},
}
println(x.mul(10, 9)) //90
}
//函数存于通道中
func testChannel() {
c := make(chan func(x, y int) int, 2)
c <- func(x, y int) int {
return x + y
}
println((<-c)(1, 3)) //4
}
- 未使用的匿名函数会被编译器当做错误。
- 除了闭包,匿名函数也是常见的重构手段,可将大函数分解成多个独立的匿名函数,然后用简洁的调用实现逻辑流程,来分离主流程和细节。
- 匿名函数可起到作用域隔离,不会引发作用域污染。
- 闭包是词法上下文引用了自由变量的函数。闭包是函数和引用环境的组合体。
//闭包
func closureDemo1() []func() {
var s []func()
for i:=0; i < 3; i++ {
s = append(s, func() {
println(&i, i)
})
}
return s
}
func closureDemo2() []func() {
var s []func()
for i:=0; i < 3; i++ {
x := i
s = append(s, func() {
println(&x, x)
})
}
return s
}
func closureDemoMain() {
for _, f := range closureDemo1() {
f()
//输出
//0xc00001c9c8 3
//0xc00001c9c8 3
//0xc00001c9c8 3
}
for _, f := range closureDemo2() {
f()
//输出
//0xc00007c990 0
//0xc00007c998 1
//0xc00007c9a0 2
}
}
- 多个匿名函数引用同一个环境变量,修改也会变得复杂。
延迟调用
- 多个defer延迟调用,先进后出。编译器通过插入额外的指令来实现延迟调用,return和panic都会终止函数流程,引发defer语句执行。
- defer在函数结束时才会执行。不合理的使用defer更多的资源浪费,甚至错误。如循环处理多个日志文件,导致文件关闭延迟。
- defer延迟调用,相比直接CALL汇编指令调用函数,开销更大,需要注册,调用,额外的缓存开销。
- 通过go基准测试,对比延迟调用。通过对比,defer性能开销更大,在高性能要求场合需慎重使用
//go 性能测试基准测试
//基准测试的代码文件必须以_test.go结尾
//基准测试的函数必须以Benchmark开头,必须是可导出的
//基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
//基准测试函数不能有返回值
//最后的for循环很重要,被测试的代码要放到循环里
//b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能
import (
"sync"
"testing"
)
var m sync.Mutex
func call() {
m.Lock()
m.Unlock()
}
func deferCall() {
m.Lock()
defer m.Unlock()
}
func BenchmarkCall(b *testing.B) {
for i := 0; i < b.N; i++ {
call()
}
}
func BenchmarkCallDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
deferCall()
}
}
//运行 go test -bench=. -run=none
//输出
//BenchmarkCall-8 92373063 13.6 ns/op
//BenchmarkCallDefer-8 32450937 37.4 ns/op 每次操作需要话费37.4纳秒
//go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out
- 常用flag
-bench regexp:性能测试,支持表达式对测试函数进行筛选。-bench .则是对所有的benchmark函数测试 -benchmem:性能测试的时候显示测试函数的内存分配的统计信息 -count n:运行测试和性能多少此,默认一次 -run regexp:只运行特定的测试函数, 比如-run ABC只测试函数名中包含ABC的测试函数 -timeout t:测试时间如果超过t, panic,默认10分钟 -v:显示测试的详细信息,也会把Log、Logf方法的日志显示出来
错误处理
- 内置error接口
type error interface {
Error() string
}
- 按照惯例,错误总是最后一个返回参数。
- 应该通过错误变量来判断错误类型,而非文本内容来判断错误类别
- errors.New()与fmt.Errorf()都可以返回一个错误对象。
func div(x, y int) (int, error) {
if y == 0 {
return 0, errors.New("division by zero")//创建包含错误文本的error对象
}
return x / y, nil
}
//自定义错误类型
type DivError struct {
x, y int
}
func (DivError) Error() string {
return "division by zero"
}
func division(x, y int) (int, error) {
if y == 0 {
return 0, DivError{x, y}
}
return x / y, nil
}
func errorDemo1() {
z, err := division(5, 0)
if err != nil {
switch e := err.(type) {
case DivError:
fmt.Println(e, e.x, e.y) //division by zero 5 0
default:
fmt.Println(e)
}
log.Fatalln(err)
}
println(z)
}
- panic ,revocer更接近try/catc。
- panic会立刻终止正常的函数流程,执行defer语句,在延迟函数中,recover可以捕获panic提交的错误对象。
- 有时,过多的错误处理代码会让代码变得难看。
func recoverDemo() {
defer func() {
if err := recover(); err != nil {
log.Fatalln(err)
}
}()
println("hi")
panic("panic")
println("hello")
}
- panic 函数参数是空接口,可以接收任何类型作为错误状态。
- 中断性错误沿着堆栈往外传递,要么被外层捕获,要么导致进程崩溃。
- 连续调用panic只有最后一个会被recover捕获。
- recover必须在defer语句语句中才能正常工作。
- 调试阶段可用debug.PrintStack()输出详细的堆栈信息。
defer func() {
if err := recover(); err != nil {
debug.PrintStack()
}
}()
- 除非是不可恢复,导致系统无法运行的错误,否则不建议使用panic。如文件无权限,端口被占用等。
字符串
- 字符串是不可变字节序,复合结构,头部指针指向字节数组,但没有NULL结尾,默认以utf8存储Unicode字符。字面量里允许使用十六进制,八进制,utf编码格式。
- 字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符 串的字节的长度。字符串其实是一个结构体,因此字符串的赋值操作也就 是 reflect.StringHeader 结构体的复制过程,并不会涉及底层字节数组的复 制。
type StringHeader struct {
Data uintptr
Len int
}
- 字符串默认是“”而不是nil。
func demo1() {
//做转义处理,不支持夸行
s1 := "汉阳造,三八大盖,\"歪把子"
//不做转义,支持跨行
s2 := `
手榴弹,迫击炮,\"92式
汉阳造,三八大盖,\"歪把子
`
fmt.Println(s1)
fmt.Println(s2)
}
- 支持通过字节数组下表访问,但是不支持获取元素地址。
func (s *Str) demo2() {
s1 := "abcd"
fmt.Println(string(s1[0]))
fmt.Println(string(s1[2]))
fmt.Println(&s1[2])//不支持获取地址
}
//遍历
func (s *Str) demo3() {
s1 := "abcdef众志成城"
//byte方式
for i := 0; i < len(s1); i++ {
fmt.Printf(" %c", s1[i])
}
fmt.Println()
//通过range,rune方式,返回unicode字符
for _, v := range s1 {
fmt.Printf(" %c", v) // a b c d e f 众 志 成 城
}
}
- 修改字符串需要将其转换为可变类型如[]rune或[]byte,修改完再转换回来。但是无论如何,修改字符串都涉及内存重新分配的数据复制。
- 利用[]byte与string头结构部分相同,以非安全的指针类型转换来实现类型变更,从而避免底层数组复制。在高并发下可以有效改善性能,很多web framework 都有此类做法。但使用unsafe存在一定风险。
//有时类型转换会拖累性能,可尝试非安全操作,方法进行改善。
func (str Str) toString(bs []byte) string {
return *(*string)(unsafe.Pointer(&bs))
}
go语言unsafe包。unsafe 库让 golang 可以像C语言一样操作计算机内存,但这并不是golang推荐使用的,就像它的名字所表达的一样,它绕过了golang的内存安全原则,是不安全的,非必要场景,不建议使用。比如对性能有较高要求。
Golang指针
- *类型:普通指针,用于传递对象地址,不能进行指针运算。
- unsafe.Pointer:通用指针类型,用于转换不同类型的指针,不能进行指针运算。
- uintptr:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象。uintptr 类型的目标会被回收。
unsafe.Pointer 可以和 普通指针 进行相互转换。unsafe.Pointer 可以和 uintptr 进行相互转换。也就是说 unsafe.Pointer 是桥梁,可以让任意类型的指针实现相互转换,也可以将任意类型的指针转换为 uintptr 进行指针运算。
uintptr这个类型,在golang中,字节长度也是与int一致。通常Pointer不能参与运算,比如你要在某个指针地址上加上一个偏移量,Pointer是不能做这个运算的,那么谁可以呢?就是uintptr类型了,只要将Pointer类型转换成uintptr类型,做完加减法后,转换成Pointer,通过*操作,取值,修改值,随意。 参考 https://www.jianshu.com/p/c85fc3e31249
- 通过append函数可以将字符串追加到byte数组中
var a []byte
a = append(a, "abc"...) //后面对三个点表示展开切片
- 字符串类型转换容易造成性能问题。
- 动态构建字符串也容易造成性能问题。如用加法构建字符串,每次都需重新分配存储。若如此构建很大的字符串,性能会很差。
- golang的拼接字符串方法,strings.Join()比”+”效率更高。strings.Join()会统计所有参数的长度,一次性分配内存。
go字符串拼接
https://hermanschaaf.com/efficient-string-concatenation-in-go/ https://segmentfault.com/a/1190000012978989
- 直接“+”连接,每次运算都会产生一个新的字符串,重复内存分配,给 gc 带来额外的负担,所以性能比较差。
- 使用
fmt.Sprintf()
,内部使用 []byte 实现,不像直接运算符这种会产生很多临时的字符串,但是内部的逻辑比较复杂,有很多额外的判断,还用到了 interface,所以性能也不是很好。 strings.Join()
,join会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,这种方式效率相对较高。在已有字符串数组的场合,使用 strings.Join() 能有比较好的性能。buffer.WriteString()
,相对理想一些,可以当成可变字符使用,对内存的增长也有优化。在高性能场合推荐使用这种。
func (str Str) demo6() {
a := "abc"
b := "123"
c := strings.Join([]string{a, b}, "-")
fmt.Println(c) //abc-123
}
//buffer.WriteString()
func (str Str) demo7() {
a := "新型冠状病毒"
b := "SARS"
s := bytes.Buffer{}
s.WriteString(a)
s.WriteString(b)
ret := s.String()
fmt.Println(ret)//新型冠状病毒SARS
}
- 字符串拼接性能对比
//字符串连接性能测试
func bufferWrite() bytes.Buffer {
var b bytes.Buffer
b.Grow(1000)//先准备足够内存,防止中途内存扩张
for i := 0; i < 1000; i++ {
b.WriteString("a")
}
return b
}
func BenchmarkBufferWrite(b *testing.B) {
for i := 0; i < b.N; i++ {
bufferWrite()
}
// 200000 6174 ns/op
}
func strJoin() string {
s := make([]string, 1000)
for i:= 0; i < 1000; i++ {
s[i] = "a"
}
return strings.Join(s, "")
}
func BenchmarkStrJoin(b *testing.B) {
for i:= 0; i < b.N; i++ {
strJoin()
}
//200000 9485 ns/op
}
func strJoin2() string {
s := ""
for i := 0; i < 1000; i++ {
s += "a"
}
return s
}
func BenchmarkStrJoin2(b *testing.B) {
for i:= 0; i < b.N; i++ {
strJoin2()
}
//10000 106724 ns/op
}
//运行 go test -bench=. -run=none
//BenchmarkBufferWrite-4 200000 6173 ns/op
//BenchmarkStrJoin-4 200000 9416 ns/op
//BenchmarkStrJoin2-4 10000 106724 ns/op
//+连接性能最差,bytes.Buffer性能表现最好。
- Unicode。rune类型说专门用来存储Unicode码点(code int),是int32别名,utf-32编码。使用单引号字面量。
- rune, byte, string之间可以直接进行转换
func (str Str) demo8() {
r := '中'//rune
fmt.Printf("%T, %c\n", r, r)//int32,中
//rune, byte, string之间可以直接进行转换
s := string(r)
b := byte(r)
s2 := string(b)
r2 := rune(b)
fmt.Println(s, b, s2, r2)
s3 := []rune{'a', '晓', '中'}
fmt.Println(string(s3))//a晓中
//通过utf8.RuneCountInString函数能准确返回unicode字符数量
s4 := "斜风·细雨"
fmt.Println(len(s4), utf8.RuneCountInString(s4))//14 5
}
数组
- 长度是数组类型的组成部分。类型相同,但长度不同依然不属于同一类型。
- go反射 https://studygolang.com/articles/12348?fr=sidebar
- 数组初始化
//数组初始化
func (arr *ArrayDemo) initDemo() {
var a [4]int //元素自动赤化为0
fmt.Println(a)//[0 0 0 0]
b := [4]int{1, 2} //未初始化的元素自动补为0
fmt.Println(b) //[1 2 0 0]
c := [4]int{5, 2:9, 3:10} //制定下标2的为9,3的元素为10
fmt.Println(c) //[5 0 9 10]
//编译器根据元素数量确定数组长度
d := [...]int{1, 2, 2}
e := [...]int{1, 8:8}
fmt.Println(d)//[1 2 2]
fmt.Println(e)//[1 0 0 0 0 0 0 0 8]
}
//复合类型数组初始化,可省略类型标签
func (arr *ArrayDemo) initDemo2() {
type user struct {
name string
age byte
}
d := [...]user{
{"Tom", 22},//省略类型标签
{"Sword", 23},
{"Jack", 16},
}
fmt.Println(d)
}
- 定义多维数组时,仅第一维可以使用…
func (arr *ArrayDemo) initDemo3() {
a := [2][2]int{
{1,1},
{2,2},
}
b := [...][3]int{
{1,1,1},
{2,2,2},
{3,3,3},
}
fmt.Println(a, b)
}
- 内置函数len,cap返回数组第一维的长度。
- 如果元素支持“==”,“!=”操作,则对应的数组也支持。
- 指针数组是指元素为指针类型的数组,数组指针是获取数组变量的地址。
func (arr *ArrayDemo) ptrDemo1() {
x, y := 10, 20
a := [...]*int{&x, &y} //元素为指针的指针数组
p := &a
fmt.Printf("%T, %v\n", a, a)//[2]*int, [0xc000184040 0xc000184048]
fmt.Printf("%T, %v\n", p, p)//*[2]*int, &[0xc000184040 0xc000184048]
}
- 数组指针直接克直接操作元素
func (arr *ArrayDemo) ptrDemo2() {
a := [...]int{1, 2, 3}
p := &a //数组指针
p[1] += 10
fmt.Println(p[1]) //12
fmt.Println(a) //[1 12 3]
pp := a //数组拷贝,pp修改不影响源数组
pp[1] += 10
fmt.Println(pp) //[1 22 3]
fmt.Println(a) //[1 12 3]
}
- 与C数组变量隐式作为指针使用不同,Go数组是值类型,赋值和传参都会导致复制整个数组数据。
- 可以使用指针或者切片来避免数组复制。
func (arr *ArrayDemo) ptrDemo3(x *[2]int) {
fmt.Printf("x: %p, %v\n", x, *x)
x[1] += 10
}
func (arr *ArrayDemo) ptrDemo4() {
a := [2]int{1, 2}
arr.ptrDemo3(&a)
fmt.Printf("a: %p, %v\n", &a, a)
}
切片
- 切片本身并非动态数组或数组指针,切片是通过内部指针引用底层数组,设定相关属性将读写操作限定在制定区域内。
- 切片本身是只读对象,机制类似数组指针的包装
type slice struct {
Data unsafe.Pointer
Len int
Cap int
}
- 切片是引用类型,需使用make函数或显式初始化语句。会自动完成底层数组内存分配。
func (sl *SliceDemo) demo1() {
s1 := make([]int, 2, 4)
s2 := []int{1, 2, 3, 4}
fmt.Println(s1, len(s1), cap(s1)) //[0 0] 2 4
fmt.Println(s2, len(s2), cap(s2)) //[1 2 3 4] 4 4
}
- 切片不支持比较操作。
- 可以获取切片元素地址,但不能像数组那样,通过指针直接访问元素内容
//可以获取切片元素地址,但不能像数组那样,通过指针直接访问元素内容
func (sl *SliceDemo) demo3() {
s := []int{1,2,3} //切片
a := [3]int{1,2,3}//数组
p1 := &s
p2 := &a
fmt.Println((*p1)[1])//需先通过(*p1)返回[]int对象后,才能访问元素内容
fmt.Println((*p2)[1])
fmt.Println(p1[1]) //切片不支持
fmt.Println(p2[1])
}
- 切片式很小的结构体,可用来代替数组,避免复制开销。也并非所有场景都适合切片代替数组,因为切片底层数组可能在堆上分配内存(?),小数组在栈上拷贝消耗也未必比make代价大。
- make函数允许在运行期间动态指定长度,绕开数组类型长度必须使用编译器常量的限制。
- 多个切片基于同一个数组,对此层数组对修改,切片都可见
//多个切片基于同一个数组,对此层数组对修改,切片都可见
func (sl *SliceDemo) demo4() {
arr := [...]int{1,2,3,4,5,6}
s1 := arr[1:3]
s2 := arr[1:3]
fmt.Println(s1)//[2,3]
fmt.Println(s2)//[2,3]
s2[0] += 10
fmt.Println(s1)//[12 3]
fmt.Println(s2)//[12 3]
}
append
向切片尾部添加数据,返回新的切片对象。数据被追加到底层数组,如果超过cap限制,则为切片重新分配数组。新分配到cap长度上原cap的2倍。如果是较大的切片,也会尝试扩容1/4来节约资源。- copy 实现数据复制。copy 不会新建新的内存空间,由它目标切片原来的切片长度决定。
func (sl *SliceDemo) demo5() {
arr := [...]int{1,2,3,4,5,6}
s1 := arr[:3]
s2 := make([]int, 3, 3)
copy(s2, s1)//复制切片s1到s2
s1[0] += 10
fmt.Println(s1)//[11 2 3]
fmt.Println(s2)//[1 2 3]
//从字符串复制到[]byte
b := make([]byte, 3)
copy(b, "abcde")
fmt.Println(b) //[97 98 99], 只复制了abc
}
- 如果切片长时间引用大数组中的很小片段,可以复制出数据,以便于数组内存被及时回收。
字典
- 字典是引用类型,使用make函数或初始化表达式创建。无序键值对集合。key必须是支持相等运算的类型(==, !=),如字符串,数字,指针,数组,结构体,指针以及对应接口类型。
- 访问不存在的键值,返回零值,不会引发报错,但通过零值无法判断是否存在,。推荐使用ok-idiom模式来访问键值。
func (md *MapDemo) demo1() {
m1 := map[string]int{}
m2 := make(map[string]int)
m1["a"] = 1
m2["a"] = 2
if v, ok := m1["d"]; ok {
fmt.Println(`m["d"] `, v)
} else {
fmt.Println(`not found m["d"]`)
}
//值为匿名结构类型
m3 := map[int]struct{
x int`json:"x"`
}{
1 : {x:100},
2 : {x:200},
3 : {x:300},
}
fmt.Println(m3)//map[1:{100} 2:{200} 3:{300}]
//访问不存在的键值,返回零值,不会引发报错
fmt.Println(`m["d"] `, m1["d"])//0
fmt.Println(m3[4])//{0}
}
- 对字典迭代,每次返回的key顺序不定。
- len函数返回字典key数量,cap不接受map类型。
- 因为内存访问安全和哈希算法的缘故,
字典是“not addressable”不可寻址的,故不能修改value成员,即value是结构体或数组是,只能重新复制,不可修改
,理解为何这样设计了嘛?
func (md *MapDemo) demo2() {
type user struct {
name string
age byte
}
//如何把Jack的age改为23
m := map[int]user{
1:{"Tony", 20},
2:{"Jack", 22},
}
fmt.Println(m[2].age) //可以访问value的成员属性,但是不能直接修改name和age
//m[2].age = 23 //不可直接修改
m[2] = user{"Jack", 23}
fmt.Println(m) //map[1:{Tony 20} 2:{Jack 23}]
}
- 不能对nil字典(初始化)进行写操作,但可以读(返回零值)
- 在map迭代期间,删除或新增key是安全的。
- map是非线程安全的,并发的操作会(读写删除)导致进程崩溃。
func (md *MapDemo) demo4() {
var l sync.RWMutex
m := make(map[string]int)
i := 0
go func() {
for {
//加锁,
l.Lock()
m["a"] = i
i++
l.Unlock()
time.Sleep(time.Microsecond)
}
}()
go func() {
for {
l.Lock()
fmt.Println(m["a"])
l.Unlock()
time.Sleep(time.Microsecond)
}
}()
//阻止进程退出
select{}
}
- 在创建时,准备充足的空间,可减少内存重新分配和重新哈希计算。通过make函数传入map的容量。
- 对于海量小对象,应用字典直接存储键值数据拷贝,而非指针。有助于减少需要扫描对象的数量,大幅缩短垃圾回收时间。字典不会收缩内存,适当替换成新对象是必要的。
结构体
- 结构体是将不同类型命名字段序列打包为复合类型。字段名必须唯一,可用”_“补位,支持使用自身指针类型成员。字段名和排列顺序也是结构体类型的组成部分。
- 空结构struct{}
- 匿名字段,没有名字,只有类型。也叫嵌入字段(类型)
- 字段标签,用于对字段描述的元数据。也是类型的组成部分。在运行期间,可以通过反射获取标签信息。通常用作 数据格式校验,数据库关系映射。
type attr struct {
}
type file struct {
name string
attr //匿名
}
//通过反射获取标签信息
func (sd *StructDemo) demo2() {
type user struct {
name string `昵称`
sex byte `性别`
}
u := user{"Tony", 1}
v := reflect.ValueOf(u)
t := v.Type()
for i, n := 0, t.NumField(); i < n; i++ {
fmt.Printf("%s : %v\n", t.Field(i).Tag, v.Field(i))
}
//昵称 : Tony
//性别 : 1
}
- 内存布局,结构体内存总是一次性分配。各字段在相邻的地址空间安定义的顺序排列。对于引用类型,字符串,指针,在结构体内存中只包含基本的头数据。
- 在分配内存时,做字段对齐处理,通常以字段中最长的基础类型宽度为标准。
unsafe.Sizeof
函数返回操作数在内存中的字节大小,参数可以是任意类型的表达式.unsafe.Alignof
函数返回对应参数的类型需要对齐的倍数.内存地址对齐
,计算机在加载和保存数据时,如果内存地址合理地对齐的将会更有效率。由于地址对齐这个因素,一个聚合类型(结构体或数组)的大小至少是所有字段或元素大小的总和,或者更大因为可能存在内存空洞。内存空洞是编译器自动添加的没有被使用的内存空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐。
方法
- 方法是与对象实例绑定的函数。方法是面向对象的概念。
- 方法与函数的区别在于,方法有前置实例接受参数。
- 可以除接口和指针外的任何类型定义方法
- 方法不支持重载。
- 方法接受参数用简短缩写,不推荐用this,self。如果方法内没有引用实例,可省略参数名,只保留类型。
type N int
func (n N) toString() string {
return fmt.Sprintf("%#x", n)
}
func (N) sayHello() { //省略接受参数名
fmt.Println("hello")
}
- 接受参数可以是基础类型或指针类型,非指针receiver调用会复制实例。
- receiver类型选择:
- 需要修改实例状态的用
*T
- 无需修改实例状态的小对象或固定值,建议用T
- 大对象建议用
*T
减少复制成本 - 引用类型,字符串,函数等指针包装对象,直接用T
- 如果包含Mutex等同步字段,用
*T
,避免因复制到值锁操作无效。 - 其他,不确定的用*T。
- 需要修改实例状态的用
- Go更倾向于组合优于继承的细想。将模块分解成独立的小单元,分别实现,再以匿名嵌入的方式组合到一起,共同实现对外接口。
- 组合没有父子依赖关系,不会破坏封装,整体与局部松耦合。
接口
- 接口代表一种调用契约,是方法声明的集合。在一些动态语言里,接口也叫做协议。是交互双方共同遵守的规则。
- 接口解除了类型依赖,屏蔽了内部实现细节。
- 接口实现机制有运行期开销(?),接口用场的使用场景是包对外提供访问,或预留扩展空间。
- go接口实现机制很简洁,只要目标类型方法集包含接口声明的全部方法,就可认为实现了接口,无须显式声明。目标类型可以同时实现多个接口。完全非侵入设计。
- 接口本身也是一种结构类型,但是编译器对其做了很多限制。
- 不能有字段
- 不能定义自己的方法
- 只能声明方法,不能实现
- 可嵌入其他接口类型
type iface struct {
tab *itab
data unsafe.Pointer
}
- 接口名通常以er结尾,方法名是声明的组成部分,但参数名可不能活省略。
- 没有声明任何方法的接口就是空接口(interface{}),类似面向对象里的根类型Object,可以赋值为任何类型的对象。
type data struct{}
func (data) string() string {
return ""
}
type node struct {
data interface{ //匿名接口类型
string() string
}
}
func interfaceDemo1() {
var t interface{ //定义匿名接口变量
string() string
} = data{}
n := node{data:t}
println(n.data.string())
}
- 将对象赋值给接口变量t时,会复制该对象。并且t是不可取址的(能 列举出那些变量不能取值(unaddressable))。可将对象指针赋值给接口变量,接口变量存储的就是指针的赋值品。
d := 10
var t interface{} = d
&t.(data) // can not take address of t.(data)
func iDemo() {
type data struct {
X int
}
d := data{100}
var t interface{} = &d
fmt.Printf("%T, %v\n", t.(*data), t.(*data).X)
t.(*data).X = 200
fmt.Println(t.(*data).X)
}
断言-类型推断
- 类型断言是一个在接口值上的操作,通过接口类型的实际类型。类型推断可将接口变量还原为原始类型,或用来判断是否实现了某个更具体的接口类型。
- 场景:一个方法需要接收多种类型的数据并需做分别处理,可以把形参设为空接口类型以接收任意类型的值,根据断言获取接口变量里实际保存的类型。
- 使用ok-idiom即使断言判断失败也不会引发panic
断言语法:
//x表示一个接口的类型,T表示一个类型(也可为接口类型)
value,ok := x.(T)
func assertDemo1() {
var x interface{} = 10
if v, ok := x.(int); ok {
fmt.Println(v)
}
}
func assertDemo2() {
func(x interface{}) {
//element.(type)语法只能在switch中使用
switch x.(type) {
case int:
fmt.Println("type : int")
case string:
fmt.Println("type : string")
case float64:
fmt.Println("type : float")
default:
fmt.Println("type : unknown")
}}("100")
}
type Dd int
//实现fmt.Stringer接口
func (d Dd) String() string {
return fmt.Sprintf("data : %d", d)
}
func assertDemo3() {
var d Dd = 20
var x interface{} = d
//判断是否实现了具体的接口,转换为更具体的接口类型
if n, ok := x.(fmt.Stringer); ok {
fmt.Println(n) //data : 20
}
if d2, ok := x.(Dd); ok {
fmt.Println(d2)//data : 20
}
}
- fmt.Stringer 接口,Stringer接口定义在fmt包中,该接口包含String()方法。任何类型只要定义了String()方法,进行Print输出时,就可以得到定制输出。
//定义函数类型,让相同签名的函数自动实现某个接口
type FuncString func() string
func (f FuncString) String() string{
return f()
}
func iDemo3() {
var t fmt.Stringer = FuncString(func() string {
return "hello world"
})
ff := FuncString(func() string {
return "函数类型实例化"
})
fmt.Println(t) //hello world
fmt.Println(ff) //函数类型实例化
fmt.Println(ff()) //函数类型实例化
}
并发
- 并发:逻辑上同时处理多个任务的能力。
- 并行:物理上同时处理多个任务的能力,需要多核处理器。
- 协程在单个线程上通过主动上下文切换换来多任务并发,找回(减少)因阻塞的时间等待,减少了线程切换的开销,效率不错。
- 没用并发方案都有其适用的场景:
- 多进程实现分布式和负载均衡,减少进程垃圾回收压力。
- 用多进程抢夺更多的处理资源。
- 用协程来提高CPU时间片利用率。
- 关键字go并非创建并行任务操作,而是创建并发任务单元。新任务被放到系统队列中,等待调度器安排合适的系统线程去获取执行权。当前任务不会被阻塞,并发任务不保证顺序。
- 每个任务会保存函数指针,调用参数,执行所需的栈内存空间。
- 系统默认线程栈为MB级别,而goroutine自定义栈初始仅需2KB, 所以才能穿件成千上万的并发任务。自定义栈按需分配策略,在需要时扩充,最大可达GB规模。
- 与defer一样,goroutine也会因为延迟执行,而立即计算并复制执行参数。
var c int
func counter() int {
c++
return c
}
//与defer一样,goroutine也会因为延迟执行,而立即计算并复制执行参数。
func grDemo1() {
a := 100
go func(x, y int) {
time.Sleep(time.Second)
println("goroutine: ", x, y)
}(a, counter())
a += 100
println("main: ", a, counter())
time.Sleep(3 * time.Second)
//main: 200 2
//goroutine: 100 1
}
- 主进程退出,并不会等并发任务结束,可用通道阻塞来做同步控制。发送信号,关闭通道都可以解除阻塞。
//主进程退出,并不会等并发任务结束,可用通道阻塞来做同步控制。发送信号,关闭通道都可以解除阻塞。
func grDemo2() {
sig := make(chan bool)
go func() {
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
fmt.Println("goroutine: ", i)
}
sig <- true
close(sig)
}()
<- sig
}
- 等待多个任务结束时,推荐使用sync.WaitGroup. 通过设定计数器,让goroutine退出时递减,计数器减到零退出。
func grDemo3() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)//累加操作要在goroutine外进行,免得未执行加,wg.Wait()就退出了
go func(id int) {
defer wg.Done()
time.Sleep(time.Second)
fmt.Println("goroutine: ", id, " done")
}(i)
}
println("main...")
wg.Wait()
println("main exit.")
}
//可以在多处使用Wait阻塞,都可以收到通知
func grDemo4() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
wg.Wait()
println("wait exit.")
}()
go func() {
time.Sleep(time.Second)
println("done.")
wg.Done()
}()
wg.Wait()
println("main exit.")
}
- 与线程不同,goroutine任务没有优先级,无法获取编号,没有局部存储TLS,返回值也会被抛弃。
func grDemo5() {
var wg sync.WaitGroup
var gs [5]struct { //用于实现类似TLS功能
id int //编号
result int //返回值
}
for i := 0; i < len(gs); i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
gs[id].id = id
gs[id].result = id + 100
}(i)
}
wg.Wait()
fmt.Printf("%+v\n", gs)
}
- Gosched 暂停当前任务,释放线程去执行其他任务。当前任务被放回队列,等下次调度时恢复执行。
func grDemo6() {
runtime.GOMAXPROCS(1)
exit := make(chan struct{})
go func() {
defer close(exit)
go func() {
println("b")
}()
for i := 0; i < 4; i++ {
println("a: ", i)
//让出当前线程,执行任务b
if i == i {
//Gosched yields the processor, allowing other goroutines to run.
runtime.Gosched()
}
}
}()
<- exit
}
- Goexit函数立刻终止当前任务,确保所有延迟调用被执行。不会影响其他并发任务。
- Goexit函数无论出于那一层函数里,都会立刻终止当前goroutine任务,而return只是退出当前函数。
- os.Exiti可以终止进程,但是不会执行延迟调用。
func grDemo7() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
defer println("a")
func () {
defer func() {
println("b", recover() == nil)
}()
func () {
println("c")
runtime.Goexit() //立刻终止整个调用堆栈
println("c done") //不执行
}()
println("b done") //不执行
}()
println("a done") //不执行
}()
wg.Add(1)
go func() { //不受上述Goexit影响
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println("-----------------", i)
time.Sleep(time.Second)
}
}()
wg.Wait()
}
通道
- go允许全局变量,指针,引用类型这些非安全的内存共享操作,这需要开发人员自行维护数据一致性和完整性。
- go更推荐使用通信来代替内存共享,实现并发安全。
- 从底层讲,通道就是一个队列。
- 同步模式发送接收配对,直接赋值数据给对方,配对失败则等待,知道另一方出现才唤醒。
- 异步模式,抢夺数据缓冲槽。发送方要求有空槽共写入数据,接收方要求有数据可读,不满条件则等待,直到写入数据或腾出空槽共写入数据。
- 通道可用作传递数据,还常做事件通知使用。
- 同步模式必须有配对的goroutine发送和接收,否则会一直阻塞。
- 缓冲区大小属于内部属性,不属于类型组成部分。
同步通道len(),cap()都是返回0,用根据此区分同步和异步模式
func (Channel) demo1() {
a, b := make(chan int), make(chan int , 3)
go func() {
println(<- a)
}()
a <- 1
b <- 1
b <- 2
println(len(a), cap(a))//0 0
println(len(b), cap(b))//2 3
}
- 可以使用ok-idiom,range方式来处理通道数据。
- 通道默认是双向的,我们可以限制收发操作的方向来实现更严谨的逻辑。通常通过类型转换来获取单向通道。
- 不能在单向通道上进行逆向操作。
- close操作也不能用在接收端。
- 无法将单向通道重新转换为双向通道。
func (Channel) demo4() {
var wg sync.WaitGroup
wg.Add(2)
c := make(chan int)
var send chan <- int = c //发送通道
var recv <- chan int = c //接收通道
go func() {
defer wg.Done()
for x := range recv {
fmt.Println(x)
}
}()
go func() {
defer wg.Done()
defer close(send)
for i := 0; i < 3; i++ {
send <- i
}
}()
wg.Wait()
}
- 同时处理多个通道时,可以用select。
- 将通道设置为nil,该case会被阻塞,不会被select选中。
- 如果无可执行的case,且没有default,则阻塞。通过default可避免select阻塞,或通过default执行默认操作。
func (Channel) demo5() {
var wg sync.WaitGroup
ch1 := make(chan int)
ch2 := make(chan int)
wg.Add(2)
go func() {
for {
var ok bool
var v int
select {
case v, ok = <- ch1:
if !ok {
ch1 = nil //通道关闭,将其设置为nil,该case会被阻塞,不会被select选中
break
}
fmt.Println("ch1 : ", v)
case v, ok = <- ch2:
if !ok {
ch2 = nil //通道关闭,将其设置为nil,该case会被阻塞
break
}
fmt.Println("ch2 : ", v)
}
if ch1 == nil && ch2 == nil { //全部结束,退出循环
return
}
}
}()
go func() {
defer wg.Done()
defer close(ch1)
for i := 1; i < 9; i += 2 {
ch1 <- i
time.Sleep(time.Second)
}
}()
go func() {
defer wg.Done()
defer close(ch2)
for i := 2; i < 10; i += 2 {
ch2 <- i
time.Sleep(time.Second)
}
}()
wg.Wait()
}
//通过default 避免select阻塞
func (Channel) demo6() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for {
select {
case x, ok := <- c:
if !ok {
return
}
fmt.Println(x)
default: //通过default 避免select阻塞
}
fmt.Println(time.Now())
time.Sleep(time.Second)
}
}()
time.Sleep(time.Second * 5)
c <- 999
close(c)
<- done
}
- 通道是并发安全的队列,可用作ID generator, Pool等用途。
- 用通道实现信号量。
func (Channel) demo8() {
runtime.GOMAXPROCS(4)
var wg sync.WaitGroup
sem := make(chan struct{}, 2) //最多允许并发执行数
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
sem <- struct{}{}
defer func() {<- sem}()
time.Sleep(time.Second * 2)
fmt.Println(id, time.Now())
}(i)
}
wg.Wait()
}
- 标准库time提供的timeout和tick channel实现。
- time.After()回一个通道,该通道在指定时间后返回一个时间对象Time
- time.Tick()返回的是一个channel,每隔指定的时间会有数据从channel中出来
- time.AfterFunc() 在一段时间后执行指定的函数
func (Channel) demo9() {
go func() {
for {
select {
// After waits for the duration to elapse and then sends the current time
// on the returned channel.
//time.After返回一个通道,该通道在指定时间后返回一个时间对象Time
case <- time.After(time.Second * 5):
fmt.Println("timeout...")
os.Exit(0)
}
}
}()
go func() {
//time.Tick()返回的是一个channel,每隔指定的时间会有数据从channel中出来
//返回一个通道,每一秒钟会从通道返回一个时间time
tick := time.Tick(time.Second)
for {
select {
case time := <- tick:
fmt.Println(time)
}
}
}()
<- (chan struct {})(nil) //直接用nil通道阻塞进程
}
func (Channel) demo10() {
time.AfterFunc(time.Second * 3, func() {
fmt.Println("在一段时间后执行指定的函数")
os.Exit(0)
})
fmt.Println("主进程不受阻塞")
<- (chan struct {})(nil) //用nil通道阻塞进程
//主进程不受阻塞
//在一段时间后执行指定的函数
}
- 捕获INT, TREAM信号,实现简易的atexit函数
var exits = &struct {
sync.RWMutex
funcs []func()
signals chan os.Signal
}{}
func ateixt(f func()) {
exits.Lock()
defer exits.Unlock()
exits.funcs = append(exits.funcs, f)
}
func waitExit() {
if exits.signals == nil {
exits.signals = make(chan os.Signal)
//SIGINT关联ctrl+c,对当前进程发送结束信号
//SIGTERM可以被阻塞、处理和忽略;因此有的进程不能按预期的结束
// 程序接收到SIGTERM信号后,会先释放自己的资源,然后在停止。SIGTERM多半是会被阻塞的、忽略。
signal.Notify(exits.signals, syscall.SIGINT, syscall.SIGTERM)
}
exits.RLock()
for _, f := range exits.funcs {
defer f()
}
exits.RUnlock()
<- exits.signals
}
func (Channel) demo11() {
ateixt(func() {
println("exit1...")
})
ateixt(func() {
println("exit2...")
})
waitExit()
}
//执行:捕获ctrl+c
//exit2...
//exit1...
- signal.Notify(), Notify函数让signal包将输入信号转发到c。如果没有列出要传递的信号,会将所有输入信号传递到c;否则只传递列出的输入信号。signal包不会为了向c发送信息而阻塞(就是说如果发送时c阻塞了,signal包会直接放弃):调用者应该保证c有足够的缓存空间可以跟上期望的信号频率。对使用单一信号用于通知的通道,缓存为1就足够了。
func Notify(c chan<- os.Signal, sig ...os.Signal)
性能
- 将发往通道的数据打包,减少传输次数,可有效提升性能。从实现上看,通道队列依然是锁同步机制,单次获取更多的数据(批处理),可改善因频繁加锁造成的性能问题。
- 把数据打包到一个数组中批量发送和读取,从而减少加锁的开销,性能有明显提升。
//通道性能测试
const (
max = 50000000 //数据统计上限
block = 500 //数据块大小
bufsize = 100 //缓冲区大小
)
func chanTest() {
done := make(chan interface{})
c := make(chan int, bufsize)
go func() {
defer close(done)
count := 0
for x := range c {
count += x
}
}()
for i := 0; i < max; i++ {
c <- i
}
close(c)
<- done
}
func blockTest() {
done := make(chan interface{})
c := make(chan []int, bufsize)
go func() {
defer close(done)
count := 0
for v := range c {
for _, x := range v {
count += x
}
}
}()
for i := 0; i < max; i += block {
var b [block]int
for n := 0; n < block; n++ {
b[n] = i + n
if i + n == max - 1 {
break
}
}
}
close(c)
<- done
}
func BenchmarkCh1(b *testing.B) {
for i := 0; i < b.N; i++ {
chanTest()
}
}
func BenchmarkCh2(b *testing.B) {
for i := 0; i < b.N; i++ {
blockTest()
}
}
//go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out
//BenchmarkCh1-8 1 4948139000 ns/op 68880 B/op 21 allocs/op
//BenchmarkCh2-8 51 22550475 ns/op 2925 B/op 3 allocs/op
- 资源泄漏,当goroutine处于发送或接受阻塞状态时,一直未被唤醒。垃圾回收器并不能收集此类资源,导致在队列里长久休眠,占用资源。
- GODEBUG=”gctrace=1,schedtrace=1000,scheddetail=1” ./main
同步
- 通道并不是用来代替锁的,它们有各自的适用场景,通道更倾向于解决逻辑层面并发处理架构,而锁等多用于保护局部范围数据安全。
- 标准库sync提供了互斥和读写锁,原子操作等。
- sync.Mutex,互斥锁Mutex
- sync.RMutex, 针对读写操作的互斥锁,读写锁与互斥锁最大的不同就是可以分别对 读、写 进行锁定。一般用在大量读操作、少量写操作的情况
- sync.Once,确保传入的函数只被执行一次,无论调多少次,。
- sync.Cond,条件锁(唤醒锁),共享资源状态变更时通知等待的线程
- sync.Waitgroup,用于等待一组 goroutine 同步。
- sync.Pool,临时对象池,减少GC。不适合数据库等连接池。
- sync.Locker
- sync.Map, 线程安全的map
- Mutex作为匿名字段是,方法的接收者必须为指针类型,否则会因复制导致锁失效。
- 或者迁入*Mutex来避免复制,但是需要专门的初始化
type syncData1 struct {
sync.Mutex
}
func (d *syncData1) test1(s string) {
d.Lock()
defer d.Unlock()
for i := 0; i < 3; i++ {
println(s, i)
time.Sleep(time.Second)
}
}
type syncData2 struct {
*sync.Mutex
}
func (d syncData2) test1(s string) {
d.Lock()
defer d.Unlock()
for i := 0; i < 3; i++ {
println(s, i)
time.Sleep(time.Second)
}
}
func (SyncDemo) demo1() {
var wg sync.WaitGroup
wg.Add(2)
var d syncData1
//需要专门初始化
var d2 syncData2 = syncData2{
&sync.Mutex{},
}
go func() {
defer wg.Done()
d.test1("read1")
d2.test1("------read2")
}()
go func() {
defer wg.Done()
d.test1("write1")
d2.test1("------write2")
}()
wg.Wait()
}
//write1 0
//write1 1
//write1 2
//------write2 0
//read1 0
//read1 1
//------write2 1
//read1 2
//------write2 2
//------read2 0
//------read2 1
//------read2 2
- 锁粒度应该尽可能的控制在小范围,尽早释放。
- Mutex不支持递归锁, 会导致死锁。
//Mutex不支持递归锁
var m sync.Mutex
m.Lock()
{
m.Lock()
m.Unlock()
}
m.Unlock()
- 通过defer 来释放锁,会导致锁释放延迟。高性能场景避免defer Unlock
- 在读写并发RWmutex性能更好一些。
- 对单个数据的保护,尽可能使用原子操作。
- 执行严格测试,尽可能打开数据竞争测试。
- sync.Once.Do(f func())能保证once只执行一次, 无论是否更换传入的方法,sync.Once能确保实例化对象Do方法在多线程环境只运行一次。
func once1() {
fmt.Println("once 111111")
}
func once2() {
fmt.Println("once 222222")
}
//sync.Once.Do(f func())能保证once只执行一次, 无论是否更换传入的方法,
//sync.Once能确保实例化对象Do方法在多线程环境只运行一次,
var once sync.Once
func (SyncDemo) demo3() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
once.Do(once1)
fmt.Println(i)
}
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
once.Do(once1)
once.Do(once2)
fmt.Println("---------", id)
}(i)
}
wg.Wait()
}
//输出
//once 111111
//0
//1
//2
//3
//4
//--------- 4
//--------- 0
//--------- 2
//--------- 3
//--------- 1
- sync.Cond, 条件锁(唤醒锁)
- 场景: 去使用打印机,发现已经有人在用了,你可以:1、一直在那等(多耽误事啊)。2、去忙别的,等别人用完了,让他发消息告诉你。
- 条件锁是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用。
- 条件锁并不是被用来保护临界区和共享资源的,而是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程.
- 方法:
- Wait() 阻塞当前线程,直到收到该条件变量发来的通知
- Signal() 单发通知,向一个正在等待它的通知的线程发送通知,表示共享数据的状态已经改变。
- Broadcast() 广播通知,给所有正在等待的线程都发送通知。被换新的多个线程,依然要排队等待共享资源,逐个运行。
//条件锁(唤醒锁) sync.Cond
//场景: 去使用打印机,发现已经有人在用了,你可以:
//1、一直在那等(多耽误事啊)
//2、去忙别的,等别人用完了,让他发消息告诉你
//是基于互斥锁的,它必须有互斥锁的支撑才能发挥作用
//并不是被用来保护临界区和共享资源的,而是用于协调想要访问共享资源的那些线程的。当共享资源的状态发生变化时,它可以被用来通知被互斥锁阻塞的线程.
func (SyncDemo) syncCondDemo() {
var cond *sync.Cond = sync.NewCond(&sync.Mutex{})
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
cond.L.Lock()
fmt.Printf("goroutine %d wait...\n", id)
cond.Wait() //阻塞当前线程,直到收到该条件变量发来的通知
fmt.Print(id, " ")
time.Sleep(time.Second)
cond.L.Unlock()
}(i)
}
fmt.Println("start all")
time.Sleep(time.Second * 1)
fmt.Println("send a signal")
//单发通知: signal,让该条件变量向至少一个正在等待它的通知的线程发送通知,表示共享数据的状态已经改变。
cond.Signal() //发一个通知给已经获取锁的goroutine
time.Sleep(time.Second * 1)
fmt.Println("\nsend a signal")
cond.Signal()
//3秒之后 下发广播给所有等待的goroutine
time.Sleep(time.Second * 3)
fmt.Println("\nBroadcast to wate up all goroutine")
cond.Broadcast()
wg.Wait()
}
//start all
//goroutine 1 wait...
//goroutine 4 wait...
//goroutine 2 wait...
//goroutine 3 wait...
//goroutine 5 wait...
//goroutine 6 wait...
//goroutine 7 wait...
//goroutine 0 wait...
//send a signal
//1
//send a signal
//4
//Broadcast to wate up all goroutine
//0 5 2 3 6 7
包结构
- 显示包路劲列表
go list net/...
net
net/http
net/http/cgi
net/http/cookiejar
net/http/fcgi
net/http/httptest
net/http/httptrace
net/http/httputil
net/http/internal
net/http/pprof
net/internal/socktest
net/mail
net/rpc
net/rpc/jsonrpc
net/smtp
net/textproto
net/url
测试
单元测试
- testing go自带的测试框架
- 测试代码必须在当前包,已”_test.go”结尾。
- 测试函数以Test为前缀。
- 测试命令(go test)忽略以“_”或”.”开头的测试文件。
- 正常的编译操作忽略测试文件。
- 标准库testing提供转门的类型T用来控制测试结果和行为。
- Fail方法,失败,继续执行当前测试
- FailNow方法,失败,立刻终止当前测试
- SkipNow方法,跳过,停止执行当前测试函数
- Log(), 输出错误信息,仅当失败或者-v时输出
- Parallel(),与有同样设置的函数并行执行,可缩短执行测试时间
- Error(),相当于Fail + Log
- Fatal(), 相当于FailNow + Log
func sum(x, y int) int {
return x + y
}
func TestSum(t *testing.T) {
var data = []struct {
x int
y int
expect int
}{
{1, 2, 3},
{2, 3, 5},
{4, 5, 9},
}
for _, v := range data {
actual := sum(v.x, v.y)
if actual != v.expect {
t.Errorf("sum(%d, %d): expected %d, actual %d", v.x, v.y, v.expect, actual)
}
}
}
func minus(x, y int) int {
return x - y
}
func TestMinus(t *testing.T) {
if minus(3, 1) != 2 {
t.FailNow()
}
}
//通过Parallel() 实现并行测试
func TestA(t *testing.T) {
t.Parallel()
time.Sleep(time.Second * 2)
}
//go test -v -args "b"
func TestB(t *testing.T) {
if os.Args[len(os.Args) - 1] == "b" {
t.Parallel()
}
time.Sleep(time.Second * 2)
}
- 常用测试参数
- -args 命令行参数
- -v 输出详细信息
- -parallel 并发执行,默认为GOMAXPROCS, 如 -parallel 2
- -run 指定测试函数,可正则,如 -run “Sum”
- -timeout 指定执行超时时间,如 -timeout 1m30s
- -count 重复执行次数,默认1
性能测试
- 性能测试函数必须以Benchmark开头,代码文件必须以_test.go结尾。
- go test 默认不会执行性能测试函数,需要加上参数
-bench
, 通过不断调整B.N的值,反复执行测试函数,知道获取测试结果。 - 运行
go test -bench .
- 通过
run=none
可以忽略单元测试 - 设定CPU参数,设定并发限制
go test -bench . -cpu 1,2,3
- 有时耗时操作,循环测试次数过少,不够准确,可以benchtime设定最小测试时间增加循环次数。
go test -bench . -benchtime 5s
- 运行指定的方法
go test -bench Add1
- 查看内存分配
go test -bench . -benchmem
//go 性能测试基准测试
//基准测试的代码文件必须以_test.go结尾
//基准测试的函数必须以Benchmark开头,必须是可导出的
//基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数
//基准测试函数不能有返回值
//最后的for循环很重要,被测试的代码要放到循环里
//b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能
import (
"sync"
"testing"
)
var m sync.Mutex
func call() {
m.Lock()
m.Unlock()
}
func deferCall() {
m.Lock()
defer m.Unlock()
}
func BenchmarkCall(b *testing.B) {
for i := 0; i < b.N; i++ {
call()
}
}
func BenchmarkCallDefer(b *testing.B) {
for i := 0; i < b.N; i++ {
deferCall()
}
}
//运行 go test -bench=. -run=none
//输出
//BenchmarkCall-8 92373063 13.6 ns/op
//BenchmarkCallDefer-8 32450937 37.4 ns/op 每次操作需要话费37.4纳秒
//go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out
- 常用flag,通过
go help testflag
可查看 ``` -bench regexp:运行性能测试,支持表达式对测试函数进行筛选。-bench .则是对所有的benchmark函数测试 -run regexp:运行单元测试函数, 比如-run ABC只测试函数名中包含ABC的测试函数 -benchmem:性能测试的时候显示测试函数的内存分配的统计信息 -benchtime t : 性能测试运行的时间,默认是1s -count n:运行测试和性能多少此,默认一次 -timeout t:测试时间如果超过t, panic,默认10分钟 -v:显示测试的详细信息,也会把Log、Logf方法的日志显示出来 -cpuprofile cpu.out : 是否输出cpu性能分析文件 -memprofile mem.out : 是否输出内存性能分析文件 -blockprofile block.out : 是否输出内部goroutine阻塞的性能分析文件
-test.blockprofile block.out : 是否输出内部goroutine阻塞的性能分析文件 -test.blockprofilerate n: 基本同上,控制的是goroutine阻塞时候打点的纳秒数。默认不设置就相当于-test.blockprofilerate=1,每一纳秒都打点记录一下 -test.parallel n : 性能测试的程序并行cpu数,默认等于GOMAXPROCS。 -test.cpu 1,2,4 : 程序运行在哪些CPU上面,使用二进制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一个道理 -test.short : 将那些运行时间较长的测试用例运行时间缩短
```go
//内存分配
func heap() []byte {
return make([]byte, 1024*10)
}
//go test -bench Heap -benchmem
func BenchmarkHeap(b *testing.B) {
b.ReportAllocs() //输出内存分配信息,无论是否使用-benchmem
for i := 0; i < b.N; i++ {
_ = heap()
}
}
代码覆盖率
go test -cover
性能监控
-
引发性能问题的无外乎执行时间过长,内存占用过多,意外的阻塞。
-
交互模式查看内存采样数据
- flat 仅当前函数,不包括他调用的其他函数
- sum 列表前几行所占百分比总和
- cum 当前函数调用堆栈累积
$ go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out
$ go tool pprof memprofile.out
输入top10
(pprof) top10
Showing nodes accounting for 5844.77MB, 99.95% of 5847.79MB total
Dropped 23 nodes (cum <= 29.24MB)
Showing top 10 nodes out of 22
flat flat% sum% cum cum%
5184.02MB 88.65% 88.65% 5184.02MB 88.65% go-sword-202001/practice.strJoin2
242.50MB 4.15% 92.80% 242.50MB 4.15% go-sword-202001/practice.rset1
132.13MB 2.26% 95.06% 132.13MB 2.26% bytes.makeSlice
119.12MB 2.04% 97.09% 119.12MB 2.04% strings.(*Builder).grow
106.50MB 1.82% 98.91% 106.50MB 1.82% reflect.(*structType).Field
60.50MB 1.03% 99.95% 167MB 2.86% go-sword-202001/practice.rset
0 0% 99.95% 132.13MB 2.26% bytes.(*Buffer).Grow
0 0% 99.95% 132.13MB 2.26% bytes.(*Buffer).grow
0 0% 99.95% 132.13MB 2.26% go-sword-202001/practice.BenchmarkBufferWrite
0 0% 99.95% 167MB 2.86% go-sword-202001/practice.BenchmarkRSet
peek malg
列出调用来源list malg
列出源码行统计样式,一遍直观定位web
,``web malg` 输出svg图形,需要安装Graphviz
工具链
go build
- go build 每次都会编译出标准库意外的所有依赖包
- -o 指定可执行文件名,默认与源文件同名
- -a 强制编译所有依赖包,含标准库
- -p 并行编译所使用的CPU核数,默认为 CPU 逻辑核数
- -v 显示待编译包的名字
- -n 打印编译时会用到的所有命令,但不真正执行
- -x 显示正在执行的编译命令
- -work 显示临时工作目录,完成后不删除
- -race 显示数据竞争检测,只支持amd64
- -gcflags 编译器参数设置
- ldflags 链接器参数设置
- gcflags 参数
- -B 禁止越界检查
- -N 禁止优化
- -l 禁止内联
- -u 禁用unsafe
- -S 输出汇编代码,如 go build -gcflags “-S” main.go
- -m 输出优化信息
- ldflags 参数
- -s 禁用符号表
- -w 禁用DRAWF 调试信息
- -X设置字符串全局变量值
- -H设置可执行文件格式
- 更多参数查看
go tool link -h
和go tool compile -h
go install
- 与go build的参数相同,但会将编译结果安装到bin、pkg目录下。
- 更重要的是 go install 支持增量编译,在没有修改的情况下回链接pkg下的静态包。
go get
- 将第三方包下载到GOPATH列表下第一个工作空间,默认不会检查更新。
- go get 参数
- -d 仅下载,不安装
- -u 更新包,包括其依赖项
- -f 和 -u 配合使用,强制更新,不检查是否过期
- -t 下载测试代码所依赖的包
- insecure 使用http等非安全协议
- -v 输出详细信息
- -x 显示正在执行的命令
- go env 显示环境参数
- go clean 清理工作目录,删除编译和安装遗留的目标文件。
编译
- 如果使用GDB这类调试器,编译时,添加参数
-gcflags "-N -l" 可阻止内联和优化
- 当发布时,通过
-ldflags "-w -s"
会让符号表剔除符号表和调试信息,能减少可执行文件大小,还可增加反编译难度。 - 借助专业工具对可执行文件进行减肥,upx 可执行程序文件压缩器
交叉编译
- 交叉编译是指在一个平台上编译出其他平台的可执行文件。如在Mac上编译出linux上的执行文件。
- GO实现自举后,交叉编译更方便。只需通过
GOOS, GOARCH
环境变量指定目标平台和架构。 - 建议用go install 为目标平台编译出标准库,避免go build 每次都完整编译。
$ GOOS=linux go install std
$ GOOS=linux go install cmd
条件编译
可根据runtime.GOOS 判断不同的操作系统平台 方法一:编译器支持文件级别的条件编译,将平台和架构信息添加到文件尾部。标准库中很多文件都是按照类似命名的。
hello_linux.go
hello_linux_386.go
hello_darwin.go
方法二:使用build编译指令,告诉编译器当前代码只能用于指定编译器。通知指令更加丰富灵活。
a.go
// +build windows
//这里需要有空行
package main
func hello() {
println("windows")
}
b.go
// +build linux darwin
//这里需要有空行
package main
func hello() {
println("unix.")
}
可添加多条指定,表示AND, 同一行的指定空格表示AND,逗号表示OR
// +build linux darwin
// +build 386,!cgo
相当于 (linx OR darwin) AND (386 AND (NOT cgo))
go generate
就是用go generate
扫描代码源文件找出注释中,带go:generate
的,提取其中的命令进行执行。
- 必须是.go的文件中
- 必须以
//go:generate
开头,双斜线后不能有空格 - 每个文件可有多个generate命令,支持环境变量
- 按文件名提取,串行执行,出错终止后续执行
a.go
//go:generate echo $GOPATH
//go:generate ls -lh
package main
func hello() {
println("windows")
}
这种设计初衷是为了完成一些自动化的命令,如清理一些测试代码,用于基于模板生成代码如 go mock生产mock文件//go:generate mockgen -destination=../mock/version_info_mock.go -package=mock -source=version_info.go
go generate命令参数
- -v 显示处理的包及文件名
- -x 显示执行的命令
- -n 仅显示命令,但不执行