Go函数

函数

数学定义

  • y=f(x) ,y是x的函数,x是自变量。y=f(x0,x1,…,xn)

Go函数

  • 由若干语句组成的语句块、函数名称、参数列表、返回值构成,他是组织代码的最小单元
  • 完成一定的功能

函数的作用

  • 结构化编程对代码的最基本的封装,一般按照功能组织一段代码
  • 封装的目录是为了复用,减少冗余代码
  • 代码更加简洁美观、可读易懂

函数的分类

  • 内建函数,如make、new、panic等
  • 库函数,如math.Ceil()等
  • 自定义函数,使用func 关键字定义

函数定义

func 函数名(参数列表) 返回值{
    函数体
    [return 返回值]
}
return可有可无
  • 函数名就是标识符,命名要求一样
  • 定义中的参数列表成为形式参数,只是一种符号表达,简称形参
  • 返回值列表可有可无,需要return语句配合,表示一个功能函数执行完返回的结果
  • 函数名(参数列表) [返回值列表]称为函数签名
  • Go语言中形参也 被称为入参,返回值也被称为出参

函数调用

  • 函数定义,只是声明了一个函数,它不能被执行,需要调用执行
  • 调用的方式就是函数名后面加上小括号,如果必要在括号中添加参数
  • 调用时写的参数是实际参数,简称实参
  • 如果定义了返回值列表,就需要配合使用return来返回这些值
// 函数定义
// x、y是形式参数,result是返回值
func add(x, y int) int {
 result := x + y // 函数体
 return result   // 返回值
}
func main() {
 out := add(4, 5)  // 函数调用,可能有返回值,使用变量接收这个返回值
 fmt.Println(out)  // 对于Println函数来说,这也是调用,传入了实参out
 out = add(10, 11) // 请问,这次函数调用和上次有没有关系?  没有关系
 fmt.Println(out)  // 请问,函数定义了几次?调用了几次?可以调用几次?  定义了一次,可以调用无数次
}

上面代码解释:

  • 定义一个函数add,函数名是add,能接受2个整型参数
  • 该函数计算的结果,通过return一句返回“返回值” 实现
  • 调用时,通过函数名add后加两个参数,返回值可使用变量接受
  • 函数名也是标识符
  • 返回值也是值
  • 一般习惯上函数定义需要再调用之前,也就是说调用时,函数已经被定义过了。

函数调用原理

函数调用相当于运行一次函数定义好的代码,函数本来就是为了复用,比如你可以用加法函数,我也可以用加法函数,你加你的,我加我的,应该互不干扰的使用函数。为了实现这个目标,函数调用的一般实现,都是吧函数压栈(LIFO)后进先出,每一个函数调用都会在栈中分配专用的栈帧,局部变量、实参、返回值等数据都保存在这里

上面的代码,首先调用main函数,main函数压栈,接着调用add(4,5)时,add函数压栈,压在main的栈帧上,add调用return,将add返回值保存在main栈帧的本地变量out上,add栈帧消亡,回到main栈帧上

package main
import "fmt"
func fn1()             {}
func fn2(i int) int     { return 100 }
func fn3(j int) (r int) { return 200 }
func main() {
 fmt.Printf("%T\n", fn1)
 fmt.Printf("%T\n", fn2)
 fmt.Printf("%T\n", fn3) }
// 输出如下
func()
func(int) int
func(int) int

返回值

参考 https://go.dev/ref/spec#Return_statements

  • 返回值变量是局部变量
1、无返回值

在Go语言中仅仅一个return并不一定表示无返回值,只能说在一个无返回值的函数中,return表示无返回值函数返回。

// 无返回值函数,可以不使用return,或在必要时使用return
func fn1() {
 fmt.Println("无返回值函数")
 return // return可有可无,如有需要,在必要的时候使用return来返回
}t := fn1()         // 错误,无返回值函数无返回值可用
fmt.Println(fn1()) // 错误,无返回值函数无返回值可打印
2、返回一个值

在函数体中,必须显式执行return

// 返回一个值,没有变量名只有类型。匿名返回值
func fn2() int {
 a := 100
 return a + 1 // return后面只要类型匹配就行
}
fmt.Println(fn2()) // 返回101
t := fn2()         // 返回101
//上面的函数还可以写成下面的形式
func fn3() (r int) {
 r = 200
 return // 如果返回的标识符就是返回值列表中的标识符,可以省略
}
fmt.Println(fn3())

Go语言中返回值不允许复制给一个常量

3、返回多值

Go语言是运行函数返回多个值

// 返回多个值
func fn4() (int, bool) {
    a, b := 100, true
 return a, b }
fmt.Println(fn4())
x, y := fn4() // 需要两个变量接收返回值
// 返回多个值
func fn4() (i int, b bool) {
 i, b = 100, true
 return // 如果和返回值列表定义的标识符名称和顺序一样,可省略
}
fmt.Println(fn4())
x, y := fn4() // 需要两个变量接收返回值
// 下面写法对吗?
func fn4() (i int, b bool) {
 return
}

上面写法正确,因为返回值i、b也是函数的局部变量,调用fn4函数时,也会被传入实参,零值可用,返回0,false

// 注意下面写法的错误
func fn5() (i int, err error) {
 if _, err := os.Open("o:/t"); err != nil {
        return // 错误,因为err被重新定义,只能在if中使用,返回值的err就被覆盖了,就是上
一行:=的问题
        // return -1, err // 正确
 }
    return
}

返回值

  • 可以返回0个或多个值
  • 可以在函数定义中写好返回值参数列表
    • 可以没有标识符,只写类型。但是有时候不便于代码阅读,不知道返回参数的含义
    • 可以和形参一样,写标识符和类型来命名返回值变量,相邻类型相同可以合并写
    • 如果返回值参数列表中只有一个返回参数值类型,小括号可以省略
    • 以上2种方式不能混用,也就是返回值参数要么都命名,要么都不命名
  • return
    • return之后的语句不会执行,函数将结束执行
    • 如果函数无返回值,函数体内根据实际情况使用return
    • return后如果写值,必须写和返回值参数类型的个数一直的数据
    • return后什么都不写那么久使用返回值参数列表中的返回参数的值

形式参数

  • 可以无形参,也可以多个形参
  • 不支持形式参数的默认值
  • 形参是局部变量
func fn1()                   {} // 无形参
func fn2(int)               {} // 有一个int形参,但是没法用它,不推荐
func fn3(x int)             {} // 单参函数
func fn4(x int, y int)       {} // 多参函数
func fn5(x, y int, z string) {} // 相邻形参类型相同,可以写到一起
fn1()
fn2(5)
fn3(10)
fn4(4, 5)
fn5(7, 8, "ok")

可变参数

可变参数variadic。其他语言也有类似的被称为剩余参数,但Go语言有所不同

func fn6(nums ...int) { // 可变形参
 fmt.Printf("%T %[1]v, %d, %d\n", nums, len(nums), cap(nums))
}
fn6(1)       // []int, [1]
fn6(3, 5)    // []int, [3 5]
fn6(7, 8, 9) // []int, [7 8 9]
  • 可变参数收集实参到一个切片中
  • 如果有可变参数,那它必须位与参数列表中最后,func fn7(x, y int, nums …int, z string){} 这是错误的
func fn7(x, y int, nums ...int) {
 fmt.Printf("%d %d; %T %[3]v, %d, %d\n", x, y, nums, len(nums), cap(nums))
}
fn7(1, 2)       // 1 2; []int [], 0, 0
fn7(1, 2, 3)    // 1 2; []int [3], 1, 1
fn7(1, 2, 3, 4) // 1 2; []int [3 4], 2, 2

可以看出有剩下的实参才留给剩余参数。

Map传递

package main // 同一个包内可见
import "fmt"

// 函数形参如何传map?
func test(m interface{}) {
	fmt.Printf("函数中1:%T  %[1]v  %[1]p\n", m)
	p := m.(map[string]int)
	fmt.Printf("函数中2:%T  %[1]v  %[1]p\n", p)
	p["a"] = 12312

}
func main() { 
	c := map[string]int{}
	fmt.Printf("原始1:%T %p %v\n", c, c, c)
	c["a"] = 123
	c["b"] = 456
	fmt.Printf("原始2:%T %p %v\n", c, c, c)
	test(c)
	fmt.Printf("原始3:%T %p %v\n", c, c, c)
}
//输出
原始1map[string]int 0xc000096060 map[]
原始2map[string]int 0xc000096060 map[a:123 b:456]
函数中1:map[string]int  map[a:123 b:456]  0xc000096060
函数中2:map[string]int  map[a:123 b:456]  0xc000096060
原始3map[string]int 0xc000096060 map[a:12312 b:456]

总结: 初始化map后,返回的是指针变量,在函数之间,传递的是map的地址。在函数修改map会在主程序中表现出来,说明map虽然值传递,但是最终指向都是引用类型。

切片传递

也可以使用切片传递传递给可变参数。

package main // 同一个包内可见
import (
	"fmt"
)

//func fn5() (i int, err error) {
//	if _, err := os.Open("p:/c"); err != nil {
//		return -1, err
//	}
//	return i, err
//
//}

// 函数形参如何传map?
func fn6(nums ...string) {
	fmt.Printf("%T  %[1]v  %d  %d  %p %p\n", nums, len(nums), cap(nums), &nums, &nums[0])
}
func main() { // main函数叫做入口函数,go约定main函数必须在main包中定义
	p := []string{"haha", "cccc"}
	fmt.Printf("%p %p\n", &p, &p[0])
	fn6(p...)
}
//输出
0xc000010030 0xc000108000
[]string  [haha cccc]  2  2  0xc000010048 0xc000108000

可以看到,这种方式并不是把p这个切片分解了,然后传递给fn6函数,在封装成一个新的切片nums,而是相当于切片header的复制

func fn7(x, y int, nums ...int) {
 fmt.Printf("%d %d; %T %[3]v, %d, %d\n", x, y, nums, len(nums), cap(nums))
}
p := []int{4, 5, 6}
fn7(p...) // 这在Go中不行,报奇怪的错,原因还是不能用在非可变参数上,就用4、5用在x、y上了
// 这个例子,本以为p被分解,4和5分别对应x和y,6被可变参数nums收集,但是这在Go语言中是错误的

If the final argument is assignable to a slice type []T and is followed by … , it is passedunchanged as the value for a …T parameter.

如果最终的参数是某类型的切片且其后跟着…,它将无变化的传递给…T的可变参数。注意,这个过程无新的切片创建。

帮助文档这一句话,原来指的是, 切片… 只能为可变参数传参。

func fn7(x, y int, nums ...int) {
 fmt.Printf("%d %d; %T %[3]v, %d, %d\n", x, y, nums, len(nums), 
cap(nums))
}p := []int{4, 5}
fn7(p...)          // 错误,不能用在普通参数上
fn7(1, p...)       // 错误,不能用在普通参数上
fn7(1, 2, 3, p...) // 错误,不能用2种方式为可变参数传参,不能混用
// fn7(1, 2, p..., 9, 10) // 语法错误
// fn7(1, 2, []int{4, 5}..., []int{6, 7}...) // 语法错误,不能连续使用p...,只能一次
// 正确的如下
fn7(1, 2, []int{4, 5}...)
fn7(1, 2, p...)
fn7(1, 2, 3, 4, 5)

可以看出,可变参数限制较多

  • 直接提供对应实参,封装成一个新的切片
  • 可以使用切片传递的方式 切片... 但是这种方式只能单独为可变形参提供实参,因为这是实参切片的header的复制

问题:

func fn6(nums []int) {} func fn6(nums ...int) {} 调用时,都可以使用切片,那有什么区别呢?形参使用切片类型还是可变参数呢?

前者在调用时实参只能传入切片类型,而后者可以传入对应类型的元素,会自动添加到切片中,

作用域

函数会开辟一个局部作用域,其中定义的标识符仅能在函数之中使用,也称为标识符在函数中的可见范围。

这种对标识符约束的可见范围,称为作用域

1、语句块作用域

if、for、switch等语句中使用短格式定义的变量,可以认为就是该语句块的变量,作用域仅在该语句块中。

s := []int{1,3,5}
for i,v := range s {
    fmt.Println(i,v)
}
fmt.Println(i,v) //错误,i、v是for的局部变量,对外不可访问
if i,err := os.Open("o:/c"); err !=nil {
    fmt.Println(i,err)
}

switch、select语句中的每个字句都被视为一个隐式的代码块

2、显式的块作用域

在任何一个大括号中定义的标识符,其作用域只能在这对大括号中。

{
    const a = 100
    var b = 200 
    c := 300
    fmt.Println(a,b,c) // 可见

}
fmt.Println(a,b,c) // 不可见
3、universe块

宇宙块,意思就是全局块,不过是语言内建的。预定义的标识符就在这个全局环境中,因此bool、int、nil、true、false、iota、append等标识符全局可见,随处可见。

4、包块

每一个package包含该包所有源文件,形成的作用域。有时在包中顶层代码定义标识符,也称为全局标识符。

所有包内定义全局标识符,包内可见。包的顶层代码中标识符首字母大写则到处,从而包外可见,使用时也要加上包名,例如fmt.Printf()

5、函数块

函数声明的时候使用了花括号,所以整个函数体就是一个显示的代码块。这个函数就是一个块作用域

package main

import "fmt"

const a = 100

var b = 200

// c := 300
var d = 400

func showB() int {
	return b
}

func main() {
	fmt.Println(1, a) // 1,100  ,地址和上面相同
	// fmt.Println(1.1, &a)
	var a = 500
	fmt.Println(2, a, &a) // 2, 500 地址更换

	fmt.Println(3, b, &b) //3, 200地址和上面相同
	b = 600
	fmt.Println(3.1, b, &b) // 3.1 600 地址和上面相同
	b := 601
	fmt.Println(3.2, b, &b)   // 3.2 601 地址更换
	fmt.Println(3.3, showB()) // 3.3 600 因为showB和变量b在同一级别下
	{
		const j = 'A'
		var k = "magedu"
		t := true
		a = 700
		b := 800
		fmt.Println(4, a, b, d, j, k, t) // 4, 700 800 400 'A' "magedu" true
		{
			x := 900
			fmt.Println(4.1, a, b, d, j, k, t, x) // 4.1 700 800 400 'A' "magedu" true 900
		}
		//fmt.Println(4.2, x)  // 4.2 报错了
	}
	//fmt.Println(4.3, j, k, t)  // 4.3 报错
	fmt.Println(4.4, a, b) // 4.4 700 601
	for i, v := range []int{1, 3, 5} {
		fmt.Println(i, v) //
	}
	//fmt.Println(i, v) //报错
}

标识符作用域

  • 标识符对外不可见,在标识符定义所在作用域外是看不到标识符的
  • 使用标识符,自己这一层定义的标识符优先,如果没有,就向外层找同名标识符——自己优先,由近及远
  • 标识符对内可见,在内部的局部作用域中,可以使用外部定义的标识符——向内穿透
  • 包级标识符
    • 在所在包内,都可见
    • 跨包访问,包级标识符必须大写开头,才能导出到包外,可以在包外使用xx包名.VarName方式访问。例如fmt.Println()

问题:

func fn6(nums []int) {} 和 func fn6(nums ...int) {} 调用时,都可以使用切片,那有什么区别呢?形参使用切片类型还是可变参数呢?

func fn6(nums []int) {} 可以使用 fn6([]int{1, 3}) 一种传参方式。本质上是切片header的复制。

func fn6(nums ...int) {} 可以使用 fn6([]int{1, 3}...) 和 fn6(1, 3) 两种传参方式。切片传递本质上也是切片header的复制。