10、函数

函数

go函数

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

函数的作用

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

函数的分类

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

函数定义

func 函数名(参数列表) [(返回值列表)]{
	函数体 (代码块)
	[return 返回值]
}
这里[]表示其中的内容可有可无

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

函数调用

函数定义,只是声明了一个函数,它不能被执行,需要调用执行
调用的方式,就是函数名后加上小括号,如有必要在括号内填写上参数
调用时写的参数是实际参数,是实实在在传入的值,简称实参,这个过程称为传实参,简称传参
如果定义了返回值列表,就需要配合使用return来返回这些值

package main

import "fmt"

// 函数定义
// 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)  // 请问,函数定义了几次?调用了几次?可以调用几次?
}
9
21

上面代码解释:
定义一个函数add,函数名是add,能接受2个整型参数
该函数计算的结果,通过return语句返回"返回值"实现
调用时,通过函数名add后加2个参数,返回值可使用变量接收
函数名也是标识符
返回值也是值
一般习惯上函数定义需要在调用之前,也就是说调用时,函数已经被定义过了。请在书写代码时,也尽量这样做,便于阅读代码

函数调用原理

​ 特别注意,函数定义只是告诉你有一个函数可以用,但这不是函数调用执行其代码。至于函数什么时候被调用,不知道。一定要分清楚定义和调用的区别。
​ 函数调用相当于运行一次函数定义好的代码,函数本来就是为了复用,试想你可以用加法函数,我也可以用加法函数,你加你的,我加我的,应该互不干扰的使用函数。为了实现这个目标,函数调用的一般实现,都是把函数压栈(LIFO),每一个函数调用都会在栈中分配专用的栈帧,局部变量、实参、返回值等数据都保存在这里。

函数类型

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
可以看出同一种签名的函数是同一种类型

返回值

​ 返回值变量是局部变量

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

3、 返回多值

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

package main

import "fmt"

// 返回多个值
func fn4() (int, bool) {
	a, b := 100, true
	return a, b
}

func main() {
	fmt.Println(fn4())
}

100 true
package main

import "fmt"

// 返回多个值
func fn4() (i int, b bool) {
	return
}

func main() {
	fmt.Println(fn4())
}
0 false
这种写法对的 调用fn4函数时,也会被传入实参值。

返回值

  • 可以返回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语言有所不同

package main

import "fmt"

func fn6(nums ...int) { // 可变形参
	fmt.Printf("%T %[1]v, %d, %d\n", nums, len(nums), cap(nums))
}

func main() {
	fn6(1)
	fn6(3, 5)
	fn6(7, 8, 9)
}

[]int [1], 1, 1
[]int [3 5], 2, 2
[]int [7 8 9], 3, 3

可变参数收集实参到一个切片中
如果有可变参数,那它必须位于参数列表中最后。 func fn7(x, y int, nums …int, z string){} 这是错误的

unc 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

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

切片传递

func fn4(x int, y int)  {} // 多参函数
p := []int{4, 5}
fn4(p...) // 错误,这在Go中不行,不能用在非可变参数non-variadic上
package main

import "fmt"

func fn6(nums ...int) { // 可变形参
	fmt.Printf("%p, %p, %v\n", &nums, &nums[0], nums)
}

var p = []int{1, 3, 5}

func main() {
	fmt.Printf("%p, %p, %v\n", &p, &p[0], p)
	fn6(p...)
}

0xb411c0, 0xb30950, [1 3 5]
0xc000008090, 0xb30950, [1 3 5]

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

重点: 切片… 只能为可变参数传参 也就是说 只能为(nums …int) 这种传参,不能为x.y传参

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的复制

作用域

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

语句块作用域

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

	s := []int{1, 3, 5}
	for i, v := range s {
		fmt.Println(i, v) // i和v在for块中可见
	}
	fmt.Println(i, v) // 错误,在for外不可见

	if f, err := os.Open("o:/t.txt"); err != nil {
		fmt.Println(f, err) // 可见
	}
	fmt.Println(f, err) // 错误,不可见

swith、select语句中的每个子句都被视为一个隐式的代码块。

显式的块作用域

func main() {
	{
		// 块作用域
		const a = 100
		var b = 200
		c := 300
		fmt.Println(a, b, c) // 可见
	}
	fmt.Println(a, b, c) // 错误,不可见
}

包块

​ 一个package包含该包所有源文件,形成的作用域。有时在包中顶层代码定义标识符,也称为全局标识符。
​ 所有包内定义全局标识符,包内可见。包的顶层代码中标识符首字母大写则导出,从而包外可见,使用时也要加上包名。例如 fmt.Prinf() 。

函数块

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

标识符作用域

  • 标识符对外不可见,在标识符定义所在作用域外是看不到标识符的

  • 使用标识符,自己这一层定义的标识符优先,如果没有,就向外层找同名标识符——自己优先,由近及远

  • 标识符对内可见,在内部的局部作用域中,可以使用外部定义的标识符——向内穿透

  • 包级标识符

    • 在所在包内,都可见
    • 跨包访问,包级标识符必须大写开头,才能导出到包外,可以在包外使用 xx包名.VarName 方式访问。例如 fmt.Print()
  • 标识符对外不可见,在标识符定义所在作用域外是看不到标识符的

  • 使用标识符,自己这一层定义的标识符优先,如果没有,就向外层找同名标识符——自己优先,由近及远

  • 标识符对内可见,在内部的局部作用域中,可以使用外部定义的标识符——向内穿透包级标识符
    在所在包内,都可见
    跨包访问,包级标识符必须大写开头,才能导出到包外,可以在包外使用 xx包名.VarName 方
    式访问。例如 fmt.Print()

你可能感兴趣的:(go,go)