Go 学习笔记(15)— 函数(01)[函数定义、函数特点、多值返回、实参形参、变长参数,函数调用]

1. 函数定义

Go 语言最少有个 main() 函数。函数声明告诉了编译器函数的名称,返回类型和参数。

func funcName(parameter_list)(result_list) {
   function_body
}

函数定义解析:

  • func:定义函数关键字;

  • funcName:函数名遵循标识符的命名规则,首字母大写其它包可见,首字母小写只能本包可见;

  • parameter_list:参数列表,使用 () 包裹,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数;

  • result_list:返回类型,函数返回一列值。result_list 是该列值的数据类型。有些功能不需要返回值,这种情况下 result_list不是必须的;

  • function_body:函数体使用 {} 包裹, { 必须位于函数定义行的行尾;

2. 函数特点

  1. 函数可以没有输入参数,也可以没有返回值,默认返回值为 0;
func A() {
    // do something
    ...
}

// or
func A() (int) {
    // do something
    ...
    return 1
}
  1. 多个相邻的相同类型参数可以使用简写模式;
func add(a int, b int) {
    ...
}

简写为:

func add(a, b int) {
    ...
}

以下两个声明是等价的:

func f(i, j, k int, s, t string) { /* ... */ }
func f(i int, j int, k int, s string, t string) { /* ... */ }
  1. 支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为类型零值,最后的 return 可以不带参数名直接返回;


命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。

// sum 相当于函数内的局部变量,被初始化为 0
func add(a, b int) (sum int) {
    sum = a + b
    return  // return sum 的简写模式
    // sum := a + b  则相当于新声明一个 sum 变量名,原有的 sum 变量被覆盖
    // return sum    需要显式地调用 return sum
}
  1. 不支持默认值参数;

每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时, Go 语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。

  1. 不支持函数重载;

  2. 函数作为值赋值给变量;

package main

import (
	"fmt"
)

func fire() {
	fmt.Println("fire")
}

func main() {
	var f func()// 将变量 f 声明为 func() 类型,此时 f 就被俗称为“回调函数”,此时 f 的值为 nil。
	f = fire	// 将 fire() 函数作为值,赋给函数变量 f,此时 f 的值为 fire() 函数
	f()	// 使用函数变量 f 进行函数调用,实际调用的是 fire() 函数。
}

  1. 不支持命名函数嵌套,但支持匿名函数的嵌套;
func add(a , b int) (sum int) {
    anonymous:= func(x , y int) int {
        return x + y
    }	
    
	return anonymous(a , b)
}
  1. 函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。
package main

import "fmt"

func test(fn func() int) int {
	return fn()
}

// FormatFunc 定义函数类型。
type FormatFunc func(s string, x, y int) string

func format(fn FormatFunc, s string, x, y int) string {
	return fn(s, x, y)
}
func main() {
	s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
	s2 := format(func(s string, x, y int) string {
		return fmt.Sprintf(s, x, y)
	}, "%d, %d", 10, 20)
	println(s1, s2)
}

  1. 命名返回参数允许 defer 延迟调用通过闭包读取和修改。
package main

func add(x, y int) (z int) {
	defer func() {
		z += 100
	}()
	z = x + y
	return
}

func main() {
	println(add(1, 2)) // 输出: 103
}

  1. 显式 return 返回前,会先修改命名返回参数。
package main

func add(x, y int) (z int) {
	defer func() {
		println(z) // 输出: 203
	}()
	z = x + y
	return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (ret)
}

func main() {
	println(add(1, 2)) // 输出: 203
}

  1. 在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、 slicemapfunctionchannel 等类型,实参可能会由于函数的间接引用被修改。

3. 多值返回

Go 支持多值返回,定义多值返回的返回参数列表时要使用 () 包含,支持命名参数的返回。

3.1 不带命名参数的返回值

如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致,示例代码如下:

func swap(a, b int) (int, int) {
    return b, a
}

如果多值返回有错误类型,一般将错误类型作为最后一个返回值。

不能用容器对象接收多返回值。只能用多个变量或 “_” 忽略。

package main

func test() (int, int) {
	return 1, 2
}

func main() {
	// s := make([]int, 2)
	// s = test() // Error: multiple-value test() in single-value context
	x, _ := test()
	println(x)
}

多返回值可直接作为其他函数调用实参。

package main

func test() (int, int) {
	return 1, 2
}

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

func sum(n ...int) int {	// 不定参数
	var x int
	for _, i := range n {
		x += i
	}
	return x
}

func main() {
	println(add(test()))
	println(sum(test()))
}

3.2 带命名参数的返回值

Go 语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。

命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。

下面代码中的函数拥有两个整型返回值,函数声明时将返回值命名为 a 和 b,因此可以在函数体中直接对函数返回值进行赋值,在命名的返回值方式的函数体中,在函数结束前需要显式地使用 return 语句进行返回,代码如下:

package main

import (
	"fmt"
)

func main() {
	x, y := namedRetValues()
	fmt.Println(x, y) // "5"
}


func namedRetValues() (a, b int) {	// 对两个整型返回值进行命名,分别为 a 和 b

	a = 1	// 命名返回值的变量与这个函数的局部变量的效果一致,可以对返回值进行赋值和值获取。
	b = 2
    // 当函数使用命名返回值时,可以在 return 中不填写返回值列表,如果填写也是可行的,
	return	// 等价 return a, b 
}

4. 实参形参

Go 函数实参到形参的传递永远是值传递,除非参数传递的是指针值得拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址, 本质上参数传递仍是值拷贝。

package main

import "fmt"

func main() {
	a := 10
	addOne(a)
	fmt.Println("main a is ", a)
	fmt.Println("main a address is ", &a)
	addPointer(&a) // 实参给形参传递时仍然是值拷贝,传递的是 a 的地址
	fmt.Println("main a is ", a)

}

func addOne(a int) int {
	a = a + 1
	fmt.Println("addOne a is ", a)
	return a
}

func addPointer(a *int) {
	fmt.Println("addPointer a address is ", a)
	*a = *a + 1
	fmt.Println("addPointer a address is ", a)
	fmt.Println("addPointer a is ", *a)
	return
}

输出结果:

addOne a is  11
main a is  10
main a address is  0xc000016068
addPointer a address is  0xc000016068
addPointer a address is  0xc000016068
addPointer a is  11
main a is  11

5. 不定参数

不定参数也叫作可变参数,是指函数传入的参数个数是可变的,Go 函数支持不定数目的形式参数,不定参数声明使用 param ...type 的语法格式。

5.1 类型相同的不定参数

为了做到这点,首先需要将函数定义为可以接受可变参数的类型:
``

package main

import (
	"fmt"
)
// 函数 myfunc() 接受不定数量的参数,这些参数的类型全部是 int,
func myfunc(args ...int) {
	for _, arg := range args {
		fmt.Println(arg)
	}
}
func main() {

	myfunc(1, 2, 3)
	myfunc(10, 20, 30)

}

从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type,这也是为什么上面的参数 args 可以用 for 循环来获得每个传入的参数。

假如没有...type这样的语法糖,开发者将不得不这么写:

func myfunc2(args []int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

从函数的实现角度来看,这没有任何影响,该怎么写就怎么写,但从调用方来说,情形则完全不同:

myfunc2([]int{1, 3, 7, 13})

大家会发现,我们不得不加上[]int{}来构造一个数组切片实例,但是有了...type这个语法糖,我们就不用自己来处理了。

函数的不定参数特点:

  1. 所有的不定参数类型必须是相同的;

  2. 不定参数必须是函数的最后一个参数;

  3. 不定参数在函数体内相当于切片,对切片的操作同样适合对不定参数的操作;

  4. 切片可以作为参数传递给不定参数,切片名后要加上 ...

  5. 形参为不定参数的函数和形参为切片的函数类型是不相同的;

可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加...这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。

package main

import "fmt"

func main() {
	n := []int{1, 2, 3, 4, 5}
	result := sumOne(n...) // 切片元素作为参数传递给函数的不定参数,需要在切片名后加上 ...
	fmt.Println("result is ", result)
	ret := sumTwo(n)	// 传递切片自身
	fmt.Println("ret is ", ret)
	fmt.Printf("sumOne type is %T\n", sumOne) // sumOne type is func(...int) int
	fmt.Printf("sumTwo type is %T\n", sumTwo) // sumTwo type is func([]int) int

}

func sumOne(a ...int) (ret int) {
	for _, v := range a { // 不定参数相当于切片,可以使用 range 访问
		ret += v
	}
	return
}

func sumTwo(a []int) (ret int) {
	for _, v := range a {
		ret += v
	}
	return
}

5.2 类型不同的不定参数

interface{} 传递任意类型数据是 Go 语言的惯例用法,使用 interface{} 仍然是类型安全的,下面通过示例来了解一下如何分配传入 interface{} 类型的数据。

package main
import "fmt"
func MyPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {
            case int:
                fmt.Println(arg, "is an int value.")
            case string:
                fmt.Println(arg, "is a string value.")
            case int64:
                fmt.Println(arg, "is an int64 value.")
            default:
                fmt.Println(arg, "is an unknown type.")
        }
    }
}
func main() {
    var v1 int = 1
    var v2 int64 = 234
    var v3 string = "hello"
    var v4 float32 = 1.234
    MyPrintf(v1, v2, v3, v4)
}

输出结果:

1 is an int value.
234 is an int64 value.
hello is a string value.
1.234 is an unknown type.

6. 函数调用

函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行,调用前的函数局部变量都会被保存起来不会丢失,被调用的函数运行结束后,恢复到调用函数的下一行继续执行代码,之前的局部变量也能继续访问。

函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。

Go语言的函数调用格式如下:

返回值变量列表 = 函数名(参数列表)

下面是对各个部分的说明:

  • 函数名:需要调用的函数名。
  • 参数列表:参数变量以逗号分隔,尾部无须以分号结尾。
  • 返回值变量列表:多个返回值使用逗号分隔。

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针, slice (切片)、 mapfunctionchannel 等类型,实参可能会由于函数的间接引用被修改。

你可能感兴趣的:(Go)