Menu
CoddyTech

函数式选项模式

本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 79 课,共 107 课。

创建包含许多可选配置字段的结构体时,构造函数可能会变得难以管理。函数选项模式通过使用函数来配置对象,优雅地解决了这个问题。

考虑一个包含多个可选设置的服务器结构体。我们不使用带有许多参数的构造函数,而是定义一个选项类型和返回选项的函数:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

构造函数接受一个可变数量的选项列表,并逐个应用:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // 默认值
        timeout: 30,      // 默认值
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

现在,创建服务器变得清晰灵活:

// 使用默认设置
s1 := NewServer("localhost")

// 自定义特定选项
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

这种模式很实用,因为调用方只需指定所需内容,默认值集中在一处且一目了然,添加新选项也不会破坏现有代码。它广泛用于 grpc 和 zap 等 Go 库中。

challenge icon

挑战

简单

让我们使用函数选项模式构建可配置的数据库连接!当你有许多带有合理默认值的可选设置时,这种模式就很有用,而数据库连接正需要这样的模式。

你会将代码分到两个文件中:

  • database.go:定义数据库连接类型和选项函数。

    创建一个包含以下未导出字段的 DBConnection 结构体:

    • host(string)
    • port(int)
    • username(string)
    • password(string)
    • maxConnections(int)
    • timeout(int),单位为秒

    定义一个 Option 类型,它是一个用于修改 *DBConnection 的函数。

    创建以下返回 Option 的选项函数:

    • WithPort(port int)
    • WithCredentials(username, password string)
    • WithMaxConnections(max int)
    • WithTimeout(seconds int)

    创建构造函数 NewDBConnection(host string, opts ...Option),使其返回一个 *DBConnection。设置以下默认值:port 为 5432,username 为 "admin",password 为 "secret",maxConnections 为 10,timeout 为 30。设置默认值后,应用所有传入的选项。

    添加一个 ConnectionString() 方法,使其按以下格式返回连接信息:

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go:使用不同配置构建数据库连接。

    读取主机名,然后读取要应用的选项数量。对于每个选项,读取选项类型及其值:

    • port,后面跟端口号
    • credentials,后面跟用户名和密码(两行)
    • maxconn,后面跟最大连接数
    • timeout,后面跟以秒为单位的超时时间

    使用指定的选项创建一个 DBConnection,并打印它的连接字符串。

输入格式如下:

  • 第 1 行:主机名
  • 第 2 行:选项数量
  • 后续各行:选项类型及其值

例如,输入:

localhost
0

输出应为:

admin:secret@localhost:5432 (max:10, timeout:30s)

再例如,输入:

db.example.com
2
port
3306
credentials
root
mypassword

输出应为:

root:mypassword@db.example.com:3306 (max:10, timeout:30s)

再例如,输入:

production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60

输出应为:

dbuser:securepass123@production.server:5433 (max:50, timeout:60s)

注意,调用方只需指定所需的选项,未指定的设置会使用合理的默认值。这就是函数选项模式的优雅之处!

亲手试试

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// 读取主机地址
	host, _ := reader.ReadString('\n')
	host = strings.TrimSpace(host)

	// 读取选项数量
	numOptionsStr, _ := reader.ReadString('\n')
	numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))

	// 收集选项
	var opts []Option

	for i := 0; i < numOptions; i++ {
		optionType, _ := reader.ReadString('\n')
		optionType = strings.TrimSpace(optionType)

		switch optionType {
		case "port":
			portStr, _ := reader.ReadString('\n')
			port, _ := strconv.Atoi(strings.TrimSpace(portStr))
			// TODO:将 WithPort 选项追加到 opts
			_ = port // 实现时删除这一行

		case "credentials":
			username, _ := reader.ReadString('\n')
			username = strings.TrimSpace(username)
			password, _ := reader.ReadString('\n')
			password = strings.TrimSpace(password)
			// TODO:将 WithCredentials 选项追加到 opts
			_, _ = username, password // 实现时删除这一行

		case "maxconn":
			maxStr, _ := reader.ReadString('\n')
			maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
			// TODO:将 WithMaxConnections 选项追加到 opts
			_ = maxConn // 实现时删除这一行

		case "timeout":
			timeoutStr, _ := reader.ReadString('\n')
			timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
			// TODO:将 WithTimeout 选项追加到 opts
			_ = timeout // 实现时删除这一行
		}
	}

	// TODO:使用 host 和 opts,通过 NewDBConnection 创建 DBConnection
	// TODO:使用 ConnectionString() 方法输出连接字符串

	_ = opts // 实现时删除这一行
	fmt.Println("TODO: Print connection string here")
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器