Gopher2020大会已经结束了几天,圈内大牛的分享可谓干货满满,分享内容涉及到诸多的业务、框架、理念等,本文系会后粗略整理,主要是将一些干货内容总结、分类;本文内容不涉及业务、框架以及设计理念,整理重点在于Go的代码技巧;这些技巧并非准则,而是由一个个小tips组成,如有疏漏或错误的地方,烦请看官指正。
Functional Options
这个主题左耳朵耗子(陈皓)和毛剑都有讲到过。毛剑老师对此的总结非常精炼:从代码层面可以明确区分接口的必要参数和可选参数。以陈皓老师的代码为例:
type Server struct {
Addr string
Port int
Protocol string
Timeout time.Duration
MaxConns int
TLS *tls.Config
}
func NewServer(addr string, port int) (*Server, error) {}
func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {}
func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {}
func NewTLSServerWithMaxAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config)(*Server, error) {}
对于Server
而言,必要参数为Addr
和Port
,其他均为可选参数,为此需要为众多可选参数写出各种可能组合的接口,代码冗余会相当严重,一旦后期扩展更多的可选参数,那么接口的数量将会是灾难,同时也无法明确区分哪些是可选参数,在使用Functional Options后,接口可以简化为一个。
type Option func (*Server)
func Protocol(proto string) Option {
return func(s *Server) {
s.Protocol = proto
}
}
func Timeout(timeout time.Duration) Option {
return func(s *Server) {
s.Timeout = timeout
}
}
func MaxConns(maxconns int) Option {
return func(s *Server) {
s.MaxConns = maxconns
}
}
func TLS(tls *tls.Config) Option {
return func(s *Server) {
s.TLS = tls
}
}
func NewServer(addr string, port int, options ...func(*Server)) (*Server, error) {}
简化之后接收数量骤减为一个,同时明确了只有Addr
和Port
是必要参数,其他均为可选参数。“代码即文档”这句话在Functional Options得到最好的体现。
Error Handling
同样也是陈皓老师带来的一种代码简化技巧:
func Parse(reader io.Reader) (*Point, error) {
var point Point
if err := binary.Read(reader, binary.BigEndian, &point.Longitude); err != nil {
return nil, error
}
if err := binary.Read(reader, binary.BigEndian, &point.Latitude); err != nil {
return nil, error
}
if err := binary.Read(reader, binary.BigEndian, &point.Distance); err != nil {
return nil, error
}
if err := binary.Read(reader, binary.BigEndian, &point.EleationLoss); err != nil {
return nil, error
}
return &point, nil
}
想必很多同学都有以上代码的经历,error的处理冗长累赘,读写相当累人。以上代码可以优化成:
func Parse(read io.Reader) (*Point, error) {
var point Point
var err error
read := func (data interface{}) {
if err != nil {
return
}
err = binary.Read(read, binary.BigEndian, data)
}
read(&point.Longitude)
read(&point.Latitude)
read(&point.Distance)
read(&point.EleationLoss)
if err != nil {
return nil ,err
}
return &point, nil
}
这种错误处理办法我曾在gouxp中也使用过。
Deep Comparison
在各种复杂业务需求里面,总会遇到各式对象之间需要比较是否相等,Go中没有C++的操作符重载,那么如何实现对象之间的比较呢?答案是Deep Comparison,本质是反射对象,逐一比较对象类型的所有字段。
type data struct {
num int
checks [10]func() bool
doit func() bool
m map[string]string
bytes []byte
}
func main() {
v1 := data{}
v2 := data{}
fmt.Println("v1 == v2:", reflect.DeepEqual(v1, v2))
m1 := map[string]string{"one": "a", "two": "b"}
m2 := map[string]string{"two": "b", "one": "a"}
fmt.Println("m1 == m2:", reflect.DeepEqual(m1, m2))
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
fmt.Println("s1 == s2:", reflect.DeepEqual(s1, s2))
}
当然,使用reflect.DeepEqual
也是需要谨慎考虑的,毕竟反射很慢。
Interface Partterns
严格来说,这个issue是我根据陈皓老师的内容得出的一点启发,同时也是我在gouxp中的关于接口的小技巧实践。
type Conn interface {
}
func (conn *Conn) Close(err error) {}
Conn
是一个TCP连接的抽象,一条网络连接在Client和Server端都会存在一个Conn
对象,如果Client Conn和Server Conn的关闭逻辑有区别,那该如何实现Close函数呢?是独立写ClientClose、ServerClose还是将Conn
进一步抽象为两端分别使用的interface?
在C++里面,使用多态重写即可解决,那么在Go中呢?
type ConnCloser interface {
close(err error)
}
type Conn interface {
ConnCloser
}
func (conn *Conn) Close(err error) {
conn.close(err)
}
客户端和服务端各自实现ConnCloser
即可实现逻辑分离。
Avoid string to byte conversion
众所周知,字符串到byte的直接转换是很昂贵的,所以代码中要尽量避免string与byte之间的直接转换。writer.Write(byteData)
要明显好于writer.Write([]byte("HelloWorld"))
,有兴趣的同学可以跑个Benchmark看看效果。
Use StringBuilder and StringBuffer
字符串的拼接与内存块直接写入的性能区别相差非常大,但凡涉及到字符串的拼接,请使用StringBuilder
或StringBuffer
。
Avoid Link Node When Use sync.Pool
有这样的代码:
type message struct {
childMsg []*message
}
var msgPool = sync.Pool{New: func() interface{} {
return &message{make([]*message, 0, 8)}
}}
func NewMsg(parent *message) *message {
ret := msgPool.Get().(*messa(ge)
if parent != nil && len(parent.childMsg) < 8 {
parent.childMsg = appen(parent.childMsg, ret)
}
return ret
}
func RecycleMessage(msg *message) {
msg.childMsg = msg.childMsg[:0]
msgPool.Put(msg)
}
从上面可以看出,相当混乱的引用关系导致sync.Pool无法成功回收进而导致内存泄漏,因此在sync.Pool的使用上要注意可能存在父子关系或其他复杂引用关系的对象。
Channel Pipeline
假设有这样的需求:对一组数值逐个进行平方运算,然后再逐个求和,该如何实现?仔细分析需求,有两个计算阶段是先平方再求和,那么所需数据如何传递呢?函数参数吗?如何平方或求和相当耗时应该怎么处理呢?
从Go的官方博客上可以找到关于Channel Pipeline的概念:
- Receive values from upstream via inbound channels
- Perform some function on that data, usually producing new values
- Send values downstream via outbound channels
本质上就是阶段和数据的拆分,使用chan作为数据中轴;这样每个阶段有独立的实现逻辑互不影响,各个阶段可以并发实现,输入与输出均为chan,典型的生产者消费者场景。
func echo(nums []int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func sum(in <-chan int) <-chan int {
out := make(chan int)
go func() {
var sum int
for n := range in {
sum += n
}
out <- sum
close(out)
}()
return out
}
func main() {
var nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for n := range sum(sq(echo(nums))) {
fmt.Println(n)
}
}