08-Go语言中函数和指针

函数

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{}
	}
	...
}
函数进阶
变量的作用域
  1. 全局变量

    全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。在函数中可以访问到全局变量。

    package main
    
    import "fmt"
    
    //定义全局变量num
    var num int64 = 10
    
    func testGlobalVar() {
    	fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
    }
    func main() {
    	testGlobalVar() //num=10
    }
    
  2. 局部变量

    局部变量又分为两种:函数内定义的变量和语句快定义的变量。

    函数内定义的变量:

    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
  • 什么场景会用到defer
    • 释放资源
    • 关闭文件
    • 释放连接
  • defer的执行顺序
    • 先注册的后执行
  • defer的执行时机
    • 返回值赋值之后,底层RET指令之前
  • defer语句不能接收返回值 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 用来做错误处理
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

注意:

  1. recover()必须搭配defer使用。
  2. defer一定要在可能引发panic的语句之前定义。

07-指针

任何程序数据载入内存中,在内存中都有他们的地址,这就是指针。而为了保存一个数据在内存的地址,我们就需要指针变量。

比如,“永远不要高估自己”这句话是我的座右铭,我想把它写入程序中,程序一启动这句话要加载到内存(假设内存地址为0x123456),我在程序中把这段话赋值给变量A,把内存地址赋值给变量B。这时候变量B就是一个指针变量。通过变量A和变量B都能找到我的座右铭。

GO语言中的指针不能进行偏移和运算。因此go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行取地址操作。GO语言中的值类型(int,float,bool,string,array,struct)都有对应的指针类型,如:*int、*string等。

去变量指针的语法如下:

ptr := &v    // v的类型为T

其中:

  • v:代表被取地址的变量,类型为T
  • ptr:用于接收地址的变量,ptr的类型就为*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)]

就两个操作:

  1. 取变量x的内存地址: &x 得到的是指针
  2. 有了指针变量p,*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和make都是用来申请内存,new用的较少

区别:

  1. new返回指针类型
  2. make只能用于slice、map、channel的初始化,返回的还是这三个引用类型本身;
  3. 而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)
}

你可能感兴趣的:(Golang,golang,运维开发,后端)