文章目录

  • 一、指针
  • 1.1 指针地址和指针类型
  • 1.2 指针取值
  • 1.3 指针传值
  • 二、new make
  • 2.1 new
  • 2.2 make
  • 2.3 new VS make
  • 三、函数
  • 3.1 声明函数
  • 3.2 函数返回值
  • 3.3 返回值
  • 3.4 返回值补充
  • 四、函数类型与变量
  • 五、高级函数
  • 5.1 把函数作为入参
  • 5.2 把函数作为返回值
  • 5.3 匿名函数
  • 5.4 闭包
  • 六、defer
  • 七、go内置函数
  • 八、panic recover


一、指针

Go中的指针和C中的指针不同,Go中的指针是安全指针,不能偏移和计算,作用无非也就是 取址 【&】 和 取值【*】

内存中数据总是有内存地址的,内存地址就是指针变量的值。
比如:
我把 “我是中国人” 这个串赋值给 变量 A;
把内存地址赋值给变量 B;
B 就是一个指针变量

1.1 指针地址和指针类型

Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。
基本语法如 ptr:= &v ,其中 v的类型为T

  • v : 被取址的变量,类型为T
  • ptr :指针变量 ,用于接受地址,ptr的类型就是*T ,* 代表指针
func fn15(){
	var a = 10
	var b = &a
	//  b is a pointer
	fmt.Printf("type:%T , b: %v", b, b) //type:*int , b:0xc00001a078
}

指针图示:

golang func后对象 指针 区别_匿名函数

1.2 指针取值

使用 * 来取值:

func fn15(){
	var a = 10
	var b = &a	
	fmt.Printf("%v", *b) // b is a pointer variable
}

1.3 指针传值

只要记住: *a 能把指针对应地址的值取出来即可。

func fn16(){
	var a = 1
	modify(a)
	fmt.Printf("a=%v \n", a)  // a = 1
	modify2(&a)	// 注意:这里传入的不是 a ,而是 a 的地址(指针)
	fmt.Printf("a=%v \n", a) // a=100 
}

func modify(x int){
	x =10
}

func modify2(x *int){
	*x = 100
}

有一个要注意的地方:

func fn17(){
	var a *int  //a is a pointer var 
	*a = 100
	fmt.Println(*a)  // panic: runtime error: invalid memory address or nil pointer dereference
}

再看:

func fn18(){
	var b map[string]int   // 这里没有初始化
	b["test"]=1 // panic: assignment to entry in nil map 
	fmt.Println(b)  
}

这个case会报错,原因在于:

  • go对于引用类型 的变量,使用时不仅要声明它,还要为其分配内存空间,否则无法存值。
  • 对于值类型的变量,声明时不需要分配内存空间,因为在声明时已经默认分配好了内存。
  • 要分配内存就需要使用到makenew

二、new make

2.1 new

func new(Type) *Type

  • 只接受一个参数,即:类型
  • 返回的是一个指针,即*Type,并且该指针对应的值为该类型的 零值

比如说:

func fn19() {
	var a = new(int)
	fmt.Printf("%T \n", a)   // *int
	fmt.Printf("%v \n", *a)  // 0

	var b = new(bool)
	fmt.Printf("%T \n", b)  // *bool
	fmt.Printf("%v \n", *b)  // false
}

func fn21(){
	var a *int
	fmt.Println(a) // <nil>
	fmt.Printf("%T \n", a)  // *int

	a = new(int)
	fmt.Println(a) // 0xc0000120a8 

	*a = 10
	fmt.Println(*a) //10

	b := &a  // a 是指针 ,b 是指针的指针
	fmt.Printf("%T \n", b)  // **int 
	fmt.Println(b)  // 0xc000006028 
}

2.2 make

make也是用于内存分配的,区别于new,
它只用于slice、map以及channel的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

func make(t Type, size ...IntegerType) Type

func fn20(){
	var b = make(map[string]int)  // 声明并分配内存空间
	b["test"] = 1 
	fmt.Println(b)   // map[test:1]
}

2.3 new VS make

  • 都用于内存分配
  • make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身
  • new用于 基础类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针

三、函数

Go中支持函数、匿名函数、闭包,函数在Go中是一等公民

3.1 声明函数

啥也不说,先举个栗子:

func intSum(x ...int) int {
	var sum = 0
	for _, v := range x {
		sum = sum + v
	}
	return sum
}
  • 可变参数 x 本质是一个 切片
  • 可变参数一般要放到参数的最后

3.2 函数返回值

go语言的函数可以返回多个返回值,这个跟Java倒是不大一样,也十分有趣。

func handle(x int,y int) (int ,int){
	return x -y ,x +y
}
func fn34() {
	x, y := handle(10, 5)
}
  • 如果有多个返回值,需要用()将返回值类型包起来

3.3 返回值

函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return关键字返回。【Java表示做不到】

func handle2(x int,y int)(sum int,sub int){
	sum = x +y
	sub = x- y
	return
}

3.4 返回值补充

函数返回值类型为slice时,nil也是有效的slice,没必要显示返回一个长度为0的切片。

func fn36() [] int {
	// 没必要返回  []int{}
	return nil
}

四、函数类型与变量

go支持函数类型的变量,这个在Java中是木有的,没法在java中将一个方法赋值给一个变量。函数类型变量的存在,也说明了函数在Go中是一等公民

下面格式定义一个函数类型:这个函数接收两个int类型的函数,返回一个int类型的返回值。只要是满足这个条件的函数(比如下面的add sum)都属于 calculation类型。

type calculation func(int, int) int

func add(x int, y int) int {
	return x + y
}
func sub(x int, y int) int {
	return x - y
}

func fn35() {
	var cal calculation
	cal = add 
	sum := cal(10,20)
	fmt.Printf("%v \n", sum)
}

有了函数类型变量,我们就可以很方便地传递一个函数。

五、高级函数

5.1 把函数作为入参

func fn37(){
	result := calculate(1,10, add)
	fmt.Println(result)

	result = calculate(1,10, sub)
	fmt.Println(result)
}

func calculate(x int,y int,handler func(a,b int) int) int {
	return handler(x,y)
}

5.2 把函数作为返回值

func fn38() {
	a, _ := do("+")
	result := a(1, 20)
	fmt.Println(result)
}

func do(s string) (calculation, error) {
	switch s {
	case "+":
		return add, nil
	case "-":
		return sub, nil
	default:
		return nil, errors.New("not support this operand")
	}

}

5.3 匿名函数

函数可以作为返回值,但是在函数内部不能直接再定义普通函数了,可以定义匿名函数
匿名函数在回调函数和闭包中用途较多

func fn39() {
	// 1 匿名函数赋值给变量
	add := func(x, y int) int {
		return x + y
	}
	result := add(1, 11)
	fmt.Println(result)
	// 2 匿名函数立刻执行
	result = func(x, y int) int {
		return x - y
	}(2, 1)
	fmt.Println(result)
}

5.4 闭包

只要记住:闭包,就是 函数+ 函数的运行环境(函数外部变量的引用)。 要说闭包的原理,其实就两句:

  • 函数可以作为返回值
  • 函数中查找变量的顺序: 先在函数内部的代码块中找,然后往代码块外,全局这样一层层找

case0: 闭包的一个典型应用

func main() {
	fn46()
}

func fn46(){
	ret := foo3(foo2, 2,20)
	foo1(ret)
}

// 一次接口是如此定义的:以无参的函数作为入参
func foo1(f func()) {
	fmt.Println("this is f1")
	f()
}

// 另一个接口是如此定义的,接收 x,y 两个参数。
//假如 foo1 是别人的接口,要让 foo2 去适配 foo1,我们没法直接把 foo2 
// 传给 foo1 .这里就是  闭包的  典型应用场景
func foo2(x, y int) {
	fmt.Println("this is f2")
	fmt.Println(x + y)
}


//适配 f2 ,使其返回一个  没有入参、没有出参的函数类型
func foo3(f func(int,int) , a int,b int) func(){
	fmt.Println("this is f3")
	ret := func ()  {
		f(a,b)
	}
	return ret
}

case1:

func fn40() {
	f := adder()      //f 是个 闭包= f函数 + f运行环境(持有外部作用域的变量 var x)
	fmt.Println(f(1)) //1
	fmt.Println(f(3)) // 4
	fmt.Println(f(5)) //9
}

func adder() func(int) int {
	var x = 0
	return func(i int) int {
		x = x + i
		return x
	}
}

case2:

func fn41() {
	f := suffix(".aa")
	fmt.Println(f("test")) //test.aa

	f1 := suffix(".bb")
	fmt.Println(f1("test")) //test.bb
}

func suffix(s string) func(string) string {
	return func(name string) string {
		if !strings.HasSuffix(name, s) {
			return name + s
		}
		return name
	}
}

case3:

func fn42() {
	add, sub := calc(10)

	// 返回的函数,都没有去修改 base 参数的值
	fmt.Println(add(1), sub(2)) //11 8 
	fmt.Println(add(3), sub(4)) //13 6
}

func calc(base int) (func(int) int, func(int) int) {
	add := func(delta int) int {
		return base + delta
	}
	sub := func(delta int) int {
		return base - delta
	}
	return add, sub
}

六、defer

defer会将后面跟随的语句延时处理:在defer所在的函数即将return时,将defer的语句按defer的顺序逆序执行。这个关键字在处理 资源释放、解锁、记录时间时特别有用。Java没有在语言层面提供类似的特性。

不如看个例子:

func fn42() {
	add, sub := calc(10)

	// 返回的函数,都没有去修改 base 参数的值
	fmt.Println(add(1), sub(2)) //11 8 
	fmt.Println(add(3), sub(4)) //13 6
}

func calc(base int) (func(int) int, func(int) int) {
	add := func(delta int) int {
		return base + delta
	}
	sub := func(delta int) int {
		return base - delta
	}
	return add, sub
}

defer的本质:
在Go中return不是一个原子操作return x实际分为了两个指令:

  • 为返回值赋值
  • RET指令

defer执行的时机就在返回值赋值操作后,RET指令执行前

golang func后对象 指针 区别_Go_02

下面来看一个很有意思的面试热题:

Case1:

func fn1() {
	fmt.Println(f1()) //5
	fmt.Println(f2()) //6
	fmt.Println(f3()) //5
	fmt.Println(f4()) //5
	fmt.Println(f5()) //5
	fmt.Println(f6()) //5
	fmt.Println(f7()) //6
}

func f1() int { // 没有给返回值 命名
	x := 5
	defer func() {
		x++ // 修改的 x 并不是返回值,而是 f1 函数的局部变量
	}()
	return x //  return 的三个操作:1.返回值赋值 2.defer 3.真正的RET 命令
}

func f2() (x int) { // 返回值 x
	defer func() {
		x++
	}()
	return 5 // 给返回值赋值 x =5 ; 执行defer 中的 x++,则x=6 ;return x,则return 6
}

func f3() (y int) { //返回值 y
	x := 5
	defer func() {
		x++
	}()
	return x //给返回值赋值 y =x=5 ; 执行 defer 中的x++ ,修改了x=6, y 不变;return y=5
}

// f4  和  f5 其实是 完全等价的函数
func f4() (x int) {
	defer func(x int) {
		// 只是将 函数中的 x 变量的副本加了1 而已,其实这里主要是变量命名 x 容易迷惑人,假如改成别的命名,比如 y 就
		// 容易区分了
		x++
	}(x)
	return 5 //给返回值赋值 x=5
}

func f5() (x int) {
	defer func(y int) {
		y++
	}(x)
	return 5
}

func f6() (x int) {
	defer func(x int) int {
		x++ // 修改的仍然只是 这个立即执行的匿名函数 的变量副本
		return x
	}(x)
	return 5 //给返回值赋值 x=5
}

// 传一个 变量 指针到匿名函数中去
func f7() (x int) {
	defer func(y *int) {
		(*y)++
	}(&x)
	return 5
}

再来看defer的另一个特性:defer注册要延迟执行的函数时该函数所有的参数都需要确定其值

func fn() {
	x := 1
	y := 2
	defer calc("AA", x, calc("A", x, y))
	 //  calc("A", x, y) 这个语句并不是在return前执行的,而是在 y:=2 之后就执行了
	 // 只不过最后在 return前,执行了整个 defer 语句
	x = 10
	defer calc("BB", x, calc("B", x, y))
	 //  calc("B", x, y) 这个语句并不是在return前执行的,而是在 x = 10 之后就执行了
	 // 只不过最后在 return前,执行了整个 defer 语句
	y = 20
}

func calc(index string, a, b int) int {
	ret := a + b
	fmt.Println(index, a, b, ret)
	return ret
}
// A 1 2 3
// B 10 2 12
// BB 10 12 22
// AA 1 3 4

七、go内置函数

  • close : 用来关闭channel
  • len
  • new : 分配内存;分配值类型,比如 int struct,返回指针
  • make :分配内存,分配引用类型,比如 map slice channel,返回集合
  • append
  • panic 、 recover

八、panic recover

目前go (1.15)没有异常机制,但是可以用 painc recover来处理错误。go的设计思想是所有的异常都是具体的值,这可能导致开发时可以会出现很多的对错误的判断,这也是go为人诟病的地方。 panic recover并不是 try catch在go中的实现。

go中的panic可以在任何地方引发,但recover只有在defer调用的函数中有效:

先看个简单的例子:

func fn2() {
	f8()
	f10()
	f9()
}

func f8(){
	fmt.Println("begins....")
}

func f9(){
	fmt.Println("ends....")
}
// 主动 触发了 panic
func f10(){
	panic("panic happens")
}

再看另一个例子:

func fn2() {
	f8()
	f10()
	f9()
	//  这句还是会执行的
	fmt.Println("here it still runs")
}

func f8(){
	fmt.Println("begins....")
}

func f9(){
	fmt.Println("ends....")
}
// 主动 触发了 panic
func f10(){
	defer func ()  {
		err:= recover()
		if err!=nil {
			fmt.Println("recovers....")
		}
	}()
	panic("panic happens")
}

这里要注意:

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