文章目录
- 一、传递参数
- 1. 按值传参
- 2. 引用传参
- 2.1 特殊情况
- 2.1.1 切片slice
- 2.1.2 字典map
- 二、变长参数
- 1. 基本定义和传值
- 1.1 基本定义
- 1.2 传值
- 1.2.1 普通传值
- 1.2.2 传递切片
- 2. 任意类型的变长参数(泛型)
- 三、多返回值
- 1. 命名返回值
一、传递参数
1. 按值传参
Go语言默认使用按值传参来传递参数,即传递参数值的一个副本:
函数接收到传递进来的参数后,会将参数值拷贝给声明该参数的变量(形式参数,简称形参),如果在函数体中有对参数值做修改,实际上修改的是形参值,这不会影响到实际传递进来的参数值(简称实参)。
func add(a, b int) int {a *= 2b *= 3return a + b
}func main() {x, y := 1, 2z := add(x, y)fmt.Printf("add(%d, %d) = %d\n", x, y, z) //add(1, 2) = 8
}
x、y把值传递给a、b,主要原因是x、y的内存地址与a、b的内存地址并不相同,可以看作是两个值相等的不同变量,所以修改a、b的值并不会改变x、y的值。
2. 引用传参
如果想要实现在函数中修改形参值可以同时实参值,需要通过引用传参来完成,此时传递给函数的参数是指一个指针,而指针代表的是实参的内存地址,修改指针引用的值即修改内存地址中存储的值,所以实参的值也会被修改。
! 注意:这种情况下,传递的变量是地址值的拷贝,所以从本质上讲还是按值传递。
func add(a, b *int) int {*a *= 2*b *= 3return *a + *b
}func main() {x, y := 1, 2z := add(&x, &y)fmt.Printf("add(%d, %d) = %d\n", x, y, z)
}
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型默认使用引用传参。
2.1 特殊情况
以下具体分析详见:(留个坑)
2.1.1 切片slice
把slice作为函数参数传递,若在函数内slice发生扩容的话,会让函数内外原本指向同一个底层数组的两个slice变量,分别指向两个不同的底层数组
2.1.2 字典map
把map作为函数参数传递,若在函数内map发生扩容的话,函数内外的map变量指向的底层内存仍是一致的。
二、变长参数
所谓变长参数指的是函数参数的数量不确定,即可以传递任意数量的参数到指定函数。合适地使用变长参数,可以让代码更简洁,尤其是输入输出类函数,fmt.Println
的参数就是典型的变长参数。
1. 基本定义和传值
1.1 基本定义
在参数类型前加上...
前缀,就可以将该参数声明为变长参数
func myfunc(numbers ...int) {for _, number := range numbers {fmt.Println(number)}
}
1.2 传值
1.2.1 普通传值
myfunc(1, 2, 3, 4, 5)
函数 myfunc()
接受任意数量的参数,这些参数的类型全部是 int
1.2.2 传递切片
传递切片时需要末尾加上...
作为标识,表示对应的参数类型是变长参数:
slice := []int{1, 2, 3, 4, 5}
myfunc(slice...)
myfunc(slice[1:3]...)
注:形如
...type
格式的类型只能作为函数的参数类型存在,并且是函数的最后一个参数
之所以支持传入切片,是因为从底层实现原理上看,类型...type
本质上是一个切片,也就是[]type
,这也是为什么上面的numbers
可以用for循环来获取每个传入的参数值。
2. 任意类型的变长参数(泛型)
即指定变长参数类型为interface{}
func myPrintf(args ...interface{}) {for _, arg := range args {switch reflect.TypeOf(arg).Kind() {case reflect.Int:fmt.Println(arg, "is an int value.")case reflect.String:fmt.Printf("\"%s\" is a string value.\n", arg)case reflect.Array:fmt.Println(arg, "is an array type.")default:fmt.Println(arg, "is an unknown type.")}}
}func main() {myPrintf(1, "1", [1]int{1}, true)
}
这里实现的是泛型功能,Go语言并没有在语法层面提供对泛型的支持,目前只能自己通过反射和interface{}
类型实现。
interface{}
是一个空接口,可以用于表示任意类型。但这个范围过于宽泛,像C语言中的void
一样,我们根本不知道真正传递进来的参数到底是什么类型的,这在强类型的静态语言中是不能接受的,为了保证代码类型安全,需要在运行时通过反射对数据类型进行检查,以便让程序在预设的轨道内运行,避免因为类型问题导致程序崩溃。
三、多返回值
Go语言与其他编程语言一大不同之处在于支持多返回值,这才处理程序出错的时候非常有用。
func add(a, b *int) (int, error) {if (*a == 0 || *b == 0) {err := errors.New("只支持非负整数相加")return 0, err}*a *= 2*b *= 3return *a + *b, nil
}func main() {x, y := -1, 2z, err := add(x, y)if err != nil {fmt.Println(err.Error())return}fmt.Printf("add(%d, %d) = %d\n", x, y, z)
}
如上,通过error
指定多返回一个表示错误信息的、类型为error的返回值,函数的多个返回值之间可以通过逗号分隔,并且在最外面通过圆括号包起来。
1. 命名返回值
在设置多返回值时,可以对返回值进行变量命名,这样可以在函数中直接对返回值进行赋值,而不必每次都按照指定的返回值格式返回多个变量了。
func add(a, b *int) (c int, err error) {if (*a == 0 || *b == 0) {err = errors.New("只支持非负整数相加")return}*a *= 2*b *= 3c = *a + *breturn
}
这种机制避免了每次进行 return 操作时都要关注函数需要返回哪些返回值,为开发者节省了精力,尤其是在复杂的函数中。