Go编程模式

Go编程模式

注意
本文最后更新于 2023-09-14,文中内容可能已过时。

Go编程模式

原文

切片,接口,时间和性能

当我们复杂一个对象时,这个对象可以是内建数据类型,数组,结构体,map……我们在复制结构体的时候,当我们需要比较两个结构体中的数据是否相同时要使用到反射 reflect.DeepEqual()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
    "fmt"
    "reflect"
)

func main() {

    v1 := data{}
    v2 := data{}
    fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2))
    //prints: v1 == v2: true

    m1 := map[string]string{"one": "a","two": "b"}
    m2 := map[string]string{"two": "b", "one": "a"}
    fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2))
    //prints: m1 == m2: true

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2))
    //prints: s1 == s2: true
}

接口作为参数,调用方法,解藕业务类型(实例对象)和控制逻辑(方法)

这种编程模式在 Go 的标准库有很多的示例,最著名的就是 io.Readioutil.ReadAll 的玩法,其中 io.Read 是一个接口,你需要实现他的一个 Read(p []byte) (n int, err error) 接口方法,只要满足这个规模,就可以被 ioutil.ReadAll 这个方法所使用

 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
type Country struct {
    Name string
}

type City struct {
    Name string
}

type Stringable interface {
    ToString() string
}
func (c Country) ToString() string {
    return "Country = " + c.Name
}
func (c City) ToString() string{
    return "City = " + c.Name
}

func PrintStr(p Stringable) {
    fmt.Println(p.ToString())
}

d1 := Country {"USA"}
d2 := City{"Los Angeles"}
PrintStr(d1)
PrintStr(d2)

var​ _ Shape = (*Square)(nil)

声明一个 _ 变量(没人用),其会把一个 nil 的空指针,从 Square 转成 Shape

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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())
}

在 Go 语言中,你一定要使用 time.Timetime.Duration 两个类型:

  • 在命令行上,flag 通过 time.ParseDuration 支持了 time.Duration
  • Json 中的 encoding/json 中也可以把 time.Time 编码成 RFC 3339 的格式
  • 数据库使用的 database/sql 也支持把 DATATIMETIMESTAMP 类型转成 time.Time
  • YAML 你可以使用 gopkg.in/yaml.v2 也支持 time.Timetime.DurationRFC 3339 格式

如果你要和第三方交互,实在没有办法,也请使用 RFC 3339 的格式。

最后,如果你要做全球化跨时区的应用,你一定要把所有服务器和时间全部使用 UTC 时间

下面是一个在编程方面和性能相关的提示。

  • 如果需要把数字转字符串,使用 strconv.Itoa() 会比 fmt.Sprintf() 要快一倍左右
  • 尽可能地避免把 String 转成 []Byte 。这个转换会导致性能下降。
  • 如果在 for-loop 里对某个 slice 使用 append() 请先把 slice 的容量很扩充到位,这样可以避免内存重新分享以及系统自动按 2 的 N 次方幂进行扩展但又用不到,从而浪费内存。
  • 使用 StringBuffer 或是 StringBuild 来拼接字符串,会比使用 ++= 性能高三到四个数量级。
  • 尽可能的使用并发的 go routine,然后使用 sync.WaitGroup 来同步分片操作
  • 避免在热代码中进行内存分配,这样会导致 gc 很忙。尽可能的使用 sync.Pool 来重用对象。
  • 使用 lock-free 的操作,避免使用 mutex,尽可能使用 sync/Atomic 包。
  • 使用 I/O 缓冲,I/O 是个非常非常慢的操作,使用 bufio.NewWrite()bufio.NewReader() 可以带来更高的性能。
  • 对于在 for-loop 里的固定的正则表达式,一定要使用 regexp.Compile() 编译正则表达式。性能会得升两个数量级。
  • 如果你需要更高性能的协议,你要考虑使用 protobufmsgp 而不是 JSON,因为 JSON 的序列化和反序列化里使用了反射。
  • 你在使用 map 的时候,使用整型的 key 会比字符串的要快,因为整型比较比字符串比较要快。

错误处理

函数式编程进行错误处理

  1. 外部传入 err 进行判定执行
1
2
3
4
5
6
doFunc := func(err error,f func()error){
    if err != nil {
        return err
    }
    err = f()
}
  1. 增添结构体存放 err
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type Reader struct {
  r   io.Reader
  err error
}

func (r *Reader) read(data interface{}) {
  if r.err == nil {
  	r.err = binary.Read(r.r, binary.BigEndian, data)
  }
}

我们需要包装一下错误,而不是干巴巴地把err给返回到上层,我们需要把一些执行的上下文加入。

  1. 可以将错误和信息封装之后返回
1
2
3
4
5
6
7
8
type authorizationError struct {
   operation string
   err error   // original error
}

func (e *authorizationError) Error() string {
   return fmt.Sprintf("authorization failed during %s: %v", e.operation, e.err)
}

更好的方式是通过一种标准的访问方法,这样,我们最好使用一个接口,比如 causer 接口中实现 Cause() 方法来暴露原始错误,以供进一步检查:

1
2
3
4
5
6
type causer interface {
    Cause() error
}
func (e *authorizationError) Cause() error {
    return e.err
}
  1. 第三方库
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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

可配置的模块化加载方法

 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
// 业务对象
type Server struct {
    Addr string
    Port int
    Conf *Config
}
// 配置方法类型
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
    }
}

初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
}

s1, _ := NewServer("localhost", 1024)
s2, _ := NewServer("localhost", 2048, Protocol("udp"))
s3, _ := NewServer("0.0.0.0", 8080, Timeout(300*time.Second), MaxConns(1000))
  • 直觉式的编程
  • 高度的可配置化
  • 很容易维护和扩展
  • 自文档
  • 对于新来的人很容易上手
  • 没有什么令人困惑的事(是 nil 还是空)

MAP-REDUCE

1
2
3
4
5
6
7
func MapStrToStr(arr []string, fn func(s string) string) []string {
    var newArray = []string{}
    for _, it := range arr {
        newArray = append(newArray, fn(it))
    }
    return newArray
}
1
2
3
4
5
6
7
func Reduce(arr []string, fn func(s string) int) int {
    sum := 0
    for _, it := range arr {
        sum += fn(it)
    }
    return sum
}
1
2
3
4
5
6
7
8
9
func Filter(arr []int, fn func(n int) bool) []int {
    var newArray = []int{}
    for _, it := range arr {
        if fn(it) {
            newArray = append(newArray, it)
        }
    }
    return newArray
}

相关内容