likes
comments
collection
share

go 语言的函数选项模式

作者站长头像
站长
· 阅读数 43

个人主页:二郎腿 (erlangtui.top)

一、基本介绍

  • 对于C++语言,如果某个函数需要新增参数,可以通过函数重载或是添加可选参数的方式实现,该函数以前的调用方不需要重新更改,如下:
#include <iostream>
#include <string>

void printMessage(const std::string& message) {
    std::cout << message << std::endl;
}

// 函数重载
void printMessage(const std::string& message, int number) {
    std::cout << message << "The number is: " << number << std::endl;
}

// 原先函数
// void greet(const std::string& name)

// 可选参数的函数
void greet(const std::string& name, const std::string& greeting = "Hello") {
    std::cout << greeting << ", " << name << "!" << std::endl;
}

int main() {
    // 调用函数重载
    printMessage("Hello, world!"); // 此调用方不用更改
    printMessage("Hello, world!", 42);

    // 调用带有可选参数的函数
    greet("Alice"); // 此调用方不用更改
    greet("Bob", "Hi");

    return 0;
}
  • go 语言没有函数重载,也不支持可选参数,所以对于某个函数,需要新增不同个数不同类型的参数时,如果直接修改的话,那么该函数以前的所有调用方都需要更改
  • go 语言支持可变数量的参数,但是该参数的类型必须一致,可以将自定义的函数类型作为参数类型,每个函数参数都是一个闭包,可以读取并处理其外部不同类型的变量,并将其赋值给函数的选项,以此实现某个函数可以接收不同数量不同类型的参数,且需要扩展参数时不用更改函数签名以及调用方,这种方式能够在语言缺陷下很好的解决代码扩展性问题;

二、基本原理

  • 先声明一个 serverCfgs结构体,用于存储函数的参数;
  • 再声明一个函数指针 Option,该函数的参数是 serverCfgs类型的指针,该函数的功能一般是接收其外部的参数并校验,存储到 serverCfgs类型的结构体指针中;
  • 构造需要传递参数的函数,这些函数可以接受不同类型的参数,并返回一个 option 类型的闭包,在该闭包中处理该参数,每次需要新增参数时,构建一个 func withXXX(XXX any) option函数即可,XXX 为任意类型;
  • func newServerCfg(opts ...option) (*serverCfgs, error)是真正需要接收参数的函数,它可以接收任意个 option 类型的函数指针,这样将本应该接收任意个不同类型参数的函数转换为接收任意个 option 类型参数的函数,即使扩展参数该函数的签名不变,不需要更改其调用方
  • 只需要在该函数中依次调用 option 类型的函数,并向其传递 serverCfgs类型的结构体指针用于接收 withXXX 的参数;
package main

import (
	"errors"
	"fmt"
)

type serverCfgs struct {
	Host    string
	Port    int
	Timeout int
	DealFun func() error
}

// 表示更新配置结构体的函数类型
type option func(*serverCfgs) error

// 该函数返回一个闭包,引用其外部的变量,并进行校验,变量的类型是可变的
func withHost(host string) option {
	return func(cfgs *serverCfgs) error {
		if host == "" {
			return errors.New("host is null")
		}
		cfgs.Host = host
		return nil
	}
}

func withPort(port int) option {
	return func(cfgs *serverCfgs) error {
		if port < 0 {
			return errors.New("port is invalid")
		}
		cfgs.Port = port
		return nil
	}
}

func withTimeout(timeout int) option {
	return func(cfgs *serverCfgs) error {
		if timeout < 0 {
			return errors.New("timeout is invalid")
		}
		cfgs.Timeout = timeout
		return nil
	}
}

func withDealFun(f func() error) option {
	return func(cfgs *serverCfgs) error {
		if f == nil {
			return errors.New("f is nil")
		}
		cfgs.DealFun = f
		return nil
	}
}

// 通过传递函数列表,每个函数都是一个闭包,函数列表长度是可变的
func newServerCfg(opts ...option) (*serverCfgs, error) {
	// 初始化选项
	op := &serverCfgs{}

	// 应用选项
	for _, opt := range opts {
		if err := opt(op); err != nil {
			// 参数错误能够及时返回
			return nil, err
		}
	}
	return op, nil
}

func newClient(c *serverCfgs) error {
	if c == nil {
		return errors.New("server cfg is null")
	}
	// do somethings
	return nil
}

func f() error {
	// do something
	return nil
}
func main() {
	// 在需要增加参数的地方构建 withXXX 函数,并添加到参数列表中,其他调用的地方不需要更改
	cfgs, err := newServerCfg(withHost("erlangtui.top"), withPort(80), withTimeout(10), withDealFun(f))
	if err != nil {
		fmt.Println(err)
		return
	}
	_ = newClient(cfgs)
	fmt.Println("succeed init")
}

三、总结

优点

  • 函数式选项模式能够解决函数参数扩展时其他语言需要函数重载才能解决的问题
  • 函数式选项模式能够支持任意顺序、任意数量、任意类型的参数,以及默认值
  • 函数式选项模式使代码更容易维护与扩展,方便向后兼容,也更加清晰可读;

应用

  • 简化接口:当函数有很多参数时,使用函数选项模式可以将这些参数组织成更简洁、可读性更高的形式;
  • 库设计:当不知道需要使用哪些配置时,函数式选项模式允许使用者根据需求自定义配置,比如MySQL,Redis,Kafka的配置;
  • 可扩展的API:通过将 API 的配置选项作为函数参数,用户可以根据需要灵活地配置 API 的行为,而不需要修改现有的代码;
转载自:https://juejin.cn/post/7376563996423962658
评论
请登录