让团队一起使用官方的 Go 格式工具,不要重新发明轮子。
尝试减少代码复杂度。 这将帮助所有人使代码易于阅读。
// NOT BADif foo() { // ...} else if bar == baz { // ...} else { // ...}// BETTERswitch {case foo(): // ...case bar == baz: // ...default: // ...}
当你在结构中看到 chan bool 的定义时,有时不容易理解如何使用该值,例如:
type Service struct { deleteCh chan bool // what does this bool mean? }
但是我们可以将其改为明确的 chan struct {} 来使其更清楚:我们不在乎值(它始终是 struct {}),我们关心可能发生的事件,例如:
type Service struct { deleteCh chan struct{} // ok, if event than delete something.}
你不需要将无类型的常量包装成类型,编译器会找出来。
另外最好将常量移到第一位:
// BADdelay := time.Second * 60 * 24 * 60// VERY BADdelay := 60 * time.Second * 60 * 24// GOODdelay := 24 * 60 * 60 * time.Second
// BADvar delayMillis int64 = 15000// GOODvar delay time.Duration = 15 * time.Second
// BADconst ( foo = 1 bar = 2 message = "warn message")// MOSTLY BADconst foo = 1const bar = 2const message = "warn message"// GOODconst ( foo = 1 bar = 2)const message = "warn message"
这个模式也适用于 var。
defer func() { err := ocp.Close() if err != nil { rerr = err } }()
package main type Status = int type Format = int // remove `=` to have type safety const A Status = 1 const B Format = 1 func main() { println(A == B) }
func f(a int, _ string) {}
func f(a int, b int, s string, p string)
func f(a, b int, s, p string)
var a <>string b := <>string{} fmt.Println(reflect.DeepEqual(a, <>string{})) fmt.Println(reflect.DeepEqual(b, <>string{})) // Output: // false // true
value := reflect.ValueOf(object) kind := value.Kind() if kind >= reflect.Chan && kind <= reflect.Slice { // ... }
func f1() { var a, b struct{} print(&a, "\n", &b, "\n") // Prints same address fmt.Println(&a == &b) // Comparison returns false } func f2() { var a, b struct{} fmt.Printf("%p\n%p\n", &a, &b) // Again, same address fmt.Println(&a == &b) // ...but the comparison returns true }
const ( _ = iota testvar // testvar 将是 int 类型 )
vs
type myType int const ( _ myType = iota testvar // testvar 将是 myType 类型 )
在某些时候,结构可能会改变,而你可能会错过这一点。因此,这可能会导致很难找到 bug。
// BAD return res, json.Unmarshal(b, &res) // GOOD err := json.Unmarshal(b, &res) return res, err
type Point struct { X, Y float64 _ struct{} // to prevent unkeyed literals}
对于 Point {X:1,Y:1} 都可以,但是对于 Point {1,1} 则会出现编译错误:
./file.go:1:11: too few values in Point literal
当在你所有的结构体中添加了 _ struct{} 后,使用 go vet 命令进行检查,(原来声明的方式)就会提示没有足够的参数。
type Point struct { _ <0>func() // unexported, zero-width non-comparable field X, Y float64 }
用 http.HandlerFunc 你仅需要一个 func,http.Handler 需要一个类型。
这可以提高代码可读性并明确函数结束时调用了什么。
用 json:"id,string" 代替
type Request struct { ID int64 `json:"id,string"`}
b := a<:0> for _, x := range a { if f(x) { b = append(b, x) } }
res, _ := client.Do(req) io.Copy(ioutil.Discard, res.Body) defer res.Body.Close()
ticker := time.NewTicker(1 * time.Second) defer ticker.Stop()
func (entry Entry) MarshalJSON() (<>byte, error) { buffer := bytes.NewBufferString("{") first := true for key, value := range entry { jsonValue, err := json.Marshal(value) if err != nil { return nil, err } if !first { buffer.WriteString(",") } first = false buffer.WriteString(key + ":" + string(jsonValue)) } buffer.WriteString("}") return buffer.Bytes(), nil }
// noescape hides a pointer from escape analysis. noescape is // the identity function but escape analysis doesn't think the // output depends on the input. noescape is inlined and currently // compiles down to zero instructions. //go:nosplit func noescape(p unsafe.Pointer) unsafe.Pointer { x := uintptr(p) return unsafe.Pointer(x ^ 0) }
for k := range m { delete(m, k) }
m = make(mapint)
func TestSomething(t *testing.T) { if testing.Short() { t.Skip("skipping test in short mode.") } }
if runtime.GOARM == "arm" { t.Skip("this doesn't work under ARM") }
go func() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGQUIT) buf := make(<>byte, 1<<20) for { <-sigs stacklen := runtime.Stack(buf, true) log.Printf("=== received SIGQUIT ===\n*** goroutine dump...\n%s\n*** end\n" , buf<:stacklen>) } }()
var hits struct { sync.Mutex n int } hits.Lock() hits.n++ hits.Unlock()
垃圾回收就是对程序中不再使用的内存资源进行自动回收的操作。
造成引用对象丢失的条件:
一个黑色的节点A新增了指向白色节点C的引用,并且白色节点C没有除了A之外的其他灰色节点的引用,或者存在但是在GC过程中被删除了。以上两个条件需要同时满足:满足条件1时说明节点A已扫描完毕,A指向C的引用无法再被扫描到;满足条件2时说明白色节点C无其他灰色节点的引用了,即扫描结束后会被忽略 。
写屏障破坏两个条件其一即可
满足强三色不变性:黑色节点不允许引用白色节点 当黑色节点新增了白色节点的引用时,将对应的白色节点改为灰色
满足弱三色不变性:黑色节点允许引用白色节点,但是该白色节点有其他灰色节点间接的引用(确保不会被遗漏) 当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色
协程的深入剖析
CSP 模型是“以通信的方式来共享内存”,不同于传统的多线程通过共享内存来通信。用于描述两个独立的并发实体通过共享的通讯 channel (管道)进行通信的并发模型。
M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。
type hchan struct { qcount uint // 队列中的总元素个数 dataqsiz uint // 环形队列大小,即可存放元素的个数 buf unsafe.Pointer // 环形队列指针 elemsize uint16 //每个元素的大小 closed uint32 //标识关闭状态 elemtype *_type // 元素类型 sendx uint // 发送索引,元素写入时存放到队列中的位置 recvx uint // 接收索引,元素从队列的该位置读出 recvq waitq // 等待读消息的goroutine队列 sendq waitq // 等待写消息的goroutine队列 lock mutex //互斥锁,chan不允许并发读写}
向 channel 写数据:
若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。
若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。
若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。
从 channel 读数据
若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。
如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。
如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。
将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒。
关闭 channel
1.关闭 channel 时会将 recvq 中的 G 全部唤醒,本该写入 G 的数据位置为 nil。将 sendq 中的 G 全部唤醒,但是这些 G 会 panic。
panic 出现的场景还有:
// 无缓冲的channel由于没有缓冲发送和接收需要同步ch := make(chan int) //有缓冲channel不要求发送和接收操作同步ch := make(chan int, 2)
channel 无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据;channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。
Context(上下文)是Golang应用开发常用的并发控制技术 ,它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。Context 是并发安全的,主要是用于控制多个协程之间的协作、取消操作。
Context 只定义了接口,凡是实现该接口的类都可称为是一种 context。
并发控制神器之Context
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{}}
并发控制,同步原语 sync 包
资源竞争,就是在程序中,同一块内存同时被多个 goroutine 访问。我们使用 go build、go run、go test 命令时,添加 -race 标识可以检查代码中是否存在资源竞争。
解决这个问题,我们可以给资源进行加锁,让其在同一时刻只能被一个协程来操作。
面试官问我go逃逸场景有哪些,我???
「逃逸分析」就是程序运行时内存的分配位置(栈或堆),是由编译器来确定的。堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
逃逸场景:
Go 中 Goroutine 可以通过 Channel 进行安全读写共享变量。
用new还是make?到底该如何选择?
首先Go的JSON 标准库对 nil slice 和 空 slice 的处理是不一致。
并发掌握,goroutine和channel声明与使用!
****协程: 协程是一种用户态的轻量级线程,协程的调度完全是由用户来控制的。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配。
Channel 可以理解是一个先进先出的队列,通过管道进行通信,发送一个数据到Channel和从Channel接收一个数据都是原子性的。不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。
主动触发(手动触发),通过调用 runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕。
被动触发,分为两种方式:
Channel是异步进行的, channel存在3种状态:
操作 | 一个零值nil通道 | 一个非零值但已关闭的通道 | 一个非零值且尚未关闭的通道 |
关闭 | 产生恐慌 | 产生恐慌 | 成功关闭 |
发送数据 | 永久阻塞 | 产生恐慌 | 阻塞或者成功发送 |
接收数据 | 永久阻塞 | 永不阻塞 | 阻塞或者成功接收 |
使用sync.WaitGroup。WaitGroup,就是用来等待一组操作完成的。WaitGroup内部实现了一个计数器,用来记录未完成的操作个数。Add()用来添加计数;Done()用来在操作结束时调用,使计数减一;Wait()用来等待所有的操作结束,即计数变为0,该函数会在计数不为0时等待,在计数为0时立即返回。
slice 实现原理
在使用 append 向 slice 追加元素时,若 slice 空间不足则会发生扩容,扩容会重新分配一块更大的内存,将原 slice 拷贝到新 slice ,然后返回新 slice。扩容后再将数据追加进去。
扩容操作只对容量,扩容后的 slice 长度不变,容量变化规则如下:
Go中map如果要实现顺序读取的话,可以先把map中的key,通过sort包排序。
究竟在什么情况下才使用指针?
参数传递中,值、引用及指针之间的区别!
方法的接收者:
但是接口的实现,值类型接收者和指针类型接收者不一样:
通常我们使用指针作为方法的接收者的理由:
Goroutine 需要维护执行用户代码的上下文信息,在运行过程中需要消耗一定的内存来保存这类信息,如果一个程序持续不断地产生新的 goroutine,且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象。
可以通过Go自带的工具pprof或者使用Gops去检测诊断当前在系统上运行的Go进程的占用的资源。
Go中两个Nil可能不相等。
接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。
两个接口值比较时,会先比较 T,再比较 V。接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。
func main() { var p *int = nil var i interface{} = p fmt.Println(i == p) // true fmt.Println(p == nil) // true fmt.Println(i == nil) // false}
CPU 访问内存时,并不是逐个字节访问,而是以字长(word size)为单位访问。比如 32 位的 CPU ,字长为 4 字节,那么 CPU 访问内存的单位也是 4 字节。
CPU 始终以字长访问内存,如果不进行内存对齐,很可能增加 CPU 访问内存的次数,例如:
变量 a、b 各占据 3 字节的空间,内存对齐后,a、b 占据 4 字节空间,CPU 读取 b 变量的值只需要进行一次内存访问。如果不进行内存对齐,CPU 读取 b 变量的值需要进行 2 次内存访问。第一次访问得到 b 变量的第 1 个字节,第二次访问得到 b 变量的后两个字节。
也可以看到,内存对齐对实现变量的原子性操作也是有好处的,每次内存访问是原子的,如果变量的大小不超过字长,那么内存对齐后,对该变量的访问就是原子的,这个特性在并发场景下至关重要。
简言之:合理的内存对齐可以提高内存读写的性能,并且便于实现变量操作的原子性。
reflect.TypeOf(a).Kind() == reflect.TypeOf(b).Kind()
reflect.DeepEqual(a, b interface{})
reflect.ValueOf(a).Elem().Set(reflect.ValueOf(b))
package mainimport "fmt"type student struct { id int32 name string}func main() { a := &student{id: 1, name: "微客鸟窝"} fmt.Printf("a=%v \n", a) // a=&{1 微客鸟窝} fmt.Printf("a=%+v \n", a) // a=&{id:1 name:微客鸟窝} fmt.Printf("a=%#v \n", a) // a=&main.student{id:1, name:"微客鸟窝"}}
Go语言的字符有以下两种:
package mainimport "fmt"func main() { var str = "hello 你好" //思考下 len(str) 的长度是多少? //golang中string底层是通过byte数组实现的,直接求len 实际是在按字节长度计算 //所以一个汉字占3个字节算了3个长度 fmt.Println("len(str):", len(str)) // len(str): 12 //通过rune类型处理unicode字符 fmt.Println("rune:", len(<>rune(str))) //rune: 8}
可以使用 unsafe.Sizeof 计算出一个数据类型实例需要占用的字节数:
package mainimport ( "fmt" "unsafe")func main() { fmt.Println(unsafe.Sizeof(struct{}{})) //0}
空结构体 struct{} 实例不占据任何的内存空间。
因为空结构体不占据内存空间,因此被广泛作为各种场景下的占位符使用。
type Set mapstruct{}func (s Set) Has(key string) bool { _, ok := s return ok}func (s Set) Add(key string) { s = struct{}{}}func (s Set) Delete(key string) { delete(s, key)}func main() { s := make(Set) s.Add("Tom") s.Add("Sam") fmt.Println(s.Has("Tom")) fmt.Println(s.Has("Jack"))}
不发送数据的信道(channel)
使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。
func worker(ch chan struct{}) { <-ch fmt.Println("do something") close(ch)}func main() { ch := make(chan struct{}) go worker(ch) ch <- struct{}{}}
结构体只包含方法,不包含任何的字段
type Door struct{}func (d Door) Open() { fmt.Println("Open the door")}func (d Door) Close() { fmt.Println("Close the door")}