一、函数定义
函数是结构化编程中最小的模块单元,日常开发过程中,将复杂的算法过程分解为若干个小任务(代码块),使程序的结构性更清晰,程序可读性提升,易于后期维护和让别人读懂你的代码。
编写多个函数的主要目的是将一个需要很多行代码的复杂问题分解为一系列简单的任务(那就是函数)来解决。在实际编程中,我们把重复性的任务抽象成一个函数。
和所有的编程语言一样,Go语言支持各种风格的函数。在Go语言中,当函数执行到代码块最后一行 } 之前或者 return 语句的时候会退出,其中 return 语句可以带有零个或多个参数;这些参数将作为返回值供调用者使用。简单的 return 语句也可以用来结束 for 死循环,或者结束一个Go协程(goroutine)。
二、定义语法
func 函数名(参数列表) (返回值列表) {
// 函数体
}
func funcName (input1 type1, input2 type2) (output1 type1, output2 type2) {
// 逻辑代码
// 返回多个值
return value1, value2
}
上面的代码我们可以看出:
1)关键字 func 用来声明一个函数 funcName;
2)函数可以有一个或者多个参数,每个参数后面带有类型,多个参数之间通过“,”分割;
3)函数可以返回多个值;
4)上面返回值声明了两个变量 output1 和 output2,如果你不想声明也可以,直接就两个类型;
5)如果只有一个返回值且不声明返回值变量,那么你可以省略,包括返回值的括号;
6)如果没有返回值,那么就直接省略最后的返回信息;
7)如果有返回值, 那么必须在函数的外层添加return语句;
这里需要强调的是,Go语言函数的返回值类型和变量定义的数据类型一样,都要遵循Go语言的“后置原则”放在后面,这一点和C语言函数定义有显著不同。
另外,Go语言函数定义中如果参数列表中若干个相邻参数的类型相同,则可以在参数列表中省略前面的变量类型声明。
func Add(a, b int) int { //这里a和b都是int类型
// 函数体
}
最后,Go语言函数定义中左花括号的位置被强制规范,如果左花括号放置不规范,golang编译器会报编译错误。
//错误!!!左括号必须紧跟在括号后面
func hello()
{ //左括号不能另起一行
}
三、综合示例代码1
// 返回a、b中最大值
func max(a, b int) int {
if a > b {
return a
}
return b
}
func main() {
x := 3
y := 4
z := 5
max_xy := max(x, y) // 调用函数max(x, y)
max_xz := max(x, z) // 调用函数max(x, z)
fmt.Printf("max(%d, %d) = %d\n", y, z, max(y, z)) // 也可在这直接调用
}
上面这个里面我们可以看到max函数有两个参数,它们的类型都是int,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时我们注意到它的返回值就是一个类型,这个就是省略写法。
四、支持多返回值、支持命名返回值
Go语言的函数可以返回不止一个结果,即支持“多值返回”。
Go语言函数多值返回一般用于处理错误。比如在IO操作时候,不一定每次都能成功:可能文件不存在或者磁盘损坏无法读取文件。所以一般在函数调用发生错误时返回一个附加的结果作为错误值,习惯上将错误值作为最后一个结果返回。
func sumProductDiff(i, j int) (int, int, int) { // 多返回值
return i+j, i*j, i-j
}
func Split(sum int) (x, y int) { // 命名返回
x = sum * 4 / 9
y = sum - x
return
}
如果是命名返回值,则返回的时候不用带上变量名,因为直接在函数里面初始化了。
如果方法是公开访问的,建议最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。
五、不定参数
Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:
func myfunc (arg ...int) {}
arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。
调用该函数的时候可以传递该类型任意数目的参数。在函数体中,变量arg是一个int的slice。
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}
六、综合示例代码2
我们在这里写三个函数,依次展示出函数最常态模样,多值返回和不定参数。
func Add(i int, j int) (int) { //常规函数
return i+j
}
func Add_Multi_Sub(i, j int) (int, int, int) { //多值返回
return i+j, i*j, i-j
}
func sum(nums ...int) { //变参函数
total := 0
for _, num := range nums {
total += num
}
fmt.Println(total)
}
func main(){
a, b := 2,3
arr := []int{1, 2, 3}
var c int = Add(a,b)
d,e,f := Add_Multi_Sub(a,b)
fmt.Println(c,d,e,f) // 5 5 6 -1
sum(arr...) //注意传参形式 6
}
七、传值与传指针
当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。
// 简单的一个函数,实现了函数+1的操作
func add1(a int) int {
a = a + 1 // 改变了a值
return a // 返回新值
}
func main () {
x := 3
fmt.Println("x=", x) // 输出"x=3"
x1 := add1(x)
fmt.Println("x+1=", x1) // 输出"x+1=4"
fmt.Println("x=", x) // 输出"x=3"
}
虽然我们调用了add1函数,并且在add1中执行a = a+1操作,但是上面例子中x变量的值没有发生变化。
理由很简单:因为当我们调用add1的时候,add1接收的参数其实是x的copy,而不是x本身。
如果真的需要传这个x本身,该怎么办呢?
我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有add1函数知道x变量所在的地址,才能修改x变量的值。所以我们需要将x所在地址&x传入函数,并将函数的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。
// 简单的一个函数,实现了函数+1的操作
func add1(a *int) int {
*a = *a + 1 // 改变了a值
return *a // 返回新值
}
func main () {
x := 3
fmt.Println("x=", x) // 输出"x=3"
x1 := add1(&x)
fmt.Println("x+1=", x1) // 输出"x+1=4"
fmt.Println("x=", x) // 输出"x=4"
}
这样,我们就达到了修改x的目的。
那么到底传指针有什么好处呢?
1)传指针使得多个函数能操作同一个对象;
2)传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当要传递大的结构体的时候,用指针是一个明智的选择。
3)Go语言中channel,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)
八、函数作为值、类型
在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型。
type typeName func(input1 inputType1, input2 inputType2 [, ...]) (result1 resultType1 [, ...])
函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递。
type testInt func(int) bool // 声明了一个函数类型
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// 声明的函数类型作为一个参数
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
return result
}
func main() {
slice := []int {1,2,3,4,5,6,7}
fmt.Println("slice=", slice)
odd := filter(slice, isOdd) // 函数作为值传递
fmt.Println("odd elements of slice are:", odd)
even := filter(slice, isEven) // 函数作为值传递
fmt.Println("even elements of slice are:", even)
}
函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。
九、函数只能判断是否为nil
什么是nil?
In Go, nil is the zero value for pointers, interfaces, maps, slices, channels and function types, representing an uninitialized value.
func add(a, b int) int {
return a + b
}
func main() {
fmt.Println(add == nil)
//fmt.Println(add == 1) //错误 mismatched types func(int, int) int and int)
}