这边文章没有很复杂的概念,单纯介绍下函数式编程的概念,以及在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个配置项,其中假设Addr
与Port
是必填项,MaxIdle
与Timeout
是可选项(有默认值)。通过一个函数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
模式。
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
。更多信息请参考之前的博客,里面有更详细的介绍。