前段时间一直在找工作,将自己的Go总结分享出来,期待大家交流~
目录
Wall clock(time) VS Monotonic clock(time)
Wall clock(time)就是我们一般意义上的时间,就像墙上钟所指示的时间。
Monotonic clock(time)字面意思是单调时间,实际上它指的是从某个点开始后(比如系统启动以后)流逝的时间,jiffies一定是单调递增的!
而特别要强调的是计算两个时间点的差值一定要用Monotonic clock(time),因为Wall clock(time)是可以被修改的,比如计算机时间被回拨(比如校准或者人工回拨等情况),或者闰秒( leap second),会导致两个wall clock(time)可能出现负数。(因为操作系统或者上层应用不一定完全支持闰秒,出现闰秒后系统时间会在后续某个点会调整为正确值,就有可能出现时钟回拨(当然也不是一定,比如ntpdate就有可能出现时钟回拨,但是ntpd就不会))
channel, goroutine, [], map, sync.Map等,
列举它们常见的严重伤害性能的 anti-pattern?
select 很多 channel 的时候,并发较高时会有性能问题。因为 select 本质是按 chan 地址排序,顺序加锁。lock1->lock2->lock3->lock4 活跃 goroutine 数量较多时,会导致全局的延迟不可控。比如 99 分位惨不忍睹。slice append 的时候可能触发扩容,初始 cap 不合适会有大量 growSlice。map hash collision,会导致 overflow bucket 很长,但这种基本不太可能,hash seed 每次启动都是随机的。此外,map 中 key value 超过 128 字节时,会被转为 indirectkey 和 indirectvalue,会对 GC 的扫描阶段造成压力,如果 k v 多,则扫描的 stw 就会很长。sync.Map 有啥性能问题?写多读少的时候?
noCopy原理:
type noCopy struct {
}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
https://www.cnblogs.com/qcrao-2018/p/12736031.html https://xiaorui.cc/archives/5878
首先,调用 p.pin() 函数将当前的 goroutine 和 P 绑定,禁止被抢占,返回当前 P 对应的 poolLocal,以及 pid。然后直接取 l.private,赋值给 x,并置 l.private 为 nil。判断 x 是否为空,若为空,则尝试从 l.shared 的头部 pop 一个对象出来,同时赋值给 x。如果 x 仍然为空,则调用 getSlow 尝试从其他 P 的 shared 双端队列尾部“偷”一个对象出来。Pool 的相关操作做完了,调用 runtime_procUnpin() 解除非抢占。最后如果还是没有取到缓存的对象,那就直接调用预先设置好的 New 函数,创建一个出来。
先绑定 g 和 P,然后尝试将 x 赋值给 private 字段。如果失败,就调用 pushHead 方法尝试将其放入 shared 字段所维护的双端队列中
package main
import (
"encoding/json"
"log"
)
type S struct {
A []string
}
func main() {
data := &S{}
data2 := &S{A: []string{}}
buf, err := json.Marshal(&data)
log.Println(string(buf), err)
buf2, err2 := json.Marshal(&data2)
log.Println(string(buf2), err2)
}
输出:
2009/11/10 23:00:00 {"A":null} <nil>
2009/11/10 23:00:00 {"A":[]} <nil>
结论:string与[]byte转换过程中的Data都会发生拷贝,例如:
// 可能的结果:
// 1374390312984
// 1374390312984 1374390230744
// 说明,string to slice,一定发生内容copy
func checkToSlice() {
str := fmt.Sprint(rand.Int63())
strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str))
fmt.Println(strHeader.Data)
toSlice := []byte(str)
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&toSlice))
fmt.Println(strHeader.Data, sliceHeader.Data)
}
// 可能的结果:
// 1374390152904
// 1374390152904 1374390152872
// 说明,slice to string,一定发生内容copy
func checkToString() {
slice := []byte(fmt.Sprint(rand.Int63()))
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
fmt.Println(sliceHeader.Data)
toStr := string(slice)
strHeader := (*reflect.StringHeader)(unsafe.Pointer(&toStr))
fmt.Println(sliceHeader.Data, strHeader.Data)
}
像如下代码会进行优化。
func Equal(a, b []byte) bool {
// Neither cmd/compile nor gccgo allocates for these string conversions.
return string(a) == string(b)
}
func (b *Builder) String() string {
return *(*string)(unsafe.Pointer(&b.buf))
}
func NoAllocBytes(buf string) []byte {
return *(*[]byte)(unsafe.Pointer(&buf))
}
正确做法:
func NoAllocBytes(buf string) []byte {
x := (*reflect.StringHeader)(unsafe.Pointer(&buf))
h := reflect.SliceHeader{x.Data, x.Len, x.Len}
return *(*[]byte)(unsafe.Pointer(&h))
}
https://github.com/go-delve/delve
brew install dlv/yum install dlv
const maxInt = int(^uint(0) >> 1)
rune = uint32
byte = uint8
注意点:两个strings.Builder禁止在写入之后拷贝,写入之前可以拷贝,如下例子,主要原因是写入会触发copyCheck检查。
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
// b2.WriteString("DEF") 失败 在写操作会进行copyCheck -> 如果内存空 会操作addr,不空则会判断地址是否一致
fmt.Println(b1, b2)
var b3, b4 strings.Builder
b4 = b3 // 一开始都为空 所以可以进行copy
b3.WriteString("123")
b4.WriteString("456")
fmt.Println(b3.String(), b4.String())
Go 结构体有时候并不能直接比较,当其基本类型包含:slice、map、function 时,是不能比较的。若强行比较,就会导致出现例子中的直接报错的情况。reflect.DeepEqual来判断不可比较类型,如:map、slice、func等
type Value struct {
Name string
Gender *string
}
func main() {
v1 := Value{Name: "1", Gender: new(string)}
v2 := Value{Name: "1", Gender: new(string)}
if v1 == v2 {
fmt.Println("yes")
return
}
fmt.Println("no")
}
输出:no,因为地址不一样
%v、%+v、%#v %v输出结构体各成员的值;%+v输出结构体各成员的名称和值;%#v输出结构体名称和结构体各成员的名称和值
%v的方式 = &{test 123456} %+v的方式 = &{name:test id:123456} %#v的方式 = &main.student{name:"test", id:123456}
14.Go垃圾回收机制
垃圾回收机制是Go一大特(nan)色(dian)。Go1.3采用标记清除法, Go1.5采用三色标记法,Go1.8采用三色标记法+混合写屏障。
标记清除法(mark and sweep):
分为两个阶段:标记和清除 第一步,暂停程序业务逻辑, 分类出可达和不可达的对象 第二步, 开始标记,程序找出它所有可达的对象,并做上标记 第三步, 标记完了之后,然后开始清除未标记的对象. 第四步, 停止暂停,让程序继续跑。然后循环重复这个过程,直到process程序生命周期结束
缺点:
三色标记法:
第一步 , 每次新创建的对象,默认的颜色都是标记为“白色”, 第二步, 每次GC回收开始, 会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入"灰色"集合。第三步, 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合。第四步, 重复第三步, 直到灰色中无任何对象。第五步: 回收所有的白色标记表的对象. 也就是回收垃圾。我们将全部的白色对象进行删除回收,剩下的就是全部依赖的黑色对象。
不加STW,会遇到对象丢失问题:
屏障机制:
强弱三色不变式:
1.强三色不变式
强制不允许黑色对象引用白色对象,目的在于破坏条件1。
2.弱三色不变式
黑色对象允许引用白色对象,白色对象存在其他灰色对象引用,或者可达它的链路上存在灰色对象,目的在于破坏条件2。
因此,在三色标级中满足强三色不变式或弱三色不变式之一,即可保证对象不丢失。
1.插入屏障 (为了保证栈的速度,不在栈上使用)
具体操作: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)
满足: 强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)
插入屏障不会在栈上操作,堆上处理没问题,但是如果栈不添加,当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况(黑色引用白色对象). 所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动STW暂停. 直到栈空间的三色标记结束.
所以插入屏障,最后会对栈上的所有对象进行一次三色标记法+STW保护。
2. 删除屏障
具体操作: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
满足: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)
这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。
Go V1.8的混合写屏障(hybrid write barrier)机制
插入写屏障和删除写屏障的短板:
有缓冲的channel是异步的,而无缓冲channel是同步的。
runtime三大函数:runtime.Gosched()、runtime.Goexit()、runtime.GOMAXPROCS() 可以,使用runtime.GOMAXPROCS(num int)可以设置线程数目。该值默认为CPU逻辑核数,如果设的太大,会引起频繁的线程切换,降低性能。runtime.Gosched(),用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。runtime.Goexit(),调用此函数会立即使当前的goroutine的运行终止(终止协程),而其它的goroutine并不会受此影响。runtime.Goexit在终止当前goroutine前会先执行此goroutine的还未执行的defer语句。请注意千万别在主函数调用runtime.Goexit,因为会引发panic。
chan panic触发时机:
chan 阻塞触发时机:
无缓存channel:
有缓存channel:
make 和 new 关键字的实现原理,make 关键字的作用是创建切片、哈希表和 Channel 等内置的数据结构,而 new 的作用是为类型申请一片内存空间,并返回指向这片内存的指针。
panic 函数可以被连续多次调用,它们之间通过 link 可以组成链表,内部包含:argp 是指向 defer 调用时参数的指针;arg 是调用 panic 时传入的参数;link 指向了更早调用的 runtime._panic 结构;recovered 表示当前 runtime._panic 是否被 recover 恢复;aborted 表示当前的 panic 是否被强行终止;
index := hash("author") % array.len
哈希在存储元素过多时会触发扩容操作,每次都会将桶的数量翻倍,扩容过程不是原子的,而是通过 runtime.growWork 增量触发的,在扩容期间访问哈希表时会使用旧桶,向哈希表写入数据时会触发旧桶元素的分流。除了这种正常的扩容之外,为了解决大量写入、删除造成的内存泄漏问题,哈希引入了 sameSizeGrow 这一机制,在出现较多溢出桶时会整理哈希的内存减少空间的占用。
哈希表的每个桶都只能存储 8 个键值对,一旦当前哈希的某个桶超出 8 个,新的键值对就会存储到哈希的溢出桶中。随着键值对数量的增加,溢出桶的数量和哈希的装载因子也会逐渐升高,超过一定范围就会触发扩容,扩容会将桶的数量翻倍,元素再分配的过程也是在调用写操作时增量进行的,不会造成性能的瞬时巨大抖动。
1.Context接口
Deadline()
Done()
Err()
Value()
type cancelCtx struct {
Context // 传入的context
mu sync.Mutex // 保护接下来的三个字段
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号 3.WithValue WithValue从父上下文中创建一个子上下文,返回valueCtx
type valueCtx struct {
Context
key, val interface{}
}
重写Value方法,从父上下文中获取 val 4.WithDeadline WithDeadline创建可以被取消的计时器上下文timerCtx
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
context.WithDeadline 在创建 context.timerCtx 的过程中判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc 创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel 同步取消信号。context.timerCtx 内部不仅通过嵌入 context.cancelCtx 结构体继承了相关的变量和方法,还通过持有的定时器 timer 和截止时间 deadline 实现了定时取消的功能 5.WithDeadline WithTimeout会调用WithDeadline
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
24.数组与切片的区别 数组是值类型,定长数组, StringHeader
type StringHeader struct {
Data uintptr
Len int
}
切片是引用类型,动态指向数组的指针,不定长指向数组array, SliceHeader
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
Go 语言根据接口类型是否包含一组方法将接口类型分成了两类:
type iface struct { // 16 字节
tab *itab
data unsafe.Pointer
}
type eface struct { // 16 字节
_type *_type
data unsafe.Pointer
}
接口类型interface{}
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
原则1: 接口到任意对象,入参均是interface{},出参为任意对象。
原则2: 反射对象获取interface{}变量。
可以通过reflect.Value.Interface完成这项工作。
v := reflect.ValueOf(1)
v.Interface().(int)
上述两个原则转换:
func main() {
i := 1
v := reflect.ValueOf(&i)
v.Elem().SetInt(10)
fmt.Println(i)
}
按照HTTP/1.1的规范,Go http包的http server和client的实现默认将所有连接视为长连接,无论这些连接上的初始请求是否带有Connection: keep-alive。1.http.Client 如果要关闭keep-alive, 需要通过http.Transport设置:
tr := &http.Transport{ DisableKeepAlives: true, } c := &http.Client{ Transport: tr, }
// 发送请求 rsp, err:= c.Do(req) defer rsp.Body.Close() // 读取数据 ioutil.ReadAll(rsp.Body)
2.http.Server server端完全不支持keep-alive连接方式:
s := http.Server{ Addr: ":8080", Handler: xxx, } s.SetKeepAliveEnabled(false) s.ListenAndServe()
闲置连接超时控制:
s := http.Server{ Addr: ":8080", Handler: xxx, IdleTimeout: 5 * time.Second, } s.ListenAndServe()
客户端和服务端响应的次数
由于map底层实现与 slice不同, map底层使用hash表实现,插入数据位置是随机的, 所以遍历过程中新插入的数据不能保证遍历。主要是对 key 排序,那么我们便可将 map 的 key 全部拿出来,放到一个数组中,然后对这个数组排序后对有序数组遍历,再间接取 map 里的值就行了。
package main
const cl = 100
var bl = 123
func main() {
println(&bl,bl)
println(&cl,cl) // 不可取cl地址
}
常量 常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用,
goto不可以跳转到其他函数或内层代码
package main
func main() {
for i:=0;i<10 ;i++ {
loop:
println(i)
}
goto loop
}
type aType int // defintion
type bType = int // alias
var i int
var i1 aType = i // 报错 需要强转
var i2 bType = i // 可以直接赋值
f, err := os.Open(xxx)
bufio.NewReader(f) // 默认 4k
buf := bufio.NewReaderSize(f, xxx)
for {
line, prefix, err := buf.ReadLine()
// dosomething
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}