[Go语言系列] 11 Go 语言函数

Go语言系列知识快速查看入口

:point_down::point_down::point_down:


本章内容

Screen Shot 2022-01-17 at 9.49.19 PM

11.1 普通函数声明(定义)

函数是基本的代码块,用于执行一个任务。
Go语言最少有个main()函数。
函数声明告诉了编译器函数的名称、返回类型、参数。

基本语法:

func function_name( [ parameter list ] )  [ return_types ] {
   //函数体
}

(1)func: 函数由func开始声明。
(2)function_name: 函数名称,参数列表和返回值类型构成了函数签名
(3)parameter list: 参数列表。函数也可以不包含参数。
(4)return_types: 返回列表。函数也可以不包含返回列表。
(5)函数体: 函数定义的代码集合。


实例:

以下实例为 max() 函数的代码,该函数传入两个整型参数num1和num2,并返回这两个参数的最大值:

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
    /* 声明局部变量 */
    var result int
    if (num1 > num2) {
        result = num1
    } else {
        result = num2
    }
    return result
}

11.2 函数调用

当创建函数时,你定义了函数需要做什么,通过调用该函数来执行指定任务。
调用函数,向函数传递参数,并返回值。

:bulb: 函数返回一个值

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int = 200
   var ret int

   /* 调用函数并返回最大值 */
   ret = max(a, b)

   fmt.Printf( "最大值是 : %d\n", ret )
}

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
   /* 定义局部变量 */
   var result int

   if (num1 > num2) {
      result = num1
   } else {
      result = num2
   }
   return result
}

输出

最大值是: 200

:bulb: 函数返回多个值

package main

import "fmt"

func swap(x, y string) (string, string) {
   return y, x
}

func main() {
   a, b := swap("Google", "Runoob")
   fmt.Println(a, b)
}

输出

Runoob Google

11.3 函数参数

函数如果使用参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数。

传递类型 描述
值传递 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递 引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

:bulb: 值传递

值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
默认情况下,Go语言使用的是值传递,即在调用过程中不会影响到实际参数。

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int = 200

   fmt.Printf("交换前 a 的值为 : %d\n", a )
   fmt.Printf("交换前 b 的值为 : %d\n", b )

   /* 通过调用函数来交换值 */
   swap(a, b)

   fmt.Printf("交换后 a 的值 : %d\n", a )
   fmt.Printf("交换后 b 的值 : %d\n", b )
}

/* 定义相互交换值的函数 */
func swap(x, y int) int {
   var temp int

   temp = x /* 保存 x 的值 */
   x = y    /* 将 y 值赋给 x */
   y = temp /* 将 temp 值赋给 y*/

   return temp;
}

输出

交换前 a 的值为 : 100
交换前 b 的值为 : 200
交换后 a 的值 : 100
交换后 b 的值 : 200

从输出结果可以看出,经过值传递交换后两个数的值并没有发生改变。

:bulb: 引用传递

引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

package main 

import "fmt"

func main() {
	/* 定义局部变量 */
	var a int = 100
	var b int = 200

	fmt.Printf("交换前,a的值:%d\n", a)
	fmt.Printf("交换前,b的值:%d\n", b)

	/* 调用 swap() 函数
	* &a 指向 a 指针,a 变量的地址
	* &b 指向 b 指针,b 变量的地址
	*/

	swap(&a, &b)

	fmt.Printf("交换后,a的值:%d\n", a)
	fmt.Printf("交换后,b的值:%d\n", b)
}

func swap(x *int, y *int) {
	var temp int 
	temp = *x   /* 保存 x 地址上的值 */
	*x = *y     /* 将 y 值赋给 x */
	*y = temp   /* 将 temp 值赋给 y */
}

输出

交换前,a的值:100
交换前,b的值:200
交换后,a的值:200
交换后,b的值:100

从输出结果可以看出,经过引用传递交换后两个数的值发生了改变。

11.4 可变参数函数

理解本小节知识,需要先了解Go语言切片等知识,可学完这些知识,再来看本小节内容。

(1)什么是可变参数函数?

可变函数函数是指传入参数是可变数量(0到更多)的函数。在输入的变量类型前面的省略号(三点)前缀即构成一个有效的变量。

func f(names ...string)
// 声明一个可变参数名为"names",类型为string的可变参数函数。

(2)一个简单的可变参数函数

package main

import (
	"fmt"
	"strings"
)

func main() {
	// 传入0个或多个参数。
	fmt.Println(toFullName("AAA", "BBB"))
	fmt.Println(toFullName("ccc"))
	fmt.Println(toFullName())
}

// 这个函数以字符串的形式返回传递的参数,字符串之间用"+"分隔。
func toFullName(names ...string) string {
	return strings.Join(names, "+")
}

输出:

AAA+BBB
ccc
//空

(3)什么时候使用可变参数函数?

  • 省略创建仅作为函数参数创建临时slice变量
  • 当输入参数的长度未知时。
  • 表达你增加可读性的意图

(4)向可变参数函数传参

有以下的可变参数函数:

func toFullName(names ...string) string {
	return strings.Join(names, "+")
}

第一种:不传参

toFullName() //output:空值

不向可变参数函数传递任何值,就相当于向可变参数函数传递了nil切片。


第二种:直接传递参数

toFullName("aaa", "bbb", "ccc") // output:aaa+bbb+ccc

鼓励使用这种方法传参。


第三种:使用切片传参

  • ① 传递一个切片

将可变参数运算符""加在现有切片后,即可传递给可变参数。

names := []string{"aaa", "bbb", "ccc"}
toFullName(names...) // output:aaa+bbb+ccc

toFullName([]string{"aaa", "bbb", "ccc"}...) // output:aaa+bbb+ccc
  • ② 动态传递多个切片

利用append函数,append函数会创建一个新的切片,然后将names展开,然后将值依次添加到新创建的切片上,然后再将展开的结果传给 toFullname 函数:

names := []string{"aaa", "bbb"}\
toFullName(append([]string{"ddd"}, names...)...) //output:ddd+aaa+bbb
  • ③ 注意事项

假设你将一个slice作为参数传给一个可变参数函数:

dennis := []string{"dennis", "ritchie"}
toFullname(dennis...)

再假设你修改了函数中变量参数的第一项:

func toFullname(names ...string) string {
	names[0] = "guy"
	return strings.Join(names, "+")
}

修改它也会影响原始的切片。"dennis"切片现在变成了:

[]string{"guy", "ritchie"}

而不是原始值:

[]string{"dennis", "ritchie"}

因为传入的切片与函数内部的切片共享相同的底层数组,所以在函数内部改变切片的值也会影响传入的切片值。


第四种:非可变参数与可变参数混合传参

混合传参时,必须把非可变参数放在可变参数之前。不能将非可变参数放到可变参数之后。

package main

import (
	"fmt"
)

func main() {
	names := []string{"aaa", "bbb", "ccc"}
	toFullName(15, names...)
}

func toFullName(age int, names ...string) {
	fmt.Println(age, names)
}

输出:

15 [aaa bbb ccc]

参考链接:
https://cloud.tencent.com/developer/article/1763856

11.5 匿名函数

匿名函数,顾名思义,就是没有名字的函数。
Go语言支持随时在代码中定义匿名函数

(1)匿名函数声明(定义)

func( [ parameter list ]) [return_types] {
     // 函数体
}

例:

func(data int) {
    fmt.Println("hello", data)
}

(2)匿名函数调用

匿名函数只有在被调用的时候才会被初始化。

方式一:在定义时调用匿名函数

func(data int) {
    fmt.Println("hello", data)
}(100)

输出:

hello 100

方式二:将匿名函数赋值给变量,利用变量调用匿名函数

// 将匿名函数体保存到f()中
f := func(data int) {
    fmt.Println("hello", data)
}
// 使用f()调用
f(100)

输出:

hello 100

(3)匿名函数使用场景

匿名函数经常用于实现回调函数和闭包等功能

:bulb: 匿名函数用作回调函数

package main

import "fmt"

func proc(input string, processor func(str string))  {
	// 调用匿名函数
	processor(input)
}

func main()  {
	proc("王小二", func(str string) {
		for _, v := range str{
			fmt.Printf("%c\n", v)
		}
	})
}

输出:

王
小
二

上面代码中的匿名函数被作为回调函数用于对传递的字符串进行处理,用户可以根据自己的需要传递不同的匿名函数实现对字符串进行不同的处理操作。

:bulb: 匿名函数用作闭包

什么是闭包?

  • 函数中(外部函数)又定义了函数(内部函数),因此匿名函数是实现闭包的前提
  • 外部函数返回的是内部函数,相关参数和变量都保存在返回的内部函数中。
  • 内部函数可以引用外部函数的参数和局部变量
  • 调用外部函数后,返回的内部函数并没有立刻执行,而是再次调用了返回的函数之后才执行。
  • 当我们调用外部函数时,每次调用都会返回一个新的(内部)函数,即使传入相同的参数。

拥有以上特点的程序称为"闭包"。

下面看一个例子

package main

import "fmt"

func getSequence() func() int {
	i := 0 
	return func() int {
		i += 1
		return i
	}
}

func main() { 
	// nextNumber为一个函数,函数i为0
	nextNumber := getSequence()

	// 调用nextNumber函数,变量自增i并返回
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())
	fmt.Println(nextNumber())

	// 创建新的函数nextNumber1,并查看结果
	nextNumber1 := getSequence()
	fmt.Println(nextNumber1())
	fmt.Println(nextNumber1())
}

输出:

1
2
3
1
2

从上述程序中可以看出:

(1)外部函数getSequence()返回值是一个函数
(2)外部函数getSequence()返回的内部函数使用了外部函数的局部变量i
(3)在main函数中调用getSequence()函数,返回的nextNumber是一个函数
(4)只有再次调用返回的nextNumber函数,才会输出值
(5)我们调用了两次getSequence()函数,返回的nextNumber和nextNumber1是两个不同的函数,因此调用nextNumber1会重新计数(从1开始)

11.6 函数用途

函数在Go语言中属于“一等公民”,众所周知,并不是所有的编程语言中函数都是“一等公民”。

什么是“一等公民”

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为函数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。在动态类型语言中,语言运行时还支持对“一等公民”类型的检查。

基于上面关于“一等公民”的诠释,我们来看看 Go 语言的函数是如何满足上述条件而成为“一等公民”的。

(1)正常创建

func myAdd(x, y int) int {
    return x+y
} 

(2)在函数内创建

在Go语言中,可以在函数内定义一个新函数,如下面代码中在getNumber函数内部定义的匿名函数( 被赋值给变量p )。在C/C++中无法实现这一点,这也是C/C++语言中函数不是“一等公民”的原因之一。

func getNumber() {
	p := func(x int) {
		fmt.Println(x)
	}
	......
}

(3)作为类型

可以使用函数来自定义类型,如下面代码中的 HandlerFunc

// $GOROOT/src/net/http/server.go
type HandlerFunc func(ResponseWriter, *Request)

(4)存储到变量中

可以将定义好的函数存储到一个变量中,如下面代码中的p

func getNumber() {
	p := func(x int) {
		fmt.Println(x)
	}
    // 使用函数
	p(5)
}

(5)作为参数传入函数

可以将函数作为参数传入函数,比如下面代码中函数 test 的参数 f

package main

import "fmt"

// 将函数func(int)自定义为p类型
type p func(int)


func main() {
	// myPrint函数作为实参传递给test函数 
	test(3, myPrint)
}

func myPrint(x int) {
	fmt.Println(x)
}

// p类型函数作为形参传递给test函数 
func test(x int, f p) {
	f(x)
}

输出:

3

(6)作为返回值从函数返回

函数还可以被作为返回值从函数返回,如下面代码中函数test的返回值就是一个函数。
可以看出,下面test的函数形成了一个“闭包”。

package main

import "fmt"

func main() {
	p := test()
	result := p(6)
	fmt.Println(result)
}

func test() func(x int) bool {
	return func(x int) bool { return x > 5 }
}

输出:

true

参考链接:Go 函数是“一等公民”的理解 - HelloWorld开发者社区