go语言编程模式总结

面向对象最佳实践

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

type Shape interface {
    Sides() int
    Area() int
}
type Square struct {
    len int
}
func (s* Square) Sides() int {
    return 4
}
func main() {
    s := Square{len: 5}
    fmt.Printf("%d\n",s.Sides())
}

Square 并没有实现 Shape 接口的所有方法,程序虽然可以跑通,但是这样的编程方式并不严谨,如果我们需要强制实现接口的所有方法,那该怎么办呢?

在 Go 语言编程圈里,有一个比较标准的做法:

1
2

var _ Shape = (*Square)(nil)

声明一个 _ 变量(没人用)会把一个 nil 的空指针从 Square 转成 Shape,这样,如果没有实现完相关的接口方法,编译器就会报错:

这样就做到了强验证的方法。

时间性能问题

Go 语言是一个高性能的语言,但并不是说这样我们就不用关心性能了,我们还是需要关心的。下面我给你提供一份在编程方面和性能相关的提示。

  1. 用 Itoa 而不是 sprintf
  2. 避免 String 转 []byte
  3. for-loop 对某个slice使用 append,请扩容到尾,避免重新分配内存,浪费内存
  4. 用stringBuild 拼接字符串,效率比 + 或者 += 高
  5. 使用 协程,用sync.WaitGroup 来同步分片操作。
  6. 避免在热代码进行内存分配,导致gc很忙,尽量用 sync.Pool 重用对象
  7. lock-free 的操作,避免使用 mutex,尽量使用 sync/atomic包
  8. 使用I/O缓冲, I/O 是个非常慢的操作,使用 bufio.NewWrite()bufio.NewReader 可以带来更高性能。
  9. for-loop 里面固定的正则表达式,使用 regexp.Compile 编译表达式,性能可以提高。

异常处理

error 实现 Cause 接口,这样就可以 获取根因。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

import "github.com/pkg/errors"

//错误包装
if err != nil {
    return errors.Wrap(err, "read failed")
}

// Cause接口
switch err := errors.Cause(err).(type) {
case *MyError:
    // handle specifically
default:
    // unknown error
}

Functional Options 编程模式

这是一个函数式编程的应用案例,编程技巧也很好,是目前 Go 语言中最流行的一种编程模式。‘

1
2
3
4
5
6
7
8
9

type Server struct {
    Addr     string
    Port     int
    Protocol string
    Timeout  time.Duration
    MaxConns int
    TLS      *tls.Config
}

针对这样的对象配置,我们需要有多种不同的创建不同配置 Server 的函数签名,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16

func NewDefaultServer(addr string, port int) (*Server, error) {
  return &Server{addr, port, "tcp", 30 * time.Second, 100, nil}, nil
}

func NewTLSServer(addr string, port int, tls *tls.Config) (*Server, error) {
  return &Server{addr, port, "tcp", 30 * time.Second, 100, tls}, nil
}

func NewServerWithTimeout(addr string, port int, timeout time.Duration) (*Server, error) {
  return &Server{addr, port, "tcp", timeout, 100, nil}, nil
}

func NewTLSServerWithMaxConnAndTimeout(addr string, port int, maxconns int, timeout time.Duration, tls *tls.Config) (*Server, error) {
  return &Server{addr, port, "tcp", 30 * time.Second, maxconns, tls}, nil
}

因为 Go 语言不支持重载函数,所以,你得用不同的函数名来应对不同的配置选项。

我们可以使用 Builder 模式 ,对这个对象添加很多的 withOptions 方法, 然后就可以链式编程。

1
2
3
4
5
6
7
8
9
func (sb *ServerBuilder) WithTLS( tls *tls.Config) *ServerBuilder { sb.Server.TLS = tls return sb}func (sb *ServerBuilder) Build() (Server) { return sb.Server}


sb := ServerBuilder{}
server, err := sb.Create("127.0.0.1", 8080).
  WithProtocol("udp").
  WithMaxConn(1024).
  WithTimeOut(30*time.Second).
  Build()

但是这样自己要写一个 额外的 builder类,另一种比较好的方法就是 funtional Options 可以省掉 builder 这个结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

type Option func(*Server)

func Protocol(p string) Option {
    return func(s *Server) {
        s.Protocol = p
    }
}
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) {

  srv := Server{
    Addr:     addr,
    Port:     port,
    Protocol: "tcp",
    Timeout:  30 * time.Second,
    MaxConns: 1000,
    TLS:      nil,
  }
  for _, option := range options {
    option(&srv)
  }
  //...
  return &srv, nil
}

这种写法的好处:

直觉式的编程;高度的可配置化;很容易维护和扩展;自文档;新来的人很容易上手;没有什么令人困惑的事(是 nil 还是空)。

Map reduce 模式

其实,在全世界范围内,有大量的程序员都在问 Go 语言官方什么时候在标准库中支持 Map、Reduce。Rob Pike 说,这种东西难写吗?还要我们官方来帮你们写吗?这种代码我多少年前就写过了,但是,我一次都没有用过,我还是喜欢用“For 循环”,我觉得你最好也跟我一起用 “For 循环”。

目前并没有太好的实现和做法。。

修饰器模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

package main

import "fmt"

func decorator(f func(s string)) func(s string) {

    return func(s string) {
        fmt.Println("Started")
        f(s)
        fmt.Println("Done")
    }
}

func Hello(s string) {
    fmt.Println(s)
}

func main() {
        decorator(Hello)("Hello, World!")
}

可以看到,我们动用了一个高阶函数 decorator(),在调用的时候,先把 Hello() 函数传进去,然后会返回一个匿名函数。这个匿名函数中除了运行了自己的代码,也调用了被传入的 Hello() 函数。这个玩法和 Python 的异曲同工,只不过,有些遗憾的是,Go 并不支持像 Python 那样的 @decorator 语法糖。所以,在调用上有些难看。 先看一个简单的 HTTP Server 的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"
)

func WithServerHeader(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Println("--->WithServerHeader()")
        w.Header().Set("Server", "HelloServer v0.0.1")
        h(w, r)
    }
}

func hello(w http.ResponseWriter, r *http.Request) {
    log.Printf("Recieved Request %s from %s\n", r.URL.Path, r.RemoteAddr)
    fmt.Fprintf(w, "Hello, World! "+r.URL.Path)
}

func main() {
    http.HandleFunc("/v1/hello", WithServerHeader(hello))
    http.HandleFunc("/v1/hello", WithServerHeader(WithAuthCookie(hello)))
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

多个修饰器的 Pipeline在使用上,需要对函数一层层地套起来,看上去好像不是很好看,如果需要修饰器比较多的话,代码就会比较难看了。不过,我们可以重构一下。重构时,我们需要先写一个工具函数,用来遍历并调用各个修饰器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14

type HttpHandlerDecorator func(http.HandlerFunc) http.HandlerFunc

func Handler(h http.HandlerFunc, decors ...HttpHandlerDecorator) http.HandlerFunc {
    for i := range decors {
        d := decors[len(decors)-1-i] // iterate in reverse
        h = d(h)
    }
    return h
}


http.HandleFunc("/v4/hello", Handler(hello,
                WithServerHeader, WithBasicAuth, WithDebugLog))