Jochen的golang小抄-基础篇-章二

本系列写的是学习过程中的纯代码笔记记录,该系列为代码流,基本只写代码,代码开始前会有一段导读注释,建议先看注释在学习和练习代码

小抄系列主要分为皮毛、基础和进阶篇,本篇为基础,篇幅较长,故分为多个章节,本章主要讲golang中的函数、指针的知识

函数


函数就是一块可重用的通用代码块

牛刀小试

package main

import (
	"fmt"
	"strconv"
	"strings"
)

//主函数,程序运行时,系统自动调用
func main() {
	/*
		function(函数)
			一、概念:具有特定功能一段代码块,可以被多次复用

			二、作用:复用代码,不用重复写功能一样的代码,提高程序的拓展性

			三、使用:
				step1:函数的声明
					func funcName(paramName1 type1,paramName type2) (resType1, resType2) {
					}
					解析:
						func->函数定义的关键字
						funcName->函数的名组
						paramName->参数名
						paramName1->类型
						resType->返回值类型,go支持多返回值,还是挺美滋滋的
					参数列表和返回值列表:
						上述funcName()中为参数列表:用以接收外部传入的数据,其中的参数叫做形参,外部传入的参数叫做实参
						形参列表跟着的()中为返回值列表,是函数执行后返回给调用处(调用函数的地方)的结果
							返回值列表可以为空(即别的语言void的意思),当为空或单个返回值时,可省略()

				step2:函数的调用,执行函数代码块的过程
					调用方法:函数名(实参)
					有个调用时候带(),很标志

		注意:
			1)go语言至少需要有一个主函数main,用以作为程序的入口
			2)函数必须先定义再调用(定义的位置可以在调用之前也可以在后边)
			3)main函数是一个特殊的函数,是程序的入口,由系统自动调用,其他函数在程序中通过函数名调用

	*/
	//通过小示例回顾之前的一点内容和初识函数
	var strList string
	fmt.Println(`拿起键盘猛锤一组数据,以","分割,你将得到他们的和:`)
	fmt.Scanf("%s", &strList)
	var list = strings.Split(strList, ",")
	//2.函数的调用
	var sum = getSum(convertStrList(list)) //嵌套调用
	fmt.Println("结果为:", sum)
}

//1.定义函数,求和
func getSum(list []int) int {
	sum := 0
	for _, v := range list {
		sum += v
	}
	return sum

}

//类型转换
func convertStrList(slist []string) []int {
	iList := make([]int, 8)
	for _, v := range slist {
		i, _ := strconv.Atoi(v) // 转换失败为int零值0,不会抛异常
		iList = append(iList, i)
	}
	return iList
}

函数的参数

参数的使用

package main

import "fmt"

func main() {
	/*
		1. 函数的参数
			形式参数:声明函数时,用于接收外部传入的数据叫做形参
			实际参数:函数调用时,传给形参的实际的数据叫做实参
			注意点:实参与形参顺序与个数以及类型必须一一对应

	*/
	//1.函数的使用
	//1.1函数调用,传入实参
	//getSum() //not enough arguments in call to getSum 参数不足,调用失败
	s := []int{1, 2, 3}
	sum := getSum(s)
	fmt.Println(sum) //6

	//1.4多个参数的函数调用要注意实参的的类型和个数要和形参的一一对应
	fmt.Println(getSum2(1, 2)) //3
	showParams(2, 6, "Jochen") ////go不存在函数的重载,即不可定义同名但参数列表不同的多个函数
}

//1.1函数的声明,定义形参
func getSum(list []int) int { //小括号里面的就是形参,定义的时候它是不确定的,只有在调用处传入实参才能确定
	sum := 0
	for _, v := range list {
		sum += v
	}
	return sum
}

//1.3定义多个形参
//go不存在函数的重载,即不可定义同名但参数列表不同的多个函数
func getSum2(a int, b int) int {
	return a + b
}

//类型一致的多个参数还能换种写法
func showParams(a, b float64, c string) {
	fmt.Printf("a:%.2f,b:%.2f,c:%s", a, b, c)
}

可变参

package main

import "fmt"

func main() {
	/*
		2.可变形参
			1)概念:即可以用特定的语法让函数可以接收不确定数量的参数
			2)语法:
				func f(paramName ...int){
				// ...Type就是可变参的特定语法
				}
			3)本质:可变参数在还函数中就是一个切片(对于外部不是),调用函数时,可以传入0或多个参数

		注意:
			如果一个多参数函数,其中包含可变参,则可变参要放在参数列表最后
			一个函数的参数列表中,最多只能有一个可变参数
	*/
	//2.可变参数
	//2,2可变参数的调用
	fmt.Println(Sum(1, 2, 3, 4, 5, 6)) //21
	fmt.Println(Sum())                 //0

	//2.3可变参数可传入切片,但需要在末尾使用...
	fmt.Println(Sum([]int{1, 2, 3, 4, 5, 6}...)) //21

}

//2.1可变参数的声明
func Sum(nums ...int) int {
	fmt.Printf("nums type:%T\n", nums) //nums type:[]int  可以看到可变参数是本质为切片类型
	sum := 0
	if len(nums) > 0 {
		for _, v := range nums {
			sum += v
		}
	}
	return sum
}

//func text1(arg ...string, a, b int) {
//	//syntax error: cannot use ... with non-final parameter arg
//	//如果一个多参数函数,其中包含可变参,则可变参要放在参数列表最后
//}

//func test2(arg ...int, arg2 ...string){
//	//syntax error: cannot use ... with non-final parameter arg
//	//一个函数的参数列表中,最多只能有一个可变参数
//}

参数的传递

package main

import "fmt"

func main() {
	/*
		3.参数的传递:实参传递给形参的过程
			实参传递给形参的过程和变量传递其实是一样的,也是分为值传递和引用传递

			1)值传递:传递的是数据的副本,修改数据,对原始的数据没有影响
				值类型的数据都是值传递:基本数据类型 array struct

			2)引用传递:传递的是数据的地址,修改数据,对原始的数据有影响
				引用类型数据都是引用传递:slice, map, chan
	*/

	//3.1值传递(数组是值类型)
	a1 := [3]int {1,2,3}
	fmt.Println("函数调用前:",a1) //函数调用前: [1 2 3]
	changeArray(a1)
	fmt.Println("函数调用后:",a1) //函数调用后: [1 2 3]
	/*
		可以看到,值类型参数传递的是值的副本,所以函数内部对形参进行值修改,并不会影响调用处传入的实参变量
	*/


	//3.2引用类型传递(切片是引用类型)
	s1:=[]int{1,2,3}
	fmt.Println("函数调用前:",s1) //函数调用前: [1 2 3]
	changeSlice(s1)
	fmt.Println("函数调用后:",s1) //函数调用后: [100 200 3]
	/*
		可以看到,引用类型传递的是值地址,所以函数内部对形参进行值修改,实际是对同一块内存地址指向的对象进行修改
	所以会一同影响调用处传入的实参变量
	*/



}

func changeArray(a [3]int)  { // a = a1 就和变量的赋值是一样的
	a[0] = 100
	a[1] = 200
	fmt.Println("函数中,数组的数据修改后",a) //函数中,数组的数据修改后: [100 200 3]


}

func changeSlice(s []int)  { //s = s1 就和变量的赋值是一样的
	s[0] = 100
	s[1] = 200
	fmt.Println("函数中,切片的数据修改后",s) //函数中,切片的数据修改后: [100 200 3]

}

函数的返回值

package main

import "fmt"

func main() {
	/*
		4.函数的返回值:函数被调用后,返回给调用处的执行结果
			如果一个函数的定义上有返回值,那么函数中必须使用return语句,将结果返回给调用处
			函数的返回结果,必须和函数定义的类型、个数、顺序一致

		1)函数可以没有返回值
			step1:func funName(params type)
			step2: 函数内部不需要写return返回值,如果使用return则表示结束函数调用

		2)函数返回一个值 []表示可选项
			step1:func funName([params type]) type 或 func funName([params type]) (retName type)
			step2:函数中通过return语句:return res 或 return(相当于return了retName)
				当使用后面一种方式的时候,相当于会在函数内部初始化retName变量,并返回它,所以返回的时候可以省略它直接return


		3)函数可以返回多个值:
			step1:函数声明时在返回值列表使用括号包裹,多个返回类型使用逗号分割:
				func funName(params type) (type1,type2,type3)
			step2:将结果返回给调用处,如果有多个返回值,则使用逗号分割返回:return res1,res2

		注意点:
			1)一个函数定义了返回值,必须使用return语句将结果返回给调用处,return后的数据必须和函数定义的个数、类型顺序一致
			2)可以使用空白标识符:_ 来舍弃多余的返回值
			3)如果一个函数定义了有返回值,那么函数中分支,循环要保证每个分支最终都有return语句可执行

	*/

	//4.1没有返回值的函数
	printString("hello golang") //hello golang

	//4.2函数可以有多个返回值
	var res1 = sum(1,2) //相当于res1 = 函数中return的数值
	println(res1) //3
	var res2 = sum2()
	println(res2) //45

	var s = []int{-1,2,-3}
	var res3,isPositive =  getSum(s) // 接收多个返回值的写法,前面应该也接触了不少了
	if(isPositive){
		println("结果为正数:",res3)
	} else {
		println("结果为负数",res3) //结果为负数 -2
	}

	//空白标识符_用来舍弃数据
	var s2 = []int{1,2,-3}
	var _,isPositive2 = getSum2(s2)
	if(isPositive2){
		println("结果为正数:") //结果为正数
	} else {
		println("结果为负数")
	}
}

//4.1没有返回值的函数
func printString(str string) {
	//return //提前中止函数
	fmt.Println(str)
}

//4.2函数可以有多个返回值
//1个返回值
func sum(a int, b int) int {
	return a + b //通过return语句返回函数里面的值
}

//也可以给返回值命名,此时相当于在函数内部初始化了该值,可以在函数内部操作sum
func sum2() (sum int) {
	for i := 0; i < 10; i++ {
		sum +=i
	}
	return //这里肯定是返回的sum,可以省略,如显示返回别的值,这里相当于会把值赋值给sum返回,即sum = value
}

//多个返回值:对切片内的各个元素求和,返回求和结果与是否为负数的标识,如果结果为负数则返回fasle
func getSum(s []int) (int, bool) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	isPositive := true
	if sum < 0 {
		isPositive = false
	}
	return sum, isPositive
}

//也可以换种写法
func getSum2(s []int) (sum int, isPositive bool) {
	for _, v := range s {
		sum += v
	}
	if sum >= 0 {
		isPositive = true //布尔值类型的零值为false
	}
	return
}

函数中变量的作用域

package main

import "fmt"
//全局变量:必须定义在函数外面
//NUM := 100 //syntax error: non-declaration statement outside function body 全局变量不支持简短定义的写法
var NUM int = 100
func main() {
	/*
		函数中变量的作用域:
			1)变量作用域的概念:变量可以被使用的范围(所以全局能被使用的就叫全局变量,还有常说的局部变量也是作用域的表述)

			2)局部变量:定义在函数内部的变量(我们的main函数也是个函数,其作用域范围只在函数的大括号{}代码块中)
						变量在那里定义,就只能在哪个范围内使用,超出这个范围变量就会被回收销毁

			3)全局变量:定义在函数外面的变量
	*/

	//定义在main函数中,所以n的作用域作用域就是main的函数代码块范围内
	n := 1
	fmt.Println("访问main范围内的变量n:", n) //访问main范围内的变量n:1   n在main作用域中,可以访问

	if x:= 8; x<=8{
		fmt.Println(x) //8
		fmt.Println(n) //8
	}
	//fmt.Println(x) //undefined: x 因为x定义在if语句范围内,所以出了if语句就不能用了

	if y:=1;y <=8{
		n:= 100//不同的作用域内,是可以定义相同的变量名的
		fmt.Println(y)
		fmt.Println(n) //100  这里会访问定义在if内部的n,因为首先会找自己所在作用域范围内的变量,找不到再往父级作用域
	}

	//访问全局变量:
	println(NUM) //100
	fun1()
	//改变全局变量,全部访问的地方也会跟着改变:
	println(NUM) //1000
}

func fun1() {
	//fmt.Println("访问main范围内的变量n", n) //undefined:
	//因为n是在main函数范围内定义的,所以其访问范围即作用域仅在main函数的代码块{}中

	//改变全局变量,全部访问的地方也会跟着改变:
	NUM = 1000
	println(NUM) //1000
}

递归函数

package main

func main() {
	/*
		递归函数(recursion):一个函数自己调用自己
			注意:递归函数要必须要有一个出口,并且每次调用都会向出口靠近,否则会进入死循环,直到内存耗尽

		有点蒙不理解没关系,学习完基础后去刷刷算法题多感受一下
	*/
	var res = sum(5)
	println(res)

	println(fibonacci(20)) //6765

}

/*
	求1-5的和用递归的做法:下面拆分每一部sum的调用
	|	sum(5)						 15		一层层向上返回
	|		sum(4) + 5				 10+5	 ^
	|			sum(3) + 4     		 6+4	 |
	|				sum(2) + 3       3+3     |
	v					sum(1) + 2   1+2     |
每个函数层层拆分				得出推导公式:sum(n)  = sum(n-1) + n
*/
func sum(n int) (s int) {
	if n == 1 { //递归的出口
		return n
	}
	return sum(n-1) + n
}

/*
	fibonacci数列:第一项和第二项的数值都是1,从第三项开始是前两项的数值之和
项:	1 2 3 4 5 6 7 8...
值:	1 1 2 3 5 8 13 21...
*/
func fibonacci(n int) int {
	if n == 1 || n == 2 {
		return 1
	}
	return fibonacci(n-1) + fibonacci(n-2)
}

defer关键字

package main

import "fmt"

func main() { //外围函数
	/*
		defer:延迟,推迟的意思
		go语言中用defer关键字来延迟一个函数或方法的执行

			1.defer一个函数或方法就是让这个函数或者方法暂时不执行了

			2.defer的用法:
				1)对象.close(),
					如:文件的操作伴随着文件的打开关闭,时常我们操作完后会忘记关闭文件,此时就可以使用defer
			在操作完成后关闭文件:
						文件.open()
						defer close()
						读或写
					不仅是文件,像是io操作,数据库连接等都适用,好用的一批!
				2)在go语言中对于异常处理,适用panic()和recover()
					panic的词义为恐慌,一旦执行了panic函数程序就会中断执行
					recover的词义为恢复,与panic搭配用于恢复程序的执行。
				这里的recover语法上要求必须在defer函数u上执行

			3.如果多个存在多个defer函数:先延迟的后执行,后延迟的先执行

			4.defer函数发生函数传递的时候:defer函数调用时,参数已经被传递,只是暂不执行函数中的代码

		注意点:
			1)当外围函数中的语句正常执行完毕时,只有其中所有的延迟函数执行完毕,外围函数才算执行结束
			2)若外围函数执行return,只有其中所有的延迟函数执行完毕,外围函数才会return
			3)当外围函数代码引发panic恐慌时,只有其中所有延迟函数执行完毕,该运行时恐慌才会真正被抛出至调用函数
	*/

	fun1("golang")
	fmt.Println("java")
	/*
		插播:
			外围函数的概念:
				在主函数函数中调用了fun1函数,它使用defer被延迟了,那么对于这次延迟的函数fun1来讲这个主函数main就叫做外围函数
	*/
	defer fun1(".net") //原本是第三个执行,因defer所以在外围函数执行之前才执行
	fun1("python")
	/*
		print:
			函数被执行: golang
			java
			函数被执行: python
			函数被执行: .net

	*/

	//假如有多个函数被defer
	defer fun1("Tom")
	fmt.Println("Amy")
	defer fun1("Jochen")
	fun1("TiMo")
	/*
		   print:
		      函数被执行: TiMo
		      函数被执行: Jochen		-》在上面最后被defer
		      函数被执行: Tom		-》在上面第二被defer
		      函数被执行: .net		-》在上面最先被defer

			可以看到:多个函数被defer那个被defer的函数会在外围函数defer后按代码中后调用的顺序先执行
			实际上,当一个函数有多个延迟调用时,它们被添加到一个堆栈中,并按先进后出的顺序执行
			什么是栈?就是一个先进后出的容器而已
	*/

	//思考:defer是让函数的调用延迟了,还是函数的执行延迟了
	//如果是调用延迟了,那么这里的实参应该会捕获的是最终的变量结果
	//如果是执行延迟了,那么这里的实参应该会捕获的当前调用时候变量的结果
	a := 1000
	defer fun2(a)
	a++
	/*
		print:
			函数执行,n的结果为: 1000
		结论:defer是让函数的执行延迟了,而非调用
	*/

	//fun3r return前,其内的defer函数会先执行完毕
	println(fun3())

}

func fun1(s string) {
	println("函数被执行:", s)
}

func fun2(n int) {
	println("函数执行,n的结果为:", n)
}

func fun3() int {
	println("fun3函数执行")
	defer fun1("hello jochen")
	return 100
}

函数的类型

实际上,函数也是一种数据类型,函数与前面学习的array、slice和map一样同属复杂数据类型

package main

import "fmt"

func main() {
	//	函数的类型:
	fmt.Printf("f1函数的数据类型:%T\n", f1) //f1函数的数据类型:func()
	fmt.Printf("f2函数的数据类型:%T\n", f2) //f2函数的数据类型:func(string)
	fmt.Printf("f3函数的数据类型:%T\n", f3) //f3函数的数据类型:func(int, int) int
	fmt.Printf("f4函数的数据类型:%T\n", f4) //f4函数的数据类型:func(string, string) (string, string)

	//可以看到函数的数据类型为:func(参数列表的数据类型)(返回值列表的数据类型)
}

func f1() {

}

func f2(s string) {

}

func f3(a int, b int) (sum int) {
	return a + b
}

func f4(s1 string, s2 string) (a string, b string) {
	return s1, s2
}

函数的本质

package main

import "fmt"

func main() {
	/*
		go语言的各类数据类型的基本操作:
			数据类型:整数,浮点
				进行运算操作,加减乘除,打印输出

			字符串:
				可以获取单个字符,截取子串,遍历,strings包下的函数操作

			数据,切片,map --容器类型
				存储数据,修改数据,获取数据,遍历数据

			函数:通过()进行调用

		注意:
			函数作为一种复合数据类型,可以认为是一种特殊的变量
				函数名():将函数进行调用,函数体的代码会逐一执行,并将return结果返回给调用处
				函数名:指向函数体的内存地址
	*/

	//1.函数做一个变量
	fmt.Println(f) //0x499120
	/*
		实际上,定义一个函数的时候:
			step1:开辟一块内存,存储函数体(函数中的代码)
			step:函数名相当于一个特殊的变量名字,其指向的是函数体的地址
		上面打印函数名,实际上打印函数名字指向的内存地址
		当通过函数名()调用函数时,代表通过函数名来访问函数体所在的内存地址,然后把函数体代码从上到下执行一遍

		结论:函数也是一种特殊类型的变量
	*/

	//2.直接定义一个函数类型的变量
	var f1 func(int, int)
	fmt.Println(f1) //<nil> 定义未赋值则默认为空nil
	//这里如果调用空引用函数,编译是可以通过的,但是运行时会抛出异常
	//f1(100, 200) //panic: runtime error: invalid memory address or nil pointer dereference

	f1 = f //将f的值赋值给f1,注意函数类型需要和变量类型一致
	/*
		看到这里,不少小伙伴应该有点熟悉了,cpp的函数指针,.net的委托...基本上都是一种东西
		这玩意最大的用处,就是可以将函数作为参数去传递,至于妙处,后面开发过程中会更加深有体会
	*/
	fmt.Println(f1, f) //0x4991e0 0x4991e0
	f1(100, 200)       //f1也是函数类型的,加小括号可以被调用

	res1 := f //将f的值(函数的地址)赋值给res1,此时re1和f指向同一个函数体
	res2 := f2(1,2) //将f2函数进行调用,将函数的执行结果赋值给res2
	println(res1) //0x4c4b90
	println(res2) //3
}

func f(a, b int) {
	fmt.Println("a + b =", a+b)
}
func f2(a, b int) int {
	return a + b
}

匿名函数

package main

import "fmt"

func main() {
	/*
	匿名函数:没有名字的函数

	注意:
		1)定义一个匿名函数,直接进行调用,通常匿名函数只能使用一次
		2)也可以将匿名函数赋值给变量,就可以实现多次调用
		3)有经验的小伙伴应该马上能联想到在各个语言中都有的lambada表达式,其也是匿名函数变形 ,
	匿名函数的精髓作用是作为参数进行传递使用

	*/

	//匿名函数定义并使用
	func (){
		fmt.Println("匿名函数")
	}()


	//将匿名函数赋值给变量,就可以在多处使用
	f := func(a,b int) int {
		return a+b
	}
	fmt.Println(f) //0x4991e0
	fmt.Println(f(1,2)) //3

}

go语言支持函数式编程

匿名函数在函数式编程领域有大用:

  1. 作为回调函数使用:将匿名函数作为另一个函数的参数
  2. 形成闭包结构:将匿名函数作为另一个函数的返回值

回调函数

首先我们需要学习一些概念:

高阶函数:

  • 现有函数A与函数B,函数A为函数B的参数,如:func B(A func ()) { }
  • 函数B作为接收参数的函数,此时,函数B就被称作高阶函数
  • 函数A作为另一个函数的参数,此时,函数A就被称为回调函数

上面引出了回调函数,按字面上的理解,回调函数即为一个参数,把一个函数A作为参数传递到另一个函数B中,当函数B执行到“一定阶段"再执行函数A,这个过程即称之为“回调”

不难理解,回调即回头调用的意思,函数B先干活,在某个时机(取决于代码怎么写)回头去执行函数A

package main

import "fmt"

func main() {
	/*
		高阶函数:
			GO语言可以将一个函数作为另一个函数的参数
	*/

	//设计一个函数,可根据情况对两数进行不同的运算
	fmt.Printf("add:%T\n",add) //add:func(int, int) int
	fmt.Printf("oper:%T\n",oper) //oper:func(int, int, func(int, int) int) int

	//调用普通函数
	res1 := add(1,2)
	fmt.Println(res1) //3

	//调用高阶函数
	//进行加法运算
	res2 := oper(1,2,add) //此处将add作为实参传递,add并未立刻执行,而是到了oper函数中最后才被执行
	fmt.Println(res2) //3
	//进行减法运算
	res3 := oper(10,10,mult)
	fmt.Println(res3) //100

	//类似上面每一个操作都需要定义一个方法作为参数传递实在繁琐
	//为此我们可以使用匿名函数,简化我们的代码实现,如下利用匿名函数实现减法操作
	res4 := oper(100,99, func(i int, i2 int) int {
		return i-i2
	})
	fmt.Println(res4) //1

}

//两数做什么操作,取决于函数参数
func oper(a, b int, operFunc func(int, int) int) int { //这玩意就是高阶函数(接收函数类型参数)
	//打印三个参数
	fmt.Printf("a:%v, b:%v, operFunc:%v\n",a,b,operFunc) //a:1, b:2, operFunc:0x49af00
	//operFunc就是一个函数,调用处传入add函数作为实参,所以此时operFunc指向的就是add的函数体地址
	//执行operFunc <=> 执行add
	return operFunc(a,b)  //函数传入时没有立即执行,而是在后续某一刻,如此处到了最后才被调用,这就叫做add函数被回调了
}

//加法运算
func add(a, b int) int { //这玩意是普通函数
	return a + b
}

//乘法运算
func mult(a,b int) int {
	return a * b
}

闭包(closure)

函数的闭包结构:

  • 一个外层函数中,有内层函数
  • 该内层函数中,若操作了外层函数的局部变量(无论是外层函数内部定义,还是来自外部的传参)并且该内层函数被作为外层函数的返回值返回
  • 此时这个内层函数和外层函数的局部变量统称为闭包结构
package main

import "fmt"

func main() {
	/*
		闭包(closure):
			闭包使得局部变量的生命周期发生改变。普通的局部变量随函数的结束而结束,闭包结构却不会
			因为内层函数还要继续使用(它有可能被函数的调用处继续调用,每次调用需要涉及外层函数的
			局部变量访问,所以这些局部变量就会被统一打包到内层函数的环境中保存)
	*/

	//将一个函数作为另一个函数的返回值
	res1 := increment()                    //相当于res1 = f
	fmt.Printf("res1:%T,%v\n", res1, res1) //res1:func() int,0x49aaa0

	//执行一次返回值函数f
	fmt.Println(res1()) //1
	//再执行一次f
	fmt.Println(res1()) //2
	//我再执行f
	fmt.Println(res1()) //3
	/*
			按我们之前的认知:函数每次执行完后,函数中的局部变量就会被回收销毁,每次执行函数都是全新的开始
		但是此处居然保留了函数increment中的局部变量n,即局部变量n的生命周期发生了改变
			所以,当一个函数的作为内层函数又作为函数的返回值,并且该内层函数中操作了外层函数的变量时,此时该
		内层函数就会将外层函数的被操作的变量环境打包到自己内部保存——形成闭包结构
			简单说闭包就是内层函数操作外层函数变量,被操作的变量不会被销毁,而是会被保存到内层函数的环境中保存,
		其中内层函数和外层函数的局部变量就统一称为闭包结构
	*/
}

func increment() func() int { //外层函数
	//1.定义了一个局部变量
	n := 0
	//2.定义匿名函数,让变量自增并返回
	f := func() int { //内层函数
		n++
		return n
	}
	fmt.Printf("f:%T,%v\n", f, f) //f:func() int,0x49aaa0
	//3.返回该匿名函数
	return f
}

指针


前面我们已经接触了不少“地址的概念”,而指针实际上就是一种特殊的变量,该变量存储了另一个变量的内存地址

变量是一种使用方便的占位符,其相当于给计算机内存地址起了个别名

牛刀小试

package main

import "fmt"

func main() {
	/*
		指针:存储另一个变量的内存地址的变量
	*/
	//1.获取变量地址:通过取地址符&
	n1 := 1
	fmt.Println("n1的地址是:", &n1)    //n1的地址是: 0xc0000b6010
	fmt.Printf("n1的地址是:%p\n", &n1) //格式化打印

	//2.声明指针变量 var varName *Type
	var poniter *int                      //声明了一个int类型的指针 即该变量存储的是一个指向整型变量的内存地址
	fmt.Println(poniter)                  //<nil> 空指针,表示里面没有存储任何值
	poniter = &n1                         //意味着poniter指向了n1的内存地址
	fmt.Println("poniter存储的值是:", poniter) //poniter存储的值是: 0xc0000b6010

	//3.获取指针的值 通过解引用符*
	fmt.Println("poniter的值为n1的内存地址,该地址存储的值为:", *poniter) //poniter的值为n1的内存地址,该地址存储的值为: 1

	//4.操作变量,更改数值
	n1 = 888
	//改变数值只是通过变量名字来访问内存空间,把其内存储的数据玩弄一下,并不会改变存储地址
	fmt.Printf("n1值为:%v, 地址是:%p\n", n1, &n1) //n1值为:888, 地址是:0xc0000b6010
	//通过指针改变变量的数值
	*poniter = 666
	fmt.Printf("n1值为:%v, 地址是:%p\n", n1, &n1) //n1值为:666, 地址是:0xc0000b6010


	//5.指针的指针:当变量存储的是一个指针的地址
	var poniter2 **int = &poniter
	fmt.Println(poniter2) //0xc0000b8020  -》p1的内存地址
	fmt.Printf("n1的类型:%T, poniter的类型:%T, poniter2的类型%T\n",n1,poniter,poniter2)
	//n1的类型:int, poniter的类型:*int, poniter2的类型**int

	//无非是俄罗斯套娃,搞懂没那么难,认清地址的含义即可
	fmt.Println("poniter2中存储的地址,对应就是poniter1的值,对应的数据为:", *poniter2)
	//poniter2中存储的地址,对应就是poniter1的值,对应的数据为: 0xc000126010
	
	fmt.Println("获取poniter1指向的值:",**poniter2) //获取poniter1指向的值: 666



}

数组指针和指针数组

package main

import "fmt"

func main() {
	/*
		数组指针:一个指向数组的指针,指针存储的是数组的地址
			*[n]Type

		指针数组,一个元素为指针的数组
			[n]*Type
	*/

	//1.创建一个普通的数组
	arr1 := [3]int{1, 2, 3}
	fmt.Println(arr1) //[123]

	//2.创建一个指针,该指针指向的是上面的数组->数组指针
	var p1 *[3]int
	p1 = &arr1
	fmt.Println(p1)                  //&[1 2 3] 直接输出的不是地址,而表示这是一个数组的指针
	fmt.Printf("指针存储的数组地址%p\n", p1)  //指针存储的数组地址0xc000016440
	fmt.Printf("指针变量自己的地址%p\n", &p1) //指针变量自己的地址0xc00000e030

	//3.使用数组指针,操作数组
	(*p1)[0] = 888    //理论写法 *p1获取到的是指针指向的数组实体
	fmt.Println(arr1) //[888 2 3]

	//上面也可以简写,通过直接操作数组变量的方式去操作数组中的元素,这是go语言提供的语法支持,比c/c++舒服的多
	p1[1] = 666       //简化写法
	fmt.Println(arr1) //[888 666 3]

	//4.指针数组:一个元素为指针的数组 [n]*type
	a, b, c := 1, 2, 3
	arr2 := [3]*int{&a, &b, &c}
	fmt.Printf("指针数组的类型:%T\n", arr2) //指针数组的类型:[3]*int
	fmt.Printf("指针数组的值:%v\n", arr2)  //指针数组的值:[0xc0000b6040 0xc0000b6048 0xc0000b6050]
	//修改指针数组中的元素
	*arr2[0] = 888
	fmt.Printf("指针数组的值:%v\n", arr2) //指针数组的值:[0xc0000b6040 0xc0000b6048 0xc0000b6050]
	fmt.Println(a)                  //888 值被改变
}

函数指针和指针函数

 package main

import "fmt"

func main() {
	/*
		函数指针:指向了一个函数的指针
			与c/c++一样在go中,func名默认就是一个函数指针,所以定义时不要*
			与函数外,slice,map引用类型变量中存储的也是数据的地址

		指针函数:一个返回值为指针的函数
	*/
	//1.函数指针:函数就是一种默认的指针类型
	var f func()
	f = A //A为函数变量存储的是函数体的地址
	f()

	//2.指针函数
	p_f := B()                                  //返回值为指针的函数就是指针函数
	fmt.Printf("指针函数的类型是:%T,值为:%v\n", p_f, p_f) //指针函数的类型是:*int,值为:0xc00001c0e0


}

func A() {
	fmt.Println("函数1")
}

func B() *int {
	i := 888
	fmt.Printf("变量i的地址:%p\n",&i) //变量i的地址:0xc00001c0e0
	return &i //因返回的是地址给调用处,调用处还存有i的地址,即p_f引用了i所以i不会被GC销毁
}

指针作为参数传递

package main

import "fmt"

func main() {
	/*
		指针作为参数:
		指针作为函数传递时,发生的是引用传递(传地址的就是引用传递,前面应该已经根深蒂固了)
			指针作为参数传递,主要作为值类型的指针进行传递,这是因为引用类型的在参数传递中传的就是地址,不必多此一举

		应用场景:
			1.当想将值类型参数同步函数操作(受函数内对值操作的影响)时,可以选择传递参数变量的地址(指针)
			2.指针作为参数传递可以节省内存,这是因为值传递会将数据拷贝一份,消耗的内存会比较大(如果是结构体可能会更大)
	*/
	//指针传递属于引用传递
	n := 1
	fmt.Println("n调用前:", n) //n调用前: 1
	A(&n)
	fmt.Println("n调用后:", n) //n调用后: 888
}

func A(p *int) { //传递的是n的地址,是引用传递
	fmt.Println("A函数中,p: ", *p) //A函数中,p:  1
	*p = 888
	fmt.Println("A中,修改p", *p) //A中,修改p 888

}

结构体


数组可以存储相同类型的一组数据,如果我们想存储一组数据类型不相同的数据呢?此时就可以使用结构体

结构体:是由一系列具有同类或不同类型的数据组成的数据集合

牛刀小试

package main

import "fmt"

func main() {
	/*
			结构体:
				一组具有相同或不同类型的数组组成的集合
				结构体的成员由一系列的成员变量组成,这些成员变量被称之为“字段”

			理解起来不困难,go中的结构体和其他语言的class地位差不多(class的本质也是由结构体演变而来),因go并非面向对象,所以没有class,
		go放弃了包括继承等面向对象的大部分特性,之保留了结构体组合这个基础特性
	*/

	//2.初始化结构体(用结构体存储数据)
	//2.1方法一:
	var p1 Person   //创建
	fmt.Println(p1) //{ 0  }   未设置初始值时候,会赋予类型默认初始值,里面每一个成员变量存储的是其数据类型的零值
	//给成员变量赋值,结构体的访问通过 . 的方式
	p1.name = "Jochen"
	p1.age = 23
	p1.sex = "boy"
	p1.address = "ShenZhen"
	fmt.Println(p1) //{Jochen 23 boy ShenZhen}
	//姓名:Jochen,性别:boy,年龄:23,地址:ShenZhen
	fmt.Printf("姓名:%s,性别:%s,年龄:%d,地址:%s\n", p1.name, p1.sex, p1.age, p1.address)

	//2.2方法二:
	p2 := Person{name: "Tom", age: 18, address: "HuiZhou", sex: "boy"}
	p3 := Person{"Amy", 20, "gril", "DongBei"} //按顺序赋值,golandIDE有提示,很方便
	fmt.Println(p2)                            //{Tom 18 boy HuiZhou}
	fmt.Println(p3)                            //{Amy 20 gril DongBei}

	//2.3方法三:new方式,struct是值类型,但是通过new创建的结构体为指针类型
	p4 := new(Person)
	fmt.Println(p4) //&{ 0  }
	fmt.Printf("%T\n",p4) //*main.Person  表示main包下的Person结构体指针类型
	p4.sex = "boy"
	p4.name = "daHuang"
	//未设置初始值的,会赋予类型默认初始值
	fmt.Println(*p4) //{daHuang 0 boy }

}

//1.定义结构体
type Person struct {
	name    string //go通过命名首字母大小写来限制成员的可见性大小,当大写时候表示可跨包访问该成员变量
	age     int
	sex     string
	address string
}

结构体指针

首先说下go语言的中的make和new

make:用于内置类型(map,slice和channel)的内存分配
make只能创建slice、map和channel,并且返回一个有着初始值的T类型,而非*T。这三中类型之所以专门使用make创建是因为他们的数据结构引用使用前必须被初始化,例如:slice中包含指向底层数组的指针、长度以及容量的三种东西的描述符,这些内容被初始化之前slice为nil
所以make是初始化了内部的数据结构,给其中填充相应的值

new:用于各种类型的堆内存分配,与其他语言中的new功能差不多,new(T)本质上是在堆空间中分配了一个零值填充的T类型的内存空间,并返回了该空间的地址,即返回一个*T类型的值或者返回了一个T类型的指针

使用new只需要记住,其返回的是指针

package main

import "fmt"

func main() {
	/*
		结构体可以是值类型,也可以是引用类型
			当使用new方式初始化结构体的时候,结构体就为引用类型
				new()是go语言创建某种类型的指针的函数

			通过new(),创建不是空指针nil,它开辟了指针某类型的内存空间,并用零值填充
	*/

	//1.结构体是值类型
	p1 := Person{name: "Tom", age: 18, address: "HuiZhou", sex: "boy"}
	fmt.Println(p1)                //{Tom 18 boy HuiZhou}
	fmt.Printf("%p,%T\n", &p1, p1) //0xc000076040,main.Person
	//改变p2的值,不影响p1,因为常规方式创建的结构体对象为值类型,值类型赋值操作传递的是数据的副本,而非地址
	p2 := p1
	//可以看到地址不一样
	fmt.Printf("%p,%T\n", &p2, p2)           //0xc000076100,main.Person
	fmt.Printf("%p,%T\n", &p2.name, p2.name) //0xc000076100,string 结构体地址为首个成员变量的地址
	//更改值不影响源
	fmt.Printf("p1:%v, p2:%v\n", p1, p2) //p1:{Tom 18 boy HuiZhou}, p2:{Tom 18 boy HuiZhou}
	p2.name = "Jochen"
	fmt.Printf("p1:%v, p2:%v\n", p1, p2) //p1:{Tom 18 boy HuiZhou}, p2:{Jochen 18 boy HuiZhou}

	//2.定义结构体指针
	var pp1 *Person //传统的方式创建指针类型
	pp1 = &p1
	fmt.Println(pp1)                //&{Tom 18 boy HuiZhou} 结构体指针的打印形式
	fmt.Printf("%p,%T\n", pp1, pp1) ////0xc000076040,*main.Person
	//访问结构体指针的成员可以省略解引用符 * 直接操作
	pp1.name = "王大锤"
	fmt.Println(*pp1) //{王大锤 18 boy HuiZhou}
	fmt.Println(p1)   //{王大锤 18 boy HuiZhou}  因为赋值的为p1的地址,pp1操作的对象为该地址的实体

	//内置函数new()创建结构体指针
	pp2 := new(Person)
	fmt.Println(pp2)        //&{ 0  }
	fmt.Printf("%T\n", pp2) //*main.Person
	pp2.name = "Jochen"
	pp2.sex = "boy"
	pp2.address = "BeiJing"
	pp2.age = 18
	fmt.Println(*pp2) //{Jochen 18 boy BeiJing}

}

//1.定义结构体
type Person struct {
	name    string //go通过命名首字母大小写来限制成员的可见性大小,当大写时候表示可跨包访问该成员变量
	age     int
	sex     string
	address string
}

匿名结构体和匿名字段

package main

import "fmt"

func main() {
	/*
		匿名结构体和匿名字段:匿名就是没有名字的东西

			匿名结构体:没有名字的结构体
				变量名 := struct{
					定义字段Field
				}{
					字段赋值
					}
			匿名字段:结构体中没有名字的成员变量
	*/

	//有名结构体
	s1 := Student{name: "Jochen", class: "三年二班"}
	fmt.Println(s1.name, s1.class) //Jochen 三年二班

	//匿名结构体
	s2 := struct {
		name  string
		class string //匿名结构体定义
	}{name: "小王", class: "一年一班"} //匿名结构体设置初始化值,该项是可选项
	fmt.Println(s2.name, s2.class) //小王 一年一班

	//匿名字段
	s3 := Teacher{"大黄","六年三班"}
	fmt.Println(s3.name, s3.string) //小王 一年一班
	//可以看到其实匿名字段还是有名称的,它将数据类型作为自己的名字了,这意味着没办法在结构体中定义相同类型的匿名字段
	s3 = Teacher{name:"小黄",string:"二年二班"}
	fmt.Println(s3.name, s3.string) //小黄 二年二班
}

type Student struct {
	name  string
	class string
}

type Teacher struct {
	name string
	string //匿名字段,默认使用数据类型作为名字
	//string //duplicate field string 匿名字段类型不能重复,否则会冲突

}

结构体的嵌套

结构体嵌套:当一个结构体的成员变量为另一个结构体,这种结构称为嵌套结构

package main

import "fmt"

func main() {

	p := Person{
		name: "Jochen",
		age:  20,
		address: Address{
			province: "广东省",
			city:     "深圳"}} //如果}不跟在最后的赋值字段后,则该字段后需要添加 “,”  否则编译报错
	fmt.Println(p)           //{Jochen 20 {广东省 深圳}}  -》括号里面有括号就是结构体的嵌套
	fmt.Printf("姓名:%s,年龄:%d,地址:%s-%s\n", p.name, p.age, p.address.province, p.address.city)
	//姓名:Jochen,年龄:20,地址:广东省-深圳

	/*
		因为结构体是值类型,所以结构体成员变量在赋值给别的变量时会重新创建一个副本,导致内存空间浪费,所以建议:
		使用结构体指针作为嵌套的结构体字段,这样在赋值时发生的是引用传递,可以节省内存空间
	*/
	s := Student{
		name: "Tom",
		age:  10,
		address: &Address{
			province: "广东省",
			city:     "惠州",
		},
	}
	fmt.Printf("姓名:%s,年龄:%d,地址:%s-%s\n", s.name, s.age, s.address.province, s.address.city)
	//姓名:Tom,年龄:10,地址:广东省-惠州
}

type Person struct {
	name    string
	age     int
	address Address //嵌套结构体 和别的语言中类里面有别的自定义类型字段没什么区别,通过.访问里面的内容就行
}

type Address struct {
	province, city string
}

type Student struct {
	name    string
	age     int
	address *Address //使用结构体指针节省内存空间
}

拓展

GO语言中的OOP

在学习方法前首先需要了解oop的东西,以便能更好的理解和掌握接下来go语言内容的设计与功能

虽然go语言不是面向对象的语言,但是oop的思想依然可以用到go中,关于oop的思想和传统语言的代码实现在此不做过多阐述(后面内容提到的相关概念,如多态,封装等也不会过多细述),这方面的内容在之前写java篇笔记的时候写的比较多了,感兴趣的朋友可以看一下这篇笔记

go语言中没有类的概念,但是却一样可以实现oop(面向对象)

如下例子使用go语言中的结构体实现oop中的继承性

package main

import "fmt"

func main() {
	/*
	go语言的结构体嵌套:
		1.模拟继承性: is - a
			type A struct{
				field
			}
			type B struct{
				A //匿名字段,字段提升,A中的可被直接访问
			}
		2.模拟聚合关系: has - a
			type C struct{
				field
			}
			type D struct{
				c C //聚合关系,非匿名字段
			}
	 */

	//1.使用匿名字段,通过结构体嵌套模拟继承特性
	a := Animal{name: "啄木鸟", weight: 5}
	//1.3创建父类对象
	fmt.Println(a, a.name, a.weight) //{啄木鸟 5} 啄木鸟 5

	//1.4创建子类对象
	b := Bird{Wing: "大翅膀", Animal: Animal{name: "沙雕", weight: 100}}
	fmt.Println(b) //{{沙雕 100} 大翅膀}

	//在结构体中,如果匿名字段是结构体类型,那么该结构体的字段可以直接被访问,就像子类可以直接访问父类成员一样
	//上面这样的特性称之为提升字段
	b.Animal.name = "小沙雕" //常规赋值方式
	b.name = "大沙雕" //name是嵌套结构体的成员,又是匿名字段,所以嵌套结构体字段被提升出来,可以被直接访问
	fmt.Println(b) //{{大沙雕 100} 大翅膀}
}

//1.1定义父类
type Animal struct {
	name   string
	weight int
}

//1.2定义子类
type Bird struct {
	Animal        //匿名字段,默认认为Bird继承了Animal的所有字段
	Wing   string //翅膀,子类的新增属性
}

这时候有小伙伴就会迷惑,没有了封装继承多态的类,go语言是否少了许多便捷性和封装能力?恰恰相反,对于之前我们写其他语言的代码习惯和功能实现方式,go中都有更加高效,易用、优雅的实现方式,例如后面将介绍到的“方法“与“接口”
学习资料参考:这里

posted @ 2021-01-28 00:48  .Jochen  阅读(155)  评论(0编辑  收藏  举报