Go进阶之长参数函数

29 阅读6分钟

在Go中.变长参数函数使用的最多的就是fmt包 log包中的几个导出函数.

源码位置:src/fmt/print.go

// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...any) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}



// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func Printf(format string, a ...any) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

源码位置:src/log/log.go

// Printf calls l.Output to print to the logger.
// Arguments are handled in the manner of [fmt.Printf].
func (l *Logger) Printf(format string, v ...any) {
	l.output(0, 2, func(b []byte) []byte {
		return fmt.Appendf(b, format, v...)
	})
}

// Println calls l.Output to print to the logger.
// Arguments are handled in the manner of [fmt.Println].
func (l *Logger) Println(v ...any) {
	l.output(0, 2, func(b []byte) []byte {
		return fmt.Appendln(b, v...)
	})
}

1.变长参数函数:

变长参数就是指调用时可以接受零个 一个或多个实际参数的函数.示例如

下:

image.png

可以看到无论传入零个 两个还是多个实际参数.都传给了Print()方法的形

式参数a(参考上面源码).形参a的类型是...any.这种接受"...T"类型形式参

数的函数就被称为变长参数函数.

一个变长参数函数只能有一个"...T"类型形式参数.并且该形式参数应该为

函数参数列表中的最后一个形式参数.否则Go编译器会有错误提示.

image.png

变长参数函数的"...T"类型形式参数在函数体内呈现为[]T类型的变量.可以

将其理解为一个Go的语法糖.示例如下:

```
func sum(arg ...int) int {
    var total int 
    //arg的类型为[]int
    for _, v := range arg {
       total += v
    }
    return total
}
```

在函数外部."...T"类型形式参数可以匹配和接受的实参类型有两种.

1).多个T类型变量.

2).t...(t为[]T类型变量).示例如下:

func main() {
	a, b, c := 1, 2, 3
	fmt.Println(sum(a, b, c))

	nums := []int{1, 2, 3}
	fmt.Println(sum(nums...))
}

func sum(arg ...int) int {
	var total int
	//arg的类型为[]int
	for _, v := range arg {
		total += v
	}
	return total
}

我们只能选择上述两种实参类型的一种.要么是多个T类型变量.要么是t...(t

为[]T类型变量).如果两种混用.会得到类似下面的编译错误.

image.png 使用变长参数函数最容易出现的问题是实参与形参不匹配.示例如下:

```
func main() {
    s := []string{"a", "b", "c"}
    dump(s...)
}

func dump(args ...interface{}) {
    for _, v := range args {
       fmt.Println(v)
    }
}
```

image.png 编译器给出了类型不匹配的错误.虽然string类型可以直接赋值给interf

ace{}类型变量.但是[]string类型变量并不能直接赋值给[]interface{}类

型变量.修改示例如下:

```
func main() {
    s := []interface{}{"a", "b", "c"}
    dump(s...)
}

func dump(args ...interface{}) {
    for _, v := range args {
       fmt.Println(v)
    }
}
```

不过有个例外.就是Go的内置函数append函数.它支持通过下面的方式将

字符串附加到一个字节后面.示例如下:

```
func main() {
    b := []byte{}
    b = append(b, "hello"...)
    fmt.Println(b)
}
```

string类型本身不满足类型要求(append本需要[]byte...).这算是Go的一

个编译优化.编译器自动将string隐式转换为了[]byte.

```
func main() {
    b := []byte{}
    b = append(b, "hello"...)
    fmt.Println(b)
    
    fooTest(b)
}

func fooTest(b ...byte) {
    fmt.Println(b)
}
```

image.png

2.模拟函数重载:

Go语言不允许在同一个作用域下定义名字相同但函数原型不同的函数.如果

定义这样的函数.Go编译器会提示下面代码的错误信息.

```
func concat(a, b int) string {
    return fmt.Sprintf("%d %d", a, b)
}

func concat(x, y string) string {
    return fmt.Sprintf("%s %s", x, y)
}

func concat(s []string)string  {
    return strings.Join(s, " ")
}
```

image.png

要修复上面的错误.只能修改函数命名.但是在其他语言中.比如java.就支持

这种名字相同.参数类型不同的重载函数.但Go语言并不支持函数重载.Go语

言官方给出不支持的理由是:

其他语言的经验告诉我们.使用具有相同名称但是函数签名不同的多种方法

有时会很有用.但在实践中也可能会造成混淆和脆弱性.在Go的类型系统中.

仅按名称进行匹配要求类型一致是一个主要的简化决策.

变长参数解决.示例如下:

```
func main() {
    fmt.Println(concat("-", 1, 2))
    fmt.Println(concat("-", "hello", "gopher"))

}

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("%v", 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("%v", v)
          }
       case []string:
          strings := v.([]string)
          result += fmt.Sprintf("%v", strings)
       default:
          fmt.Println("不支持此类型")
          return ""
       }
    }
    return result
}
```

3.模拟实现函数的可选参数和默认参数:

如果参数在传入时有隐式要求的固定顺序(调用者保证).还可以利用变长参

数函数模拟实现可选参数和默认参数.示例如下:

```
type record struct {
    name    string
    gender  string
    age     int
    city    string
    country string
}

func enroll(args ...interface{}) (*record, error) {
    if len(args) > 5 || len(args) < 3 {
       return nil, fmt.Errorf("the number of arguments passed to wrong")
    }

    r := &record{
       city:    "ShanXi",
       country: "TaiYuan",
    }

    for i, v := range args {
       switch i {
       case 0:
          name, ok := v.(string)
          if !ok {
             return nil, fmt.Errorf("the first argument to enroll must be a string")
          }
          r.name = name
       case 1:
          gender, ok := v.(string)
          if !ok {
             return nil, fmt.Errorf("the second argument to enroll must be a string")
          }
          r.gender = gender
       case 2:
          age, ok := v.(int)
          if !ok {
             return nil, fmt.Errorf("the third argument to enroll must be an uint16")
          }
          r.age = age
       case 3:
          city, ok := v.(string)
          if !ok {
             return nil, fmt.Errorf("the third argument to enroll must be a string")
          }
          r.city = city
       case 4:
          country, ok := v.(string)
          if !ok {
             return nil, fmt.Errorf("the fourth argument to enroll must be a string")
          }
          r.country = country
       default:
          return nil, fmt.Errorf("unknown argument %d", i)
       }
    }
    return r, nil

}

func main() {
    r, _ := enroll("小明", "male", 26)
    fmt.Printf("%+v\n", *r)

    r1, _ := enroll("小明", "male", 26, "linfen")
    fmt.Printf("%+v\n", *r1)

}
```

4.实现功能选项:

1).通过参数暴露配置选项:

```
type FinishedHouse struct {
    style                  int
    centralAirConditioning bool
    floorMaterial          string
    wallMaterial           string
}

func NewFinishedHouse(style int, centralAirConditioning bool, floorMaterial, wallMaterial string) *FinishedHouse {
    h := &FinishedHouse{
       style:                  style,
       centralAirConditioning: centralAirConditioning,
       floorMaterial:          floorMaterial,
       wallMaterial:           wallMaterial,
    }
    return h
}

func main() {
    fmt.Printf("%+v\n", NewFinishedHouse(0, true, "wood", "paper"))
}
```

上面设计的唯一优点就是快速实现.不足之处很多.最致命的是接口无法扩

展.

2)结构体封装配置项:

```
type FinishedHouse struct {
    style                  int
    centralAirConditioning bool
    floorMaterial          string
    wallMaterial           string
}

type Options struct {
    style                  int
    centralAirConditioning bool
    floorMaterial          string
    wallMaterial           string
}

func NewFinishedHouse(options *Options) *FinishedHouse {
    var style  = 0
    var centralAirConditioning = true
    var floorMaterial = "wood"
    var wallMaterial = "paper"
    if options != nil {
       style = options.style
       centralAirConditioning = options.centralAirConditioning
       floorMaterial = options.floorMaterial
       wallMaterial = options.wallMaterial
    }
    h := &FinishedHouse{
       style:                  style,
       centralAirConditioning: centralAirConditioning,
       floorMaterial:          floorMaterial,
       wallMaterial:           wallMaterial,
    }
    return h
}

func main() {
    fmt.Printf("%+v\n", NewFinishedHouse(0, true, "wood", "paper"))
}
```

优点:

1).后续添加配置项选项.Options结构体可以随着时间变迁而增长.但

FinishedHouse创建函数本身的Api签名不变.

2).允许调用者使用nil来表示他们希望使用默认值来创建.

3).可以更好地记录文档.

缺点:

1).每次都要为Options中所有字段赋值.

2).如果Options中的值在调用后变化了怎么办.

3).使用功能选项:

```
type FinishedHouse struct {
    style                  int
    centralAirConditioning bool
    floorMaterial          string
    wallMaterial           string
}

type Option func(h *FinishedHouse)

func NewFinishedHouse(options ...Option) *FinishedHouse {
    h := &FinishedHouse{
       style:                  0,
       centralAirConditioning: true,
       floorMaterial:          "wood",
       wallMaterial:           "paper",
    }

    for _, option := range options {
       option(h)
    }

    return h
}

func WithStyle(style int) Option {
    return func(h *FinishedHouse) {
       h.style = style
    }
}

func WithCentralAirConditioning(centralAirConditioning bool) Option {
    return func(h *FinishedHouse) {
       h.centralAirConditioning = centralAirConditioning
    }
}

func WithFloorMaterial(floorMaterial string) Option {
    return func(h *FinishedHouse) {
       h.floorMaterial = floorMaterial
    }
}

func WithWallMaterial(wallMaterial string) Option {
    return func(h *FinishedHouse) {
       h.floorMaterial = wallMaterial
    }
}

func main() {
    //默认.
    fmt.Printf("%+v\n", NewFinishedHouse())

    fmt.Printf("%+v\n", NewFinishedHouse(WithStyle(1), WithFloorMaterial("title")))

}
```

***我也曾像个疯子一样.








***如果大家喜欢我的分享的话.可以关注我的微信公众号


念何架构之路