Go 语言
函数和方法
变长参数函数

变长参数函数的妙用

我们使用得最多是因为最常用的 fmt 包、log 包中的几个导出函数都是变长参数函数。

Go 内置的常用语切片类型操作的 append 函数也是变长参数函数。

func append(slice []Type, elems ...Type) []Type

什么是变长参数函数

顾名思义,变长参数函数就是指调用时可以接受零个,一个或多个实际参数的函数。如:

println("hello")
println()
println("hello", "world")

我们看到,无论传入多少个参数,println 都可以正常工作。

一个变长参数函数的声明形式如下:

func funcName(arg ...Type) {
    // ...
}

另外,一个变长参数函数只能有一个 ...Type 类型的参数,并且必须是最后一个参数。

变长参数函数的 ...Type 类型形式参数再函数体内为 []Type 的变量,我们可以理解为一个语法糖。

func sum(agrs ...int) int {
    var sum int
    for _, arg := range args {
        sum += arg
    }
    return sum
}

在函数外部,...Type 形式参数可匹配和接受的实参类型有两种:

  • 多个 Type 类型的变量
  • t... (t 为 T 类型变量)
func main() {
    fmt.Println(sum(1, 2, 3)) // 6
    fmt.Println(sum([]int{1, 2, 3}...)) // 6
}

我们只能选择上述的两种实参类型的其中一种,不能同时使用。

另外。使用变长参数函数,最容易出现一个问题是实参与形参不匹配,如下:

func dump(args ...interface{}) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}
 
func main() {
    a := []string{"a", "b", "c"}
    dump(a) // panic: cannot use a (type []string) as type []interface {} in argument to dump
}

这是因为 a[]string 类型,而 dump 函数的形参是 ...interface{} 类型,这两者不匹配。

虽然 string 类型是 interface 类型的子集,但是 []string 类型并不是 []interface{} 类型的子集。

也有个例外,如append

模拟函数重载

Go 语言不允许在同一个作用域下定义名字相同但函数原型不同的函数。Go 语言官方给出的不支持的理由如下:

其他语言的经验告诉我们,使用具有相同名称但函数签名不同的的多种方法有时会很有用,但在实践中也可能会造成混淆和脆弱性。在 Go 的类型系统中,仅按名称进行匹配并要求类型一致是一个主要的简化策略。

既然不支持,那么我们可以使用变长参数函数来模拟函数重载。

func concat(sep string, args ...interface{}) string {
    var result string
    for i, v := range args {
        if i != 0 {
            result += sep
        }
        switch v.(type) {
        case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
            result += fmt.Sprintf("%d", v)
        case string:
            result += fmt.Sprintf("%s", v)
        case []int:
            ints := v.([]int)
            for i, v := range ints {
                if i != 0 {
                    result += sep
                }
                result += fmt.Sprintf("%d", v)
            }
        case []string:
            strs := v.([]string)
            result += strings.Join(strs, sep)
        default:
            fmt.Println("unsupported type")
            return ""
        }
    }
    return result
}
 
func main() {
    println(concat(",", 1, 2, 3)) // 1,2,3
    println(concat(",", "a", "b", "c")) // a,b,c
    println(concat(",", []int{1, 2, 3})) // 1,2,3
    println(concat(",", []string{"a", "b", "c"})) // a,b,c
    println(concat(",", 1, "a", []int{2, 3}, []string{"b", "c"})) // 1,a,2,3,b,c
}

实现功能选项模式

我们来看使用选型模式实现的例子:

type Server struct {
    addr string
    port int
    timeout time.Duration
    maxConn int
    maxReqLen int
    readTimeout time.Duration
    writeTimeout time.Duration
}
 
type Option func(*Server)
 
func Addr(addr string) Option {
	return func(s *Server) {
		s.addr = addr
	}
}
 
func Port(port int) Option {
	return func(s *Server) {
		s.port = port
	}
}
 
func Timeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}
 
func MaxConn(maxConn int) Option {
    return func(s *Server) {
        s.maxConn = maxConn
    }
}
 
func MaxReqLen(maxReqLen int) Option {
    return func(s *Server) {
        s.maxReqLen = maxReqLen
    }
}
 
func ReadTimeout(readTimeout time.Duration) Option {
    return func(s *Server) {
        s.readTimeout = readTimeout
    }
}
 
func WriteTimeout(writeTimeout time.Duration) Option {
    return func(s *Server) {
        s.writeTimeout = writeTimeout
    }
}
 
func NewServer(addr string, port int, options ...Option) (*Server, error) {
    srv := &Server{
        addr: addr,
        port: port,
        timeout: 30 * time.Second,
        maxConn: 1024,
        maxReqLen: 1024,
        readTimeout: 5 * time.Second,
        writeTimeout: 5 * time.Second,
    }
    for _, option := range options {
        option(srv)
    }
    return srv, nil
}
 
func main() {
    srv, err := NewServer("localhost", 8080, Timeout(60 * time.Second), MaxConn(2048))
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(srv) // &{localhost 8080 60000000000 2048 1024 5000000000 5000000000}
}

功能选项模式是一种用于构建复杂对象的创建模式。它可以用于解决构造函数参数过多的问题,也可以用于解决构造函数参数顺序不确定的问题。

可以收获如下好处:

  • 更漂亮的、不随时间变化的公共 API;
  • 参数可读性更好;
  • 配置选项高度可扩展;
  • 提供使用默认选项更简单方式;
  • 使用更安全;