Golang函数式编程

什么是函数式编程

这边文章没有很复杂的概念,单纯介绍下函数式编程的概念,以及在Golang中如何实现函数式编程。

简单来说就是golang中支持将函数作为变量、参数传递、返回值等操作。

打个比方,我们可以把一些“逻辑流”封装起来,放在别的地方在执行。

注入连接例子

这里举个例子,我们要初始化一个连接,基于这个连接做些网络操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type conn struct {
	Addr    string
	Port    string
	MaxIdle int
	Timeout int64
	// ... other config fields
}

// NewConn creates a new conn object
func NewConn(addr, port string, maxIdle int, timeout int64) *conn {
	return &conn{
		Addr:    addr,
		Port:    port,
		MaxIdle: maxIdle,
		Timeout: timeout,
	}
}

conn结构体内有4个配置项,其中假设AddrPort是必填项,MaxIdleTimeout是可选项(有默认值)。通过一个函数NewConn来创建一个conn对象。有个问题,随着后续配置项增多,NewConn函数的参数会变得越来越多,调用者需要记住每个参数的位置,这样会导致调用者的代码可读性变差。

可以通过一个config结构体来解决这个问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type conn struct {
	Addr string
	Port string
	cfg  config
}

type config struct {
	MaxIdle int
	Timeout int64
	// ... other config fields
}

// NewConn creates a new conn object
func NewConn(addr, port string, cfg config) *conn {
	return &conn{
		Addr: addr,
		Port: port,
		cfg:  cfg,
	}
}

这样后续扩展配置项时,无需考虑NewConn函数参数变动了。代码改到这里,看起来已经很不错了,但是如你有代码洁癖可能会不满足于此,接下来介绍javaer常用的设计模式Builder模式。

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
41
42
43
44
45
46
47
48
49
50
type conn struct {
	Addr    string
	Port    string
	MaxIdle int
	Timeout int64
	// ... other config fields
}

type builder struct {
	Addr    string
	Port    string
	MaxIdle int
	Timeout int64
	// ... other config fields
}

func (b *builder) SetAddr(addr string) *builder {
	b.Addr = addr
	return b
}

func (b *builder) SetPort(port string) *builder {
	b.Port = port
	return b
}

func (b *builder) SetMaxIdle(maxIdle int) *builder {
	b.MaxIdle = maxIdle
	return b
}

func (b *builder) SetTimeout(timeout int64) *builder {
	b.Timeout = timeout
	return b
}

func (b *builder) Build() *conn {
	return &conn{
		Addr:    b.Addr,
		Port:    b.Port,
		MaxIdle: b.MaxIdle,
		Timeout: b.Timeout,
	}
}

func main() {
	b := new(builder)
	conn := b.SetAddr("localhost").SetPort("8080").SetMaxIdle(10).SetTimeout(1000).
		Build()
}

通过一个builder结构,将所有初始化的逻辑都交给它。不过这种做法在Golang中并不是最优雅的,因为Golang中有更好的方式来实现这种功能。接下来介绍函数式编程的做法。

函数式编程做法

本文最开始说过,golang中支持将函数作为变量、参数传递、返回值等操作。这里我们就将函数封装成一个option类型,通过option类型来设置conn结构体的配置项。

 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
type conn struct {
	Addr    string
	Port    string
	MaxIdle int
	Timeout int64
	// ... other config fields
}

type option func(*conn)

func WithAddr(addr string) option {
	return func(c *conn) {
		c.Addr = addr
	}
}

func WithPort(port string) option {
	return func(c *conn) {
		c.Port = port
	}
}

// ... other options, e.g. WithMaxIdle, WithTimeout

func NewConn(opts ...option) *conn {
	c := new(conn)
	for _, opt := range opts {
		opt(c)
	}
	return c
}

func main() {
	c := NewConn(WithAddr("localhost"), WithPort("8080"))
}

这么做的好处是,后续增加配置项时,只需要增加一个WithXXX函数,然后在NewConn函数中增加一个opt即可。这样代码看起来更加简洁,也更加容易扩展。对比于Builder模式,我们无需编写一个builder结构体,也无需编写一堆SetXXX函数,代码看起来更加简洁。

函数实现接口

在golang中,函数也可以实现接口,这样可以更加灵活的使用函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type people interface {
	sayHello(name string)
}

// peopleFunc implements people interface
type peopleFunc func(string)

func (p peopleFunc) sayHello(name string) {
	p(name)
}

func main() {
	sayHello := func(name string) {
		fmt.Println("Hello", name)
	}

	var p people = peopleFunc(sayHello)
}

这里我们将peopleFunc类型定义为一个函数类型,这个函数的入参与出参和people接口的sayHello方法一致。之后再写一个方法来实现people接口,内部实现还是调用poepleFunc自己。

这么做有个好处,无需编写一些结构体去实现接口,只需要一个函数即可。这样代码看起来更加简洁。同时调用者可以传个函数,更加灵活(想象一下调用者自己可以传个“逻辑流”进去)。

关于函数作为接口的实现,golang源码中有个著名的例子:接口为http.Handler,具体函数为http.HandlerFunc。更多信息请参考之前的博客,里面有更详细的介绍。

updatedupdated2024-05-032024-05-03