ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Go编程的一些最佳实践

2026/9/11 20:29:42 拓冰建站 浏览量
Go编程的一些最佳实践

文章目录

    • 一、切片、接口、时间和性能
      • 1. Slice 容量
      • 2. 深度比较
      • 3. 接口编程
      • 4. 接口完整性检查
      • 5. 时间
      • 6. 性能优化
    • 二、错误处理
      • 1. 资源清理
      • 2. Error Check Hell
      • 3. 包装错误
    • 三、Functional Options
      • 1. 配置选项问题
      • 2. 解法1:配置对象方案
      • 3. 解法2:Builder 模式
      • 4. 解法3:Functional Options
    • 四、委托和反转控制
      • 1. 嵌入结构多态
        • 1.1 嵌入结构
        • 1.2 方法重写
        • 1.3 嵌入结构多态
      • 2. 反转控制
      • 3. 反转依赖
    • 五、Map-Reduce
      • 1. 示例
        • 1.1 基本示例
          • 1.1.1 Map示例
        • 1.1.2 Reduce示例
        • 1.1.3 Filter示例
        • 1.2 业务示例
      • 2. 泛型 Map-Reduce
        • 2.1 简单版 Generic Map
        • 2.2 健壮版 Generic Map-Reduce
          • 2.2.1 Map 示例
          • 2.2.2 Reduce 示例
          • 2.2.3 Filter 示例

一、切片、接口、时间和性能

1. Slice 容量

package mainimport ("bytes""fmt"
)func main() {path := []byte("AAAA/BBBBBBBBB")sepIndex := bytes.IndexByte(path, '/')dir1 := path[:sepIndex]dir2 := path[sepIndex+1:]fmt.Println("dir1 =>", string(dir1), len(dir1), cap(dir1)) //prints: dir1 => AAAA 4 14fmt.Println("dir2 =>", string(dir2), len(dir2), cap(dir2)) //prints: dir2 => BBBBBBBBB 9 9dir1 = append(dir1, "suffix"...) //As a special case, it is legal to append a string to a byte slicefmt.Println("dir1 =>", string(dir1), len(dir1), cap(dir1)) //prints: dir1 => AAAAsuffix 10 14fmt.Println("dir2 =>", string(dir2), len(dir2), cap(dir2)) //prints: dir2 => uffixBBBB 9 9
}

这个例子中,dir1 和 dir2 共享内存,虽然 dir1 有一个 append() 操作,但是因为 cap 足够,于是数据扩展到了dir2 的空间。

要解决这个问题,我们只需要修改一行代码:

dir1 := path[:sepIndex]

改为:

dir1 := path[:sepIndex:sepIndex]

新的代码使用了 Full Slice Expression,最后一个参数叫“Limited Capacity”,于是,后续的 append() 操作会导致重新分配内存。

2. 深度比较

在复制结构体的时候,如果我们需要比较两个结构体中的数据是否相同,就要使用深度比较,而不只是简单地做浅度比较。这里需要使用到反射 reflect.DeepEqual()。

import ("fmt""reflect"
)func main() {v1 := data{}v2 := data{}fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2))//prints: v1 == v2: truem1 := 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: trues1 := []int{1, 2, 3}s2 := []int{1, 2, 3}fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2))//prints: s1 == s2: true
}

3. 接口编程

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)

在这段代码中,我们使用了一个叫Stringable 的接口,我们用这个接口把“业务类型” Country 和 City 和“控制逻辑” Print() 给解耦了。于是,只要实现了Stringable 接口,都可以传给 PrintStr() 来使用。

这种编程模式在 Go 的标准库有很多的示例,最著名的就是 io.Read 和 ioutil.ReadAll 的玩法,其中 io.Read 是一个接口,你需要实现它的一个 Read(p []byte) (n int, err error) 接口方法,只要满足这个规则,就可以被 ioutil.ReadAll这个方法所使用。这就是面向对象编程方法的黄金法则——“Program to an interface not an implementation”。

4. 接口完整性检查

Go 语言的编译器并没有严格检查一个对象是否实现了某接口所有的接口方法。在 Go 语言编程圈里,有一个比较标准的做法:

var _ Shape = (*Square)(nil)type Shape interface {Sides() intArea() 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())
}

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

cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)

5. 时间

在 Go 语言中,你一定要使用 time.Time 和 time.Duration 这两个类型。

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

如果你要和第三方交互,实在没有办法,也请使用 RFC 3339 的格式。最后,如果你要做全球化跨时区的应用,一定要把所有服务器和时间全部使用 UTC 时间

6. 性能优化

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

二、错误处理

Go 语言中,如果一个函数返回了多个不同类型的 error,也可以使用下面这样的方式:

if err != nil {switch err.(type) {case *json.SyntaxError:...case *ZeroDivisionError:...case *NullPointerError:...default:...}
}

Go 语言的错误处理的方式,本质上是返回值检查,但是它也兼顾了异常的一些好处——对错误的扩展。

1. 资源清理

出错后是需要做资源清理的,不同的编程语言有不同的资源清理的编程模式。

  • C 语言:使用的是 goto fail; 的方式到一个集中的地方进行清理。
  • C++ 语言:一般来说使用 RAII 模式,通过面向对象的代理模式,把需要清理的资源交给一个代理类,然后再析构函数来解决。
  • Java 语言:可以在 finally 语句块里进行清理。
  • Go 语言:使用 defer 关键词进行清理。

2. Error Check Hell

Go 语言的 bufio.Scanner() 使用示例:

scanner := bufio.NewScanner(input)for scanner.Scan() {token := scanner.Text()// process token
}if err := scanner.Err(); err != nil {// process the error
}

可以看到,scanner在操作底层的 I/O 的时候,那个 for-loop 中没有任何的 if err !=nil 的情况,退出循环后有一个 scanner.Err() 的检查。

模仿它,首先,定义一个结构体和一个成员函数:

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

然后可以如下使用:

func parse(input io.Reader) (*Point, error) {var p Pointr := Reader{r: input}r.read(&p.Longitude)r.read(&p.Latitude)r.read(&p.Distance)r.read(&p.ElevationGain)r.read(&p.ElevationLoss)if r.err != nil {return nil, r.err}return &p, nil
}

基于上述实现,将其改为“流式接口 Fluent Interface”调用:

package mainimport ("bytes""encoding/binary""fmt"
)// 长度不够,少一个Weight
var b = []byte {0x48, 0x61, 0x6f, 0x20, 0x43, 0x68, 0x65, 0x6e, 0x00, 0x00, 0x2c} 
var r = bytes.NewReader(b)type Person struct {Name [10]byteAge uint8Weight uint8err error
}
func (p *Person) read(data interface{}) {if p.err == nil {p.err = binary.Read(r, binary.BigEndian, data)}
}func (p *Person) ReadName() *Person {p.read(&p.Name) return p
}
func (p *Person) ReadAge() *Person {p.read(&p.Age) return p
}
func (p *Person) ReadWeight() *Person {p.read(&p.Weight) return p
}
func (p *Person) Print() *Person {if p.err == nil {fmt.Printf("Name=%s, Age=%d, Weight=%d\n",p.Name, p.Age, p.Weight)}return p
}func main() {   p := Person{}p.ReadName().ReadAge().ReadWeight().Print()  // Fluent stylefmt.Println(p.err)  // EOF 错误
}

注意:这种错误处理方式有局限性,它只能在对于同一个业务对象的不断操作下可以简化错误处理,如果是多个业务对象,还是得需要各种 if err != nil的方式。

3. 包装错误

在 Go 语言的开发者中,更为普遍的做法是将错误包装在另一个错误中,同时保留原始内容:

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

同时最好使用一个接口,比如 causer 接口中实现 Cause() 方法来暴露原始错误,以供进一步检查:

type causer interface {Cause() error
}func (e *authorizationError) Cause() error {return e.err
}

有一个第三方的错误库,无论到哪儿都能看到它的存在,所以,这个基本上来说就是事实上的标准了。代码示例如下:

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. 配置选项问题

需要对一个对象(或是业务实体)进行相关的配置。比如下面这个业务实体:

type Server struct {Addr     stringPort     intProtocol stringTimeout  time.DurationMaxConns intTLS      *tls.Config
}

在这个 Server 对象中,我们可以看到:

  • 要有侦听的 IP 地址 Addr 和端口号 Port ,这两个配置选项是必填的(当然,IP 地址和端口号都可以有默认值,不过这里我们用于举例,所以是没有默认值,而且不能为空,需要是必填的)。
  • 还有协议 Protocol 、 Timeout 和MaxConns 字段,这几个字段是不能为空的,但是有默认值的,比如,协议是 TCP,超时30秒 和 最大链接数1024个。
  • 还有一个 TLS ,这个是安全链接,需要配置相关的证书和私钥。这个是可以为空的。

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

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 语言不支持重载函数,所以得用不同的函数名来应对不同的配置选项。

2. 解法1:配置对象方案

最常见的方式是使用一个配置对象,如下所示:

type Config struct {Protocol stringTimeout  time.DurationMaxconns intTLS      *tls.Config
}

把那些非必输的选项都移到一个结构体里,这样一来, Server 对象就会变成:

type Server struct {Addr stringPort intConf *Config
}

于是,我们就只需要一个 NewServer() 的函数了,在使用前需要构造 Config 对象。

func NewServer(addr string, port int, conf *Config) (*Server, error) {//...
}//Using the default configuratrion
srv1, _ := NewServer("localhost", 9000, nil) conf := ServerConfig{Protocol:"tcp", Timeout: 60*time.Duration}
srv2, _ := NewServer("locahost", 9000, &conf)

这段代码算是不错了,但是也可以看到其中不太好的一点,那就是Config 并不是必需的,所以,你需要判断是否是 nil 或是 Empty—— Config{}会让我们的代码感觉不太干净。

3. 解法2:Builder 模式

使用 Builder 模式可以把刚刚的代码改写成下面的样子(注:下面的代码没有考虑出错处理)

//使用一个builder类来做包装
type ServerBuilder struct {Server
}func (sb *ServerBuilder) Create(addr string, port int) *ServerBuilder {sb.Server.Addr = addrsb.Server.Port = port//其它代码设置其它成员的默认值return sb
}func (sb *ServerBuilder) WithProtocol(protocol string) *ServerBuilder {sb.Server.Protocol = protocol return sb
}func (sb *ServerBuilder) WithMaxConn( maxconn int) *ServerBuilder {sb.Server.MaxConns = maxconnreturn sb
}func (sb *ServerBuilder) WithTimeOut( timeout time.Duration) *ServerBuilder {sb.Server.Timeout = timeoutreturn sb
}func (sb *ServerBuilder) WithTLS( tls *tls.Config) *ServerBuilder {sb.Server.TLS = tlsreturn 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()

这种方式也很清楚,不需要额外的 Config 类,使用链式的函数调用的方式来构造一个对象,只需要多加一个 Builder 类。这个 Builder 类似乎有点多余,我们似乎可以直接在Server 上进行这样的 Builder 构造,的确是这样的。但是,在处理错误的时候可能就有点麻烦,不如一个包装类更好一些。

4. 解法3:Functional Options

首先定义一个函数类型:

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}
}

这组代码传入一个参数,然后返回一个函数,返回的这个函数会设置自己的 Server 参数。例如,当我们调用其中的一个函数 MaxConns(30) 时,其返回值是一个 func(s* Server) { s.MaxConns = 30 } 的函数。

现在再定义一个 NewServer()的函数,其中,有一个可变参数 options ,它可以传入多个上面的函数,然后使用一个 for-loop 来设置我们的 Server 对象。

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))

这不但解决了“使用 Config 对象方式的需要有一个 config 参数,但在不需要的时候,是放 nil 还是放 Config{}”的选择困难问题,也不需要引用一个 Builder 的控制对象,直接使用函数式编程,在代码阅读上也很优雅。

Functional Options 这种方式的6个好处:

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

四、委托和反转控制

控制反转(Inversion of Control,loC )是一种软件设计的方法,它的主要思想是把控制逻辑与业务逻辑分开,不要在业务逻辑里写控制逻辑,因为这样会让控制逻辑依赖于业务逻辑,而是反过来,让业务逻辑依赖控制逻辑。

1. 嵌入结构多态

1.1 嵌入结构

在 Go 语言中,可以把一个结构体嵌到另一个结构体中,如下所示:

type Widget struct {X, Y int
}
type Label struct {Widget        // Embedding (delegation)Text   string // Aggregation
}type Button struct {Label // Embedding (delegation)
}type ListBox struct {Widget          // Embedding (delegation)Texts  []string // AggregationIndex  int      // Aggregation
}

可以这样使用:

label := Label{Widget{10, 10}, "State:"}label.X = 11
label.Y = 12

如果在Label 结构体里出现了重名,就需要解决重名问题,例如,如果成员 X 重名,我们就要用 label.X表明是自己的X ,用 label.Wedget.X 表明是嵌入过来的。

1.2 方法重写

我们需要两个接口:用 Painter 把组件画出来;Clicker 用于表明点击事件。对于 Lable 来说,只有 Painter ,没有Clicker;对于 Button 和 ListBox来说,Painter 和Clicker都有。

type Painter interface {Paint()
}type Clicker interface {Click()
}func (label Label) Paint() {fmt.Printf("%p:Label.Paint(%q)\n", &label, label.Text)
}//因为这个接口可以通过 Label 的嵌入带到新的结构体,
//所以,可以在 Button 中重载这个接口方法
func (button Button) Paint() { // Overridefmt.Printf("Button.Paint(%s)\n", button.Text)
}
func (button Button) Click() {fmt.Printf("Button.Click(%s)\n", button.Text)
}func (listBox ListBox) Paint() {fmt.Printf("ListBox.Paint(%q)\n", listBox.Texts)
}
func (listBox ListBox) Click() {fmt.Printf("ListBox.Click(%q)\n", listBox.Texts)
}

这里,Button.Paint() 接口可以通过 Label 的嵌入带到新的结构体,如果 Button.Paint() 不实现的话,会调用 Label.Paint() ,所以,在 Button 中声明 Paint() 方法,相当于 Override。

1.3 嵌入结构多态
button1 := Button{Label{Widget{10, 70}, "OK"}}
button2 := NewButton(50, 70, "Cancel")
listBox := ListBox{Widget{10, 40}, []string{"AL", "AK", "AZ", "AR"}, 0}for _, painter := range []Painter{label, listBox, button1, button2} {painter.Paint()
}for _, widget := range []interface{}{label, listBox, button1, button2} {widget.(Painter).Paint()if clicker, ok := widget.(Clicker); ok {clicker.Click()}fmt.Println() // print a empty line 
}

可以使用接口来多态,也可以使用泛型的 interface{} 来多态,但是需要有一个类型转换。

2. 反转控制

有一个存放整数的数据结构,其中实现了 Add()Delete()Contains() 三个操作,前两个是写操作,后一个是读操作。如下所示:

type IntSet struct {data map[int]bool
}
func NewIntSet() IntSet {return IntSet{make(map[int]bool)}
}
func (set *IntSet) Add(x int) {set.data[x] = true
}
func (set *IntSet) Delete(x int) {delete(set.data, x)
}
func (set *IntSet) Contains(x int) bool {return set.data[x]
}

现在想实现一个 Undo 的功能。我们可以再包装一下 IntSet ,变成 UndoableIntSet,代码如下所示:

type UndoableIntSet struct { // Poor styleIntSet    // Embedding (delegation)functions []func()
}func NewUndoableIntSet() UndoableIntSet {return UndoableIntSet{NewIntSet(), nil}
}func (set *UndoableIntSet) Add(x int) { // Overrideif !set.Contains(x) {set.data[x] = trueset.functions = append(set.functions, func() { set.Delete(x) })} else {set.functions = append(set.functions, nil)}
}func (set *UndoableIntSet) Delete(x int) { // Overrideif set.Contains(x) {delete(set.data, x)set.functions = append(set.functions, func() { set.Add(x) })} else {set.functions = append(set.functions, nil)}
}func (set *UndoableIntSet) Undo() error {if len(set.functions) == 0 {return errors.New("No functions to undo")}index := len(set.functions) - 1if function := set.functions[index]; function != nil {function()set.functions[index] = nil // For garbage collection}set.functions = set.functions[:index]return nil
}

代码说明:

  • 在 UndoableIntSet 中嵌入了IntSet ,然后 Override 了 它的 Add()和 Delete() 方法;
  • Contains() 方法没有 Override,所以,就被带到 UndoableInSet 中来了。
  • 在 Override 的 Add()中,记录 Delete 操作;
  • 在 Override 的 Delete() 中,记录 Add 操作;
  • 在新加入的 Undo() 中进行 Undo 操作。

用这样的方式为已有的代码扩展新的功能是一个很好的选择。这样,就可以在重用原有代码功能和新的功能中达到一个平衡。但是,这种方式最大的问题是,Undo 操作其实是一种控制逻辑,并不是业务逻辑,所以,在复用 Undo 这个功能时,是有问题的,因为其中加入了大量跟 IntSet 相关的业务逻辑。

3. 反转依赖

先声明一种函数接口,表示我们的 Undo 控制可以接受的函数签名是什么样的:

type Undo []func()

之后定义 Undo 的控制逻辑:

func (undo *Undo) Add(function func()) {*undo = append(*undo, function)
}func (undo *Undo) Undo() error {functions := *undoif len(functions) == 0 {return errors.New("No functions to undo")}index := len(functions) - 1if function := functions[index]; function != nil {function()functions[index] = nil // For garbage collection}*undo = functions[:index]return nil
}

然后在 IntSet 里嵌入 Undo,接着在 Add() 和 Delete() 里使用刚刚的方法,就可以完成功能了。

type IntSet struct {data map[int]boolundo Undo
}func NewIntSet() IntSet {return IntSet{data: make(map[int]bool)}
}func (set *IntSet) Undo() error {return set.undo.Undo()
}func (set *IntSet) Contains(x int) bool {return set.data[x]
}func (set *IntSet) Add(x int) {if !set.Contains(x) {set.data[x] = trueset.undo.Add(func() { set.Delete(x) })} else {set.undo.Add(nil)}
}func (set *IntSet) Delete(x int) {if set.Contains(x) {delete(set.data, x)set.undo.Add(func() { set.Add(x) })} else {set.undo.Add(nil)}
}

这个就是控制反转,不是由控制逻辑 Undo 来依赖业务逻辑 IntSet,而是由业务逻辑 IntSet 依赖 Undo 。这里依赖的是其实是一个协议,这个协议是一个没有参数的函数数组。可以看到,这样一来 Undo 的代码就可以复用了。

五、Map-Reduce

1. 示例

1.1 基本示例
1.1.1 Map示例
func MapStrToStr(arr []string, fn func(s string) string) []string {var newArray = []string{}for _, it := range arr {newArray = append(newArray, fn(it))}return newArray
}func MapStrToInt(arr []string, fn func(s string) int) []int {var newArray = []int{}for _, it := range arr {newArray = append(newArray, fn(it))}return newArray
}var list = []string{"Hao", "Chen", "MegaEase"}x := MapStrToStr(list, func(s string) string {return strings.ToUpper(s)
})
fmt.Printf("%v\n", x)
//["HAO", "CHEN", "MEGAEASE"]y := MapStrToInt(list, func(s string) int {return len(s)
})
fmt.Printf("%v\n", y)
//[3, 4, 8]

整个 Map 函数的运行逻辑都很相似,函数体都是在遍历第一个参数的数组,然后,调用第二个参数的函数,把它的值组合成另一个数组返回。

1.1.2 Reduce示例
func Reduce(arr []string, fn func(s string) int) int {sum := 0for _, it := range arr {sum += fn(it)}return sum
}var list = []string{"Hao", "Chen", "MegaEase"}x := Reduce(list, func(s string) int {return len(s)
})
fmt.Printf("%v\n", x)
// 15
1.1.3 Filter示例
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
}var intset = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
out := Filter(intset, func(n int) bool {return n%2 == 1
})
fmt.Printf("%v\n", out)out = Filter(intset, func(n int) bool {return n > 5
})
fmt.Printf("%v\n", out)
1.2 业务示例

有一个员工对象和一些数据:

type Employee struct {Name     stringAge      intVacation intSalary   int
}var list = []Employee{{"Hao", 44, 0, 8000},{"Bob", 34, 10, 5000},{"Alice", 23, 5, 9000},{"Jack", 26, 0, 4000},{"Tom", 48, 9, 7500},{"Marry", 29, 0, 6000},{"Mike", 32, 8, 4000},
}

相关的 Reduce、Fitler 函数如下:

func EmployeeCountIf(list []Employee, fn func(e *Employee) bool) int {count := 0for i, _ := range list {if fn(&list[i]) {count += 1}}return count
}func EmployeeFilterIn(list []Employee, fn func(e *Employee) bool) []Employee {var newList []Employeefor i, _ := range list {if fn(&list[i]) {newList = append(newList, list[i])}}return newList
}func EmployeeSumIf(list []Employee, fn func(e *Employee) int) int {var sum = 0for i, _ := range list {sum += fn(&list[i])}return sum
}

EmployeeConutIf 和 EmployeeSumIf 分别用于统计满足某个条件的个数或总数。它们都是 Filter + Reduce 的语义。EmployeeFilterIn 就是按某种条件过滤,就是 Fitler 的语义。

各种自定义的统计示例:

// 1. 统计有多少员工大于 40 岁
old := EmployeeCountIf(list, func(e *Employee) bool {return e.Age > 40
})
fmt.Printf("old people: %d\n", old)
//old people: 2// 2. 统计有多少员工的薪水大于 6000
high_pay := EmployeeCountIf(list, func(e *Employee) bool {return e.Salary > 6000
})
fmt.Printf("High Salary people: %d\n", high_pay)
//High Salary people: 4// 3. 列出有没有休假的员工
no_vacation := EmployeeFilterIn(list, func(e *Employee) bool {return e.Vacation == 0
})
fmt.Printf("People no vacation: %v\n", no_vacation)
//People no vacation: [{Hao 44 0 8000} {Jack 26 0 4000} {Marry 29 0 6000}]// 4. 统计所有员工的薪资总和
total_pay := EmployeeSumIf(list, func(e *Employee) int {return e.Salary
})fmt.Printf("Total Salary: %d\n", total_pay)
//Total Salary: 43500// 5. 统计 30 岁以下员工的薪资总和
younger_pay := EmployeeSumIf(list, func(e *Employee) int {if e.Age < 30 {return e.Salary} return 0
})

2. 泛型 Map-Reduce

2.1 简单版 Generic Map

Go1.18以前的版本实现泛型只能用 interface{} + reflect来完成。

func Map(data interface{}, fn interface{}) []interface{} {vfn := reflect.ValueOf(fn)vdata := reflect.ValueOf(data)result := make([]interface{}, vdata.Len())for i := 0; i < vdata.Len(); i++ {result[i] = vfn.Call([]reflect.Value{vdata.Index(i)})[0].Interface()}return result
}
  • 首先,我们通过 reflect.ValueOf() 获得 interface{} 的值,其中一个是数据 vdata,另一个是函数 vfn。
  • 然后,通过 vfn.Call() 方法调用函数,通过 []refelct.Value{vdata.Index(i)}获得数据。

调用代码:

square := func(x int) int {return x * x
}
nums := []int{1, 2, 3, 4}squared_arr := Map(nums,square)
fmt.Println(squared_arr)
//[1 4 9 16]upcase := func(s string) string {return strings.ToUpper(s)
}
strs := []string{"Hao", "Chen", "MegaEase"}
upstrs := Map(strs, upcase);
fmt.Println(upstrs)
//[HAO CHEN MEGAEASE]

因为反射是运行时的事,所以,如果类型出问题的话,就会有运行时的错误,也就是说代码可以很轻松地编译通过,但是在运行时却会出问题,而且还是 panic 错误。

2.2 健壮版 Generic Map-Reduce

如果要写一个健壮的程序,对于这种用interface{} 的“过度泛型”,就需要我们自己来做类型检查。

2.2.1 Map 示例

一个有类型检查的 Map 代码如下:

func Transform(slice, function interface{}) interface{} {return transform(slice, function, false)
}func TransformInPlace(slice, function interface{}) interface{} {return transform(slice, function, true)
}func transform(slice, function interface{}, inPlace bool) interface{} {//check the `slice` type is SlicesliceInType := reflect.ValueOf(slice)if sliceInType.Kind() != reflect.Slice {panic("transform: not slice")}//check the function signaturefn := reflect.ValueOf(function)elemType := sliceInType.Type().Elem()if !verifyFuncSignature(fn, elemType, nil) {panic("trasform: function must be of type func(" + sliceInType.Type().Elem().String() + ") outputElemType")}sliceOutType := sliceInTypeif !inPlace {sliceOutType = reflect.MakeSlice(reflect.SliceOf(fn.Type().Out(0)), sliceInType.Len(), sliceInType.Len())}for i := 0; i < sliceInType.Len(); i++ {sliceOutType.Index(i).Set(fn.Call([]reflect.Value{sliceInType.Index(i)})[0])}return sliceOutType.Interface()}func verifyFuncSignature(fn reflect.Value, types ...reflect.Type) bool {//Check it is a funcitonif fn.Kind() != reflect.Func {return false}// NumIn() - returns a function type's input parameter count.// NumOut() - returns a function type's output parameter count.if (fn.Type().NumIn() != len(types)-1) || (fn.Type().NumOut() != 1) {return false}// In() - returns the type of a function type's i'th input parameter.for i := 0; i < len(types)-1; i++ {if fn.Type().In(i) != types[i] {return false}}// Out() - returns the type of a function type's i'th output parameter.outType := types[len(types)-1]if outType != nil && fn.Type().Out(0) != outType {return false}return true
}

该代码中的几个要点:

  1. 代码中没有使用 Map 函数,因为和数据结构有含义冲突的问题,所以使用Transform,这个来源于 C++ STL 库中的命名。
  2. 有两个版本的函数,一个是返回一个全新的数组 Transform(),一个是“就地完成” TransformInPlace()。
  3. 在主函数中,用 Kind() 方法检查了数据类型是不是 Slice,函数类型是不是 Func。
  4. 检查函数的参数和返回类型是通过 verifyFuncSignature() 来完成的:NumIn()用来检查函数的“入参”;NumOut() :用来检查函数的“返回值”。
  5. 如果需要新生成一个 Slice,会使用 reflect.MakeSlice() 来完成。

调用方式:

list := []string{"1", "2", "3", "4", "5", "6"}
result := Transform(list, func(a string) string{return a +a +a
})
//{"111","222","333","444","555","666"}list := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
TransformInPlace(list, func (a int) int {return a*3
})
//{3, 6, 9, 12, 15, 18, 21, 24, 27}var list = []Employee{{"Hao", 44, 0, 8000},{"Bob", 34, 10, 5000},{"Alice", 23, 5, 9000},{"Jack", 26, 0, 4000},{"Tom", 48, 9, 7500},
}result := TransformInPlace(list, func(e Employee) Employee {e.Salary += 1000e.Age += 1return e
})
2.2.2 Reduce 示例
func Reduce(slice, pairFunc, zero interface{}) interface{} {sliceInType := reflect.ValueOf(slice)if sliceInType.Kind() != reflect.Slice {panic("reduce: wrong type, not slice")}len := sliceInType.Len()if len == 0 {return zero} else if len == 1 {return sliceInType.Index(0)}elemType := sliceInType.Type().Elem()fn := reflect.ValueOf(pairFunc)if !verifyFuncSignature(fn, elemType, elemType, elemType) {t := elemType.String()panic("reduce: function must be of type func(" + t + ", " + t + ") " + t)}var ins [2]reflect.Valueins[0] = sliceInType.Index(0)ins[1] = sliceInType.Index(1)out := fn.Call(ins[:])[0]for i := 2; i < len; i++ {ins[0] = outins[1] = sliceInType.Index(i)out = fn.Call(ins[:])[0]}return out.Interface()
}
2.2.3 Filter 示例
func Filter(slice, function interface{}) interface{} {result, _ := filter(slice, function, false)return result
}func FilterInPlace(slicePtr, function interface{}) {in := reflect.ValueOf(slicePtr)if in.Kind() != reflect.Ptr {panic("FilterInPlace: wrong type, " +"not a pointer to slice")}_, n := filter(in.Elem().Interface(), function, true)in.Elem().SetLen(n)
}var boolType = reflect.ValueOf(true).Type()func filter(slice, function interface{}, inPlace bool) (interface{}, int) {sliceInType := reflect.ValueOf(slice)if sliceInType.Kind() != reflect.Slice {panic("filter: wrong type, not a slice")}fn := reflect.ValueOf(function)elemType := sliceInType.Type().Elem()if !verifyFuncSignature(fn, elemType, boolType) {panic("filter: function must be of type func(" + elemType.String() + ") bool")}var which []intfor i := 0; i < sliceInType.Len(); i++ {if fn.Call([]reflect.Value{sliceInType.Index(i)})[0].Bool() {which = append(which, i)}}out := sliceInTypeif !inPlace {out = reflect.MakeSlice(sliceInType.Type(), len(which), len(which))}for i := range which {out.Index(i).Set(sliceInType.Index(which[i]))}return out.Interface(), len(which)
}

使用反射来做这些东西会有一个问题,那就是代码的性能会很差。所以,上面的代码不能用在需要高性能的地方