GO学习之 函数的进阶
一.函数的调用机制
1.函数的调用过程
函数的局部变量与全局变量
举例:
package main
import "fmt"
// 一个test函数
func test(n1 int){
n1 += 1
fmt.Println("n1=",n1)
}
// 一个get函数
func getSum(n1 int, n2 int) int {
//
sum := n1 + n2
return sum
}
func main(){
// 调用 函数
n1 := 10
test(n1)
get_sum := getSum(1, 2)
fmt.Println("n1=",n1)
fmt.Println("get_sum",get_sum)
}

程序在运行时,在调用函数时,会在栈空间中分配给函数一个空间。当调用完毕后,空间被进行回收。
return 语句:
注意go 语言return支持多个返回值
基本语法:
func 函数名(形参列表) (返回值类型列表){
语句....
return 返回值列表
}
1)如果返回多个值时,在接收时,希望忽略某个返回值,则使用’_’符号表示占位忽略
2)如果返回值只有一个 (返回值类型列表)可以不写 ()
多个返回值与 _ 占位举例:
package main
import "fmt"
// 返回两个值的和和查
func getSumAndSub(a int, b int) (int, int){
sum := a + b
sub := a - b
return sum,sub
}
func main(){
// 调用 函数
n1 := 10
test(n1)
get_sum := getSum(1, 2)
fmt.Println("n1=",n1)
fmt.Println("get_sum",get_sum)
// 返回多个值
re1,re2 := getSumAndSub(3,2)
fmt.Println(re1)
fmt.Println(re2)
// 希望忽略某个返回值,用 _ 符号表示占位符
_,re3 := getSumAndSub(5,4)
fmt.Println(re3)
}
2.函数的递归调用
基本介绍
一个函数在函体内又调用了本身,我们称为递归调用。
举例:
package main
import (
"fmt"
)
func test(n int) {
if n > 2 {
n--
test(n) // 把test()函数全部带入
}
fmt.Println("n=", n)
}
func main() {
test(4)
}
举例2:
package main
import (
"fmt"
)
func test2(n int) {
if n > 2 { // 注意执行过if 不在执行else
n--
test(n) // 把test()函数全部带入
} else {
fmt.Println("n=", n) // n = 2
}
}
func main() {
test2(4)
}
函数递归需要遵守的重要原则:
1_执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)
2_函数的局部变量时独立的,不受相互影响
3_递归必须向退出递归的条件逼近,有穷性
4_当一个函数执行完毕,或遇到return,就会返回,遵守谁调用,就将结果返回给谁。
同时当函数执行完毕或者返回时,该函数本身也会被销毁。
递归经典练习题:
练习01:斐波那契数
package main
import "fmt"
func fb(n int ) int {
if (n == 2 || n == 1) {
return 1
} else {
return fb(n-1) + fb(n-2)
}
}
func main(){
//
num := fb(5)
fmt.Println("num:",num) // 输出第几个的斐波那契数
}
练习02:求函数值
已知f(1) = 3;f(n) = 2*f(n-1) + 1;
请使用递归的思想编程,求出f(n)的值
package main
import "fmt"
func test(n int) int {
if n == 1 {
return 3
} else {
return 2 * test(n-1) + 1
}
}
func main(){
fmt.Println("f(1)=",test(1)) // 3
fmt.Println("f(5)=",test(5)) // 63
}
练习03:猴子吃桃吃一半多一个,10天剩一个,原来桃子多少
package main
import "fmt"
func test(n int) int {
if n == 10 {
return 1
} else {
return 2 * (test(n+1) + 1)
}
}
func main(){
fmt.Println(test(5))
}
3.函数的主要事项和细节
1_函数的形参可以是多个,返回列表也可以是多个
2_形参列表和返回列表的数据类型可以是值类型和引用类型
3_函数的命名遵循标识符命名规范,首字母不能是数字。首字母大写包能被本包问号和其他问号使用,类似pubilc,首字母小写只能在本包使用,其他文件不能使用,类似private。
4_函数中的变量是局部的,函数外不生效
5_基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改不会影响到原来的值。
6_如果希望函数内的变量能修改函数外的变量,可以传入变的地址&,函数内以指针的方式操作变量。从效果看类似引用。
7_GO函数不能重载,函数名相同不支持
8_在GO中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
package main
import "fmt"
func getSum(n1 int,n2 int) int {
return n1 + n2
}
func main(){
// 在GO中,函数也是一种数据类型,
// 可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
a := getSum
fmt.Printf("a的类型%T,getSum类似是%T\n",a,getSum)
res := getSum(10,40)
fmt.Println("res=",res)
}
9_函数既然是一种数据类型,因此在GO中,函数可以作为形参,并且调用。
package main
import "fmt"
func getSum(n1 int,n2 int) int {
return n1 + n2
}
// 函数既然是一种数据类型,因此在GO中,函数可以作为形参,并且调用。
func myFun(funvar func(int, int) int, num1 int , num2 int) int {
return funvar(num1, num2)
}
func main(){
// 在GO中,函数也是一种数据类型,
// 可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。
a := getSum
fmt.Printf("a的类型%T,getSum类似是%T\n",a,getSum)
res := getSum(10,40)
fmt.Println("res=",res)
res2 := myFun(getSum, 50, 60)
fmt.Println("res2=",res2)
}
11_为了简化数据类型定义,GO支持自定义数据类型。
基本语法:type自定义数据类型名 数据类型 // 理解:相当于一个别名
举例:type myint int // 这时myint就等价于int来使用
package main
import "fmt"
func main(){
type myInt int // 给int取别名 ,在go中myInt和int虽然都是int类型,但是go认为myInt和int还是不同的类型
var num1 myInt
var num2 int
fmt.Printf("num1类型%T,num2类型%T",num1,num2) // 打印:num1类型main.myInt,num2类似int
}
举例02:type mySum func(int, int) int // 这时mySum就等价一个函数类型 func(int,int) int
package main
import "fmt"
// 案例2
type myFunType func(int, int) int // 这时myFun就是func(int,int) int 类型
func getSum(n1 int, n2 int) int {
return n1 + n2
}
func myFun(funvar myFunType, num1 int, num2 int) int {
return funvar(num1, num2)
}
func main() {
type myInt int // 给int取别名 ,在go中myInt和int虽然都是int类型,但是go认为myInt和int还是不同的类型
var num1 myInt
var num2 int
fmt.Printf("num1类型%T,num2类型%T\n", num1, num2) // 打印:num1类型main.myInt,num2类似int
res := myFun(getSum, 500, 600)
fmt.Println(res) \\ 1100
}
12_支持对返回值命名
package main
import "fmt"
func getSumAndSub(n1 int, n2 int) (sum int, sub int) {
sum = n1 + n2
sub = n1 - n2
return
}
func main() {
//
a, b := getSumAndSub(3, 2)
fmt.Println(a, b)
}
13.使用_标识符,忽略返回值
14.在GO中支持可变参数
语法格式:
// 支持0到多个参数
func sum(args...int) sum int {
}
// 支持1到多个参数
func sum(n1 int,args...int) sum int {
}
说明:
(1)args 是slice 切片(类似数组),通过args[index] 可以访问到各个值。
举例:编写一个函数sum ,可以求出1到多个int的和
package main
import "fmt"
func sum(n1 int,args...int) int {
sum := n1
for i := 0; i < len(args); i ++ {
sum += args[i] // args[0] 表示第一个元素的值 其他以此类推
}
return sum
}
func main(){
// 编写一个函数sum ,可以求出1到多个int的和
res := sum(5,6,7,8,1,2,3)
fmt.Println(res)
}

练习:使用函数对两个值进行交换
package main
import "fmt"
func swap(n1 *int, n2 *int) { // 指针接收地址进行值的相应交换
// 定义一个临时变量
t := *n1
*n1 = *n2
*n2 = t
}
func main() {
a := 10
b := 20
swap(&a, &b) // 传入的是地址
fmt.Printf("a=%v,b=%v", a, b)
}
二.常用函数的介绍
1.init函数
基本介绍:
每一个源文件都可以包含一个init函数,该函数在main函数执行前,被GO运行框架调用,也就是说init会在main函数前被调用。
package main
import "fmt"
func init() {
fmt.Println("run init")
}
func main(){
fmt.Println("run main")
}
// 打印结果:
// run init
// run main
init函数的注意事项和细节
(1)如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程全局变量定义--init函数-->main函数
package main
import "fmt"
var age = test()
func test() int {
fmt.Println("run test")
return 90
}
func init() {
fmt.Println("run init")
}
func main(){
fmt.Println("run main")
fmt.Println("age=",age)
}
// 得出结论:先执行变量定义,再执行test-->init -- > main
// 打印结果:
// run test
// run init
// run main
// age= 90
(2)init 函数最主要的作用就是完成一些初始化的工作。(就是定义一个初始化的变量)
(3)当一个main函数从另外文件引入包的时候,优先执行包中的init函数
2.匿名函数
GO支持匿名函数,如果我们某个函数只希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
匿名函数使用方式1:
在定义匿名函数时就直接调用(这种方式只能调用一次)
举例01:
package main
import "fmt"
func main() {
// 在定义匿名函数时就直接调用
// 使用匿名函数求两个数和
res := func (n1 int,n2 int) int {
return n1 + n2
}(10,20)
fmt.Println(res)
}
匿名函数使用方式2:
将匿名函数赋值给一个变量(函数变量),再通过该变量来调用匿名函数
举例02:
package main
import "fmt"
func main() {
// 将匿名函数func (n1 int n2 int) int 赋给 a 变量
// 则 a 的数据类型就是函数类型 此时 我们可以通过a完成调用
a := func (n1 int,n2 int) int {
return n1 - n2
}
// 可以多次调用 多态???
res2 := a(30,10)
fmt.Println(res2)
res3 := a(40,10)
fmt.Println(res3)
}
全局匿名函数
如果将匿名函数赋值给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在整个程序中有效。
举例:
package main
import "fmt"
var (
// Func1 为全局匿名函数
Func1 = func(n1 int,n2 int) int {
return n1 * n2
}
)
func main() {
// 全局匿名函数的使用
res4 := Func1(4,5)
fmt.Println(res4)
}
3.闭包
基本介绍:
闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)。
举例:
package main
import "fmt"
// 累加器
func Addupper() func(int) int {
// 匿名函数与外面的n构成一个闭包
var n int = 10
return func(x int) int { // 返回是一个函数 匿名函数引用到外面变量n
n = n + x
return n
}
}
func main() {
//
f := Addupper()
fmt.Println(f(1)) // 11
fmt.Println(f(2)) // 13
}
返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包。
可以理解为:闭包是一个类 ,函数是操作,n是字段。
当我们反复的调用f函数时,因为n只初始化一次
搞清楚闭包的关机,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和引用的变量共同构成闭包。
一个闭包的例子:
package main
import "fmt"
import "strings"
func makeSuffix(suffix string) func(string) string {
// (1)编写一个函数 makeSuffix (suffix string) 可以接收一个文件后缀名比如后缀名(.jpg)
// 并返回一个闭包
// (2)调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀比如后缀名(.jpg),则返回
// 文件名.jpg,如果已经有.jpg后缀,则返回原文件名
// (3) 要求使用闭包的方式完成
// (4)strings.HasSuffix 这个函数可以判断是否存有指定的后缀
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
// .jpg的后缀
f := makeSuffix(".jpg")
// 实例引用
fmt.Println("new_name:", f("today"))
fmt.Println("new_name:", f("today.jpg"))
}
// 以上代码说明:返回的匿名函数和maksSuffix(suffix string)的suffix 变量组成一个闭包
// 返回的函数引用到suffix变量
4.函数中defer
为什么需要defer
在函数中,需要创建资源(比如:数据库连接,文件句柄,锁等),为了在函数执行完毕后,及时的释放资源,GO的设计提供defer(延时机制)。
defer的基本介绍
(1)当go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个defer栈中(一个独立的栈),然后继续执行函数下一个语句。
(2)当函数执行完毕后,在defer栈中,依次从栈取出语句执行(注:遵循栈,先入后出的机制),所以同学们看到前面案例输出的顺序。
(3)在defer将语句放入到栈时,也会将相关的值拷贝同时入栈。
举例:
package main
import "fmt"
func sum(n1 int, n2 int) int {
// defer 的作用会让其本来要执行的语句暂时不执行,将要执行的语句压入到一个defer栈(独立栈)
// 暂时不执行
// 当函数执行完毕后,再执行defer栈中语句:先入后出执行
defer fmt.Println("n1=", n1) // defer
defer fmt.Println("n2=", n2) // defer
res := n1 + n2
fmt.Println("in_res=", res)
return res
}
func main() {
res := sum(5, 7)
fmt.Println("out_res=", res)
// 结果:
// in_res= 12
// n2= 7
// n1= 5
// out_res= 12
}
defer的最佳实践在于,当函数执行完毕后,可以及时的释放创建的资源(比如:打开文件,立即defer就不用考虑什么时候去close,数据库连接也是如此)。在defer以后可以继续使用创建的资源,当函数执行完毕后可以自动从defer中取出并进行关闭资源。
比如:
例子01:
func test() {
// 关闭文件资源
file = openfile(文件名)
def file.close()
// 使用文件句柄相关操作
}
例子02:
func test() {
// 释放数据库资源
connect = openDatabse()
defer connect.close()
// 操作数据库
}
三.函数参数的传递方式
基本介绍:
值类型参数默认就是值传递,而引用类型参数默认就是引用传递。
1.两种传递方式
1)值传递
基本数据类型(int float,bool,string,数组和结构体struct)
2)引用传递(指针传递)
指针,slice切片,map,管道chan,interface等都是引用类型
不管是值传递还是引用传递,传递函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定的数据大小,数据越大效率越低。
2.值传递和引用传递使用特点
1)值类型默认是值传递:变量直接存储值,内存通常在栈中分配。
2)引用类型默认是引用传递:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何引用这个地址时,该地址对应的数据空间就成为一个垃圾,有GC来回收。
3)如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用。
四.变量的作用域
说明:
1) 函数内声明/定义的变量叫局部变量,作用域仅限于函数内部。
2)函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效。
3)如果变量是在一个代码块,比如for /if 中,那么这个变量的作用域就在该代码块中。
例子:局部变量与全局变量
package main
import "fmt"
var age int = 50
var Name string = "zero"
// 函数
func test() {
// age 和 Name 的作用域只在test函数内
age := 10
Name := "hsz"
fmt.Println("age=", age)
fmt.Println("Name=", Name)
}
func main() {
test()
fmt.Println("age=", age) // 打印是test() 函数外的age变量
fmt.Println("Name=", Name) // 打印是test()函数外的Name变量
// 最后输出结果为:
// age= 10
// Name= hsz
// age= 50
// Name= zero
}
在GO语言中:赋值语句不能在函数体外,如:Name := “hsz” 是错误的。
四.函数的练习
1.金字塔
// 打印金字塔案例 使用函数的方式
// 1 2 3 4 5
package main
import "fmt"
func Jinzita(totalLevel int){
// 表示层数
for i :=1;i <= totalLevel; i++ {
// 在打印 *前先打印空格
for k:=1; k <= totalLevel - i; k++ {
fmt.Print(" ")
}
// j 表示每层打印多少
for j :=1; j <= 2 * i - 1; j++ {
fmt.Print("*")
}
fmt.Println()
}
}
func main() {
var n int
fmt.Println("请输入金字塔层数:")
fmt.Scanln(&n)
Jinzita(n)
}
2.九九乘法表
用函数的方式打印九九乘法表
package main
import "fmt"
func NightNight(){
// 打印九九乘法表
for i:=1;i<=9;i++{
for j := 1; j <= i; j++{
fmt.Printf("%v*%v=%v",i,j,i*j)
}
fmt.Println()
}
}
func main(){
NightNight()
}

浙公网安备 33010602011771号