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

  • Post author:
  • Post category:其他




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. 在函数中,实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、

    slice



    map



    function



    channel

    等类型,实参可能会由于函数的间接引用被修改。



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

,通过

reflect.ValueOf(args).Kind()

可以看到其类型为

slice

,这也是为什么上面的参数

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.

如果在创建函数时,可变参数存在多种数据类型,则可以将可变参数类型设置成

interface{}

,示例代码如下:

package main
import (
    "fmt"
    "reflect"
)
func demo(a string, i int, other ...interface{}) {
    fmt.Println("first:", a)
    fmt.Println("other", i)
    fmt.Println("variable parameter:", other, "variable parameter type:", reflect.ValueOf(other).Kind())
    for index, val := range other {
        fmt.Println("variable parameter index:", index, ",variable parameter value:", val, ",variable parameter type:", reflect.ValueOf(val).Kind())
    }
}
func main() {
    demo("hello", 10, 100, "b", 3.1415926)
}

输出结果:

first: hello
other 10
variable parameter: [100 b 3.1415926] variable parameter type: slice
variable parameter index: 0 ,variable parameter value: 100 ,variable parameter type: int
variable parameter index: 1 ,variable parameter value: b ,variable parameter type: string
variable parameter index: 2 ,variable parameter value: 3.1415926 ,variable parameter type: float64



5.3 获取可变参数类型

package main

import (
	"bytes"
	"fmt"
)

func printTypeValue(slist ...interface{}) string {
	// 字节缓冲作为快速字符串连接
	var b bytes.Buffer
	// 遍历参数
	for _, s := range slist {
		// 将interface{}类型格式化为字符串
		str := fmt.Sprintf("%v", s)
		// 类型的字符串描述
		var typeString string
		// 对s进行类型断言
		switch s.(type) {
		case bool: // 当s为布尔类型时
			typeString = "bool"
		case string: // 当s为字符串类型时
			typeString = "string"
		case int: // 当s为整形类型时
			typeString = "int"
		}

		// 写值字符串前缀
		b.WriteString("value: ")
		// 写入值
		b.WriteString(str)
		// 写类型前缀
		b.WriteString(" type: ")
		// 写类型字符串
		b.WriteString(typeString)
		// 写入换行符,一个输入变量一行
		b.WriteString("\n")
	}
	return b.String()
}

func main() {
	// 将不同类型的变量通过printTypeValue打印出来
	fmt.Println(printTypeValue(100, "str", true))
}

输出结果:

value: 100 type: int
value: str type: string
value: true type: bool



6. 函数调用

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

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

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

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

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

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

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,

slice

(切片)、

map



function



channel

等类型,实参可能会由于函数的间接引用被修改。



7. 函数作为参数调用

package main

//定义一个函数类型,两个 int 参数,一个 int 返回值
type math func(int, int) int

//定义一个函数 add,这个函数两个 int 参数一个 int 返回值,与 math 类型相符
func add(i int, j int) int {
	return i + j
}

//再定义一个 multiply,这个函数同样符合 math 类型
func multiply(i, j int) int {
	return i * j
}

//foo 函数,需要一个 math 类型的参数,用 math 类型的函数计算第 2 和第 3 个参数数字,并返回计算结果
//稍后在 main 中我们将 add 函数和 multiply 分别作为参数传递给它
func foo(m math, n1, n2 int) int {
	return m(1, 2)
}

func main() {
	//传递 add 函数和两个数字,计算相加结果
	n := foo(add, 1, 2)
	println(n)

	//传递 multply 和两个数字,计算相乘结果
	n = foo(multiply, 1, 2)
	println(n)
}

输出结果

3
2

示例代码

package main

import "fmt"

func funcA(f func() string, s1, s2 string) string {
	ret := f()
	return ret + s1 + s2
}

func main() {
	fu := func() string {
		return "hello,world"
	}
	ret := funcA(fu, " 你好", " 世界")
	fmt.Println(ret)
}



版权声明:本文为wohu1104原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。