在C语言时代大家一般都用过printf()函数,从那个时候开始其实已经在感受可变参数的魅力和价值。如同C语言中的printf()函数,Go语言标准库中的fmt.Println()等函数的实现也严重依赖于语言的可变参数功能。
本节我们将介绍可变参数的用法。合适地使用可变参数,可以让代码简单易用,尤其是输入输出类函数,比如日志函数等。
可变参数类型
可变参数是指函数传入的参数个数为不定数量。为了做到这点,首先需要将函数定义为接受可变参数类型:
funcmyfunc(args...int){for_,arg:=rangeargs{fmt.Println(arg)}}
上面这段代码的意思是,函数myfunc()接受不定数量的参数,这些参数的类型全部是int,所以它可以用如下方式调用:
myfunc(2,3,4)
myfunc(1,3,7,13)
形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。它是一个语法糖(syntacticsugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的机会。
从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type,这也是为什么上面的参数args可以用for循环来获得每个传入的参数。
假如没有...type这样的语法糖,开发者将不得不这么写:
funcmyfunc2(args[]int){for_,arg:=rangeargs{fmt.Println(arg)}}
从函数的实现角度来看,这没有任何影响,该怎么写就怎么写。但从调用方来说,情形则完全不同:
myfunc2([]int{1,3,7,13})
会发现,我们不得不加上[]int{}来构造一个数组切片实例。但是有了…type这个语法糖,我们就不用自己来处理了。
可变参数的传递
假设有另一个变参函数叫做myfunc3(args…int),下面的例子演示了如何向其传递变参:
funcmyfunc(args...int){//按原样传递myfunc3(args...)//传递片段,实际上任意的intslice都可以传进去myfunc3(args[1:]...)}
任意类型的可变参数
之前的例子中将可变参数类型约束为int,如果你希望传任意类型,可以指定类型为interface{}。下面是Go语言标准库中fmt.Printf()的函数原型:
funcPrintf(formatstring,args…interface{}){
//…
}
用interface{}传递任意类型数据是Go语言的惯例用法。使用interface{}仍然是类型安全的,这和C/C++不太一样。下面通过示例来了解一下如何分派传入interface{}类型的数据。
packagemainimport"fmt"funcMyPrintf(args...interface{}){for_,arg:=rangeargs{switcharg.(type){caseint:fmt.Println(arg,"isanintvalue.")casestring:fmt.Println(arg,"isastringvalue.")caseint64:fmt.Println(arg,"isanint64value.")default:fmt.Println(arg,"isanunknowntype.")}}}funcmain(){varv1int=1varv2int64=234varv3string="hello"varv4float32=1.234MyPrintf(v1,v2,v3,v4)}
该程序的输出结果为:
1isanintvalue.
234isanint64value.
helloisastringvalue.
1.234isanunknowntype.
遍历可变参数列表——获取每一个参数的值
可变参数列表的数量不固定,传入的参数是一个切片。如果需要获得每一个参数的具体值时,可以对可变参数变量进行遍历,参见下面代码:
packagemainimport("bytes""fmt")//定义一个函数,参数数量为0~n,类型约束为字符串funcjoinStrings(slist...string)string{//定义一个字节缓冲,快速地连接字符串varbbytes.Buffer//遍历可变参数列表slist,类型为[]stringfor_,s:=rangeslist{//将遍历出的字符串连续写入字节数组b.WriteString(s)}//将连接好的字节数组转换为字符串并输出returnb.String()}funcmain(){//输入3个字符串,将它们连成一个字符串fmt.Println(joinStrings("pig","and","rat"))fmt.Println(joinStrings("hammer","mom","and","hawk"))}
代码输出如下:
pigandrat
hammermomandhawk
代码说明如下:
第8行,定义了一个可变参数的函数,slist的类型为[]string,每一个参数的类型是string,也就是说,该函数只接受字符串类型作为参数。
第11行,bytes.Buffer在这个例子中的作用类似于StringBuilder,可以高效地进行字符串连接操作。
第13行,遍历slist可变参数,s为每个参数的值,类型为string。
第15行,将每一个传入参数放到bytes.Buffer中。
第19行,将bytes.Buffer中的数据转换为字符串作为函数返回值返回。
第24行,输入3个字符串,使用joinStrings()函数将参数连接为字符串输出。
第25行,输入4个字符串,连接后输出。
如果要获取可变参数的数量,可以使用len()函数对可变参数变量对应的切片进行求长度操作,以获得可变参数数量。
获得可变参数类型——获得每一个参数的类型
当可变参数为interface{}类型时,可以传入任何类型的值。此时,如果需要获得变量的类型,可以通过switch类型分支获得变量的类型。下面的代码演示将一系列不同类型的值传入printTypeValue()函数,该函数将分别为不同的参数打印它们的值和类型的详细描述。
打印类型及值:
packagemainimport("bytes""fmt")funcprintTypeValue(slist...interface{})string{//字节缓冲作为快速字符串连接varbbytes.Buffer//遍历参数for_,s:=rangeslist{//将interface{}类型格式化为字符串str:=fmt.Sprintf("%v",s)//类型的字符串描述vartypeStringstring//对s进行类型断言switchs.(type){casebool://当s为布尔类型时typeString="bool"casestring://当s为字符串类型时typeString="string"caseint://当s为整型类型时typeString="int"}//写值字符串前缀b.WriteString("value:")//写入值b.WriteString(str)//写类型前缀b.WriteString("type:")//写类型字符串b.WriteString(typeString)//写入换行符b.WriteString("n")}returnb.String()}funcmain(){//将不同类型的变量通过printTypeValue()打印出来fmt.Println(printTypeValue(100,"str",true))}
代码输出如下:
value:100type:int
value:strtype:string
value:truetype:bool
代码说明如下:
第8行,printTypeValue()输入不同类型的值并输出类型和值描述。
第11行,bytes.Buffer字节缓冲作为快速字符串连接。
第14行,遍历slist的每一个元素,类型为interface{}。
第17行,使用fmt.Sprintf配合%v动词,可以将interface{}格式的任意值转为字符串。
第20行,声明一个字符串,作为变量的类型名。
第23行,switchs.(type)可以对interface{}类型进行类型断言,也就是判断变量的实际类型。
第24~29行为s变量可能的类型,将每种类型的对应类型字符串赋值到typeString中。
第33~42行为写输出格式的过程。
在多个可变参数函数中传递参数
可变参数变量是一个包含所有参数的切片,如果要在多个可变参数中传递参数,可以在传递时在可变参数变量中默认添加...,将切片中的元素进行传递,而不是传递可变参数变量本身。
下面的例子模拟print()函数及实际调用的rawPrint()函数,两个函数都拥有可变参数,需要将参数从print传递到rawPrint中。
可变参数传递:
packagemainimport"fmt"//实际打印的函数funcrawPrint(rawList...interface{}){//遍历可变参数切片for_,a:=rangerawList{//打印参数fmt.Println(a)}}//打印函数封装funcprint(slist...interface{}){//将slist可变参数切片完整传递给下一个函数rawPrint(slist...)}funcmain(){print(1,2,3)}
代码输出如下:
1
2
3
对代码的说明:
第9~13行,遍历rawPrint()的参数列表rawList并打印。
第20行,将变量在print的可变参数列表中添加...后传递给rawPrint()。
第25行,传入1、2、3这3个整型值进行打印。
如果尝试将第20行修改为:
rawPrint("fmt",slist)
再次执行代码,将输出:
[123]
此时,slist(类型为[]interface{})将被作为一个整体传入rawPrint(),rawPrint()函数中遍历的变量也就是slist的切片值。
可变参数使用...进行传递与切片间使用append连接是同一个特性。