Go语言中支持函数、匿名函数和闭包,并且函数在G语言中属于“一等公民”
。
Go语言中定义函数使用func
关键字,具体格式如下:
func 函数名(参数)(返回值){
函数体
}
其中:
函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,
分隔。
返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()
包裹,并用,
分隔。
函数体:实现指定功能的代码块。
我们先来定义一个求两个数之和的函数:
func intSum(x int,y int)int{
return x+y
}
函数的参数和返回值都是可选的,例如我们可以实现一个既不需要参数也没有返回值的函数:
func sayHello() {
fmt.Println("Hello 沙河")
}
定义了函数之后,我们可以通过函数名()
的方式调用函数。 例如我们调用上面定义的两个函数,代码如下:
func main() {
sayHello() //调用函数
res := initSum(4, 5)
fmt.Println(res)
}
注意,调用有返回值的函数时,可以不接受其返回值。
函数参数中如果相邻变量类型相同,则可以省略类型,例如:
func initSum(x,y int)int{
return x+y
}
上面的代码中,intSum
函数有两个参数,这两个参数的类型均为int
,因此可以省略x
的类型,因为y
后面有类型说明,x
参数也是该类型。
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后...
来标识。
注意:可变参数通常要作为函数最后的一个参数。
//定义可变参数函数
func initSum2(x ...int) int {
fmt.Println(x) //x是一个切片
sum := 0
for _, v := range x {
sum += v
}
return sum
}
-----------------------------------
//调用上面的函数:
res1 := initSum2()
res2 := initSum2(10)
res3 := initSum2(10, 20)
res4 := initSum2(10, 20, 30)
fmt.Println(res1, res2, res3, res4)
//运行结果:
[]
[10]
[10 20]
[10 20 30]
0 10 30 60
可变参数搭配固定函数使用时,可变函数在固定函数的后面,示例代码如下:
//固定参数+可变参数
func initSum3(x int, y ...int) int {
fmt.Println(x, y)
sum := x
for _, v := range y {
sum += v
}
return sum
}
//调用上面函数
res5 := initSum3(100)
res6 := initSum3(100, 10)
res7 := initSum3(100, 10, 20)
fmt.Println(res5, res6, res7)
//运行结果:
100 []
100 [10]
100 [10 20]
100 110 130
本质上,函数的可变参数是通过切片实现的。
注意:没有默认参数!!!
Go语言中通过return
关键字向外输出返回值。
GO语言中函数支持多返回值,函数如果有多个返回值时,必须用()
将所有返回值包裹起来。
//多返回值
func calc(x, y int) (int, int) { //定义多返回值
sum := x + y
sub := x - y
return sum, sub
}
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。
//返回值命名
func calc2(x, y int) (sum, sub int) {
sum = x + y //因为在返回值已经声明定义参数,函数体直接使用就行
sub = x - y
return sum, sub
}
当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}
全局变量
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。在函数中可以访问到全局变量。
package main
import "fmt"
//定义全局变量num
var num int64 = 10
func testGlobalVar() {
fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
}
func main() {
testGlobalVar() //num=10
}
局部变量
局部变量又分为两种:函数内定义的变量和语句快定义的变量。
函数内定义的变量:
func testGlobalVar() {
//定义一个函数局部变量num,仅在该函数内生效
num := 10
fmt.Printf("num=%d\n", num)
}
func main() {
testGlobalVar()
fmt.Println(num) //此时无法使用变量num
}
如果局部变量和全局变量重名,优先访问局部变量。
package main
import "fmt"
//定义全局变量num
var num int64 = 10
func testNum() {
num := 100
fmt.Printf("num=%d\n", num) // 函数中优先使用局部变量
}
func main() {
testNum() // num=100
}
语句快定义的变量:
通常我们会在if条件判断,for循环,switch语句上使用这种定义变量的方式.
//语句块定义的变量(if语句块)
func testlocalVar2(x, y int) {
fmt.Println(x, y) //函数的参数也只能在本函数内生效
if x > 0 {
z := 10 //变量z只能在if语句块生效
fmt.Println(z)
}
// fmt.Println(z) //此处无法使用变量z
}
//调用
testlocalVar2(10,20)
//运行结果:
10 20
10
//语句块定义的变量(for语句块)
func testlocalVar3() {
for i := 0; i < 10; i++ {
fmt.Println(i) //变量i只在当前for循环语句块中生效
}
//fmt.Println(i) 此处无法使用变量i
}
//调用函数
testlocalVar3()
//运行结果:
0
1
2
3
4
5
6
7
8
9
1、定义函数类型
我们可以使用type
关键字来定义一个函数类型,具体格式如下:
type calculation func(int, int) int
上面语句定义了一个名为calculation的函数类型。这种函数类型里面的函数接收两个int类型的参数并且有一个int类型的返回值.
简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。
func add(x,y int)int{
return x+y
}
func sub(x,y int)int{
return x+y
}
add和sub都能赋值给calculation类型的变量
var c calculation
c=add
2、函数类型变量
我们可以声明函数类型的变量并且为该变量赋值
//type函数类型
type calculation func(int, int) int //声明函数类型
func testType() {
var c calculation //声明一个calculation类型的变量c
c = initSum //把initSum赋值给c
fmt.Printf("type of %T\n", c) // type of c:main.calculation
fmt.Println(c(2, 4)) //调用initsum函数
f := initSum // 将函数add赋值给变量f
fmt.Printf("type of %T\n", f) //type of f: func(int, int) int
fmt.Println(c(6, 10)) // 调用initsum函数
}
//运行结果:
type of main.calculation
6
type of func(int, int) int
16
只要以type
关键字开头的都是定义类型。
3、函数签名
函数签名 --> 函数定义(声明)的格式,与参数名称和返回值名称无关
函数签名一样 --> 函数的参数、返回值的类型和个数、顺序都要一样
func fi(name string, age int) {}
func fj(age int, name string) {}
type MyFF func(string, int)
func f16() {
var mf MyFF
mf = fi
// mf = fj // 函数签名不一致
mf("ddd", 1)
}
高阶函数分为函数作为参数和函数作为返回值两部分。
1、函数作为参数
//高阶函数-函数作为参数
func add(x, y int) int {
return x + y
}
func calc3(x, y int, op func(int, int) int) int { //传入一个名字为op的参数为2个int,返回值int的函数
return op(x, y)
}
//调用
func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}
//运行结果:
30
2、函数作为返回值
//高阶函数-函数作为返回值
/*
1、函数内部调用了一个变量res
2、返回值是res
*/
func do(x, y int, s string) (res func(int, int) int) {
switch s {
case "+":
return add
case "-":
return sub
}
return res
}
func do1(x, y int, s string) func(int, int) int {
var res func(int, int) int
switch s {
case "+":
return add
case "-":
return sub
}
return res
}
函数当然还可以作为返回值,但是在GO语言中函数内部不能在像之前定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式为:
func(参数)(返回值){
函数体
}
1、把匿名函数赋值给变量
//匿名函数
//1、把匿名函数赋值给变量
func f33() func() {
f1 := func() {
fmt.Println("美好的周末就要结束啦")
}
return f1
}
//调用
f33()()
//运行结果:
美好的周末就要结束啦
2、自执行函数
//2、自执行函数:匿名函数定义完加()直接执行
func f34() {
func() {
fmt.Println("哈哈哈哈")
}() //立即执行
}
//调用函数
f34()
//运行结果:
哈哈哈哈
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单说,闭包=函数+引用环境
。 首先我们来看一个例子:
//闭包
func addr() func(int) int {
var x int
//函数内部使用了它外部的函数变量x
f := func(y int) int {
x += y
return x
}
//把匿名函数当成返回值返回
return f
}
//调用
func main(){
f1 := addr() //调用addr函数
fmt.Println(f1(10)) //10
fmt.Println(f1(20)) //10+20=30
}
//运行结果:
10
30
闭包进阶示例1:
//定义一个累加器
//新的值被返回的函数一直用
func addr2(x int) func(int) int {
f := func(y int) int {
x += y
return x
}
return f
// return func (y int) int { //另外一种写的形式
// x +=y
// return x
// }
}
//调用函数
func main(){
f1 := addr2(10)
fmt.Println(f1(20)) // 30
fmt.Println(f1(30)) //60
}
//运行结果:
30
60
闭包进阶示例2:
/*
判断某个名字是否以指定的后缀结尾。
*/
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
//调用函数
func main(){
jpg := makeSuffixFunc(".jpg")
txt := makeSuffixFunc(".txt")
fmt.Println(jpg("test")) // test.jpg
fmt.Println(txt("test")) //test.txt
}
//运行结果:
test.jpg
test.txt
闭包进阶示例3:
/*
求两数的sum和sub
*/
func calcs(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
//调用
func main(){
f1, f2 := calcs(10)
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) // 12 8
}
//运行结果:
11 9
12 8
defer x := sub(10, 2)
//defer语句
func deFer() {
fmt.Println("start")
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("end")
}
//调用
deFer()
//运行结果:
start
end
3
2
1
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如channel、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
Go语言中目前没有异常机制,但是使用panic/recover模式处理错误。panic可以在任何地方引发,但recover只有在defer调用的函数中有效。
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
//运行结果:
func A
panic: panic in B
goroutine 1 [running]:
main.funB(...)
/Users/alblue/Documents/路飞go/day03/作业/fun.go:212
main.main()
/Users/alblue/Documents/路飞go/day03/作业/mian.go:52 +0x66
程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。这个时候我们就可以通过recover将程序回复回来,继续往后执行。
func funA() {
fmt.Println("func A")
}
func funB() {
defer func() {
err := recover()
//如果程序出现panic,通过recover恢复回来
if err != nil {
fmt.Println("recocer ....")
}
}()
panic("panic in B")
}
func funC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
//运行结果:
func A
recocer ....
func C
注意:
recover()
必须搭配defer
使用。defer
一定要在可能引发panic
的语句之前定义。任何程序数据载入内存中,在内存中都有他们的地址,这就是指针。而为了保存一个数据在内存的地址,我们就需要指针变量。
比如,“永远不要高估自己”这句话是我的座右铭,我想把它写入程序中,程序一启动这句话要加载到内存(假设内存地址为0x123456),我在程序中把这段话赋值给变量A,把内存地址赋值给变量B。这时候变量B就是一个指针变量。通过变量A和变量B都能找到我的座右铭。
GO语言中的指针不能进行偏移和运算。因此go语言中的指针操作非常简单,我们只需要记住两个符号:&
(取地址)和*
(根据地址取值)。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&
字符放在变量前面对变量进行取地址操作。GO语言中的值类型(int,float,bool,string,array,struct)都有对应的指针类型,如:*int、*string等。
去变量指针的语法如下:
ptr := &v // v的类型为T
其中:
T
*T
,称做T的指针类型。*代表指针。func poInt(){
a:=10
b:=&a
fmt.Printf("a:%d ptr:%p\n",a,&a) //a:10 ptr:0xc000014080
fmt.Printf("b:%p ptr:%T\n",b,b) //b:0xc000014080 ptr:*int
}
//运行结果:
a:10 ptr:0xc000014080
b:0xc000014080 ptr:*int
我们看一下b := &a
的图示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U80ibcgw-1656580370457)(day03.assets/image-20220116184209206.png)]
就两个操作:
&x
得到的是指针*p
根据内存地址去找值在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
//指针取值
func p1() {
//指针取值
a := 10
b := &a //取变量a的地址,将指针保存在b中
fmt.Printf("type of b:%T\n", b)
c := *b //指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
//运行结果:
type of b:*int
type of c:int
value of c:10
总结: 取地址操作符&
和取值操作符*
是一对互补操作符,&
取出地址,*
根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
指针传值示例:
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
//运行结果:
10
100
new和make都是用来申请内存,new用的较少
区别:
我们先来看一个例子:
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["沙河娜扎"] = 100
fmt.Println(b)
}
执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存。
1、new
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
//new
func newDemo() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) //*int
fmt.Printf("%T\n", b) //*bool
fmt.Println(*a) //0
fmt.Println(*b) //false
}
//运行结果:
*int
*bool
0
false
本节开始的示例代码中var a *int
只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:
func newDemo2() {
var a *int
a = new(int)
*a = 10
fmt.Println(*a)
}
//结果:
10
2、make
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。
本节开始的示例中var b map[string]int
只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["沙河娜扎"] = 100
fmt.Println(b)
}
/*
你有50枚金币,需要分配给以下几个人:Matthew,Sarah,Augustus,Heidi,Emilie,Peter,Giana,Adriano,Aaron,Elizabeth。
分配规则如下:
a. 名字中每包含1个'e'或'E'分1枚金币
b. 名字中每包含1个'i'或'I'分2枚金币
c. 名字中每包含1个'o'或'O'分3枚金币
d: 名字中每包含1个'u'或'U'分4枚金币
写一个程序,计算每个用户分到多少金币,以及最后剩余多少金币?
程序结构如下,请实现 ‘dispatchCoin’ 函数
*/
var (
coins = 50
users = []string{
"Matthew", "Sarah", "Augustus", "Heidi", "Emilie", "Peter", "Giana", "Adriano", "Aaron", "Elizabeth",
}
distribution = make(map[string]int, len(users))
)
func dispatchCoin() ( int) {
//统计每个user有多少个金币
for _, v := range users {
for _, s := range v {
switch s {
case 'e', 'E':
distribution[v] += 1
case 'i', 'I':
distribution[v] += 2
case 'o', 'O':
distribution[v] += 3
case 'u', 'U':
distribution[v] += 4
}
}
}
//计算剩下的金币
var sum int
for _, v := range distribution {
sum += v
}
return 50 - sum
}
func main() {
left := dispatchCoin()
fmt.Println("剩下:", left)
}