Golang面试笔试基础篇:从基础语法考察入手(一)

基础篇:从基础语法考察入手(一)

一、你觉得Golang是怎样一门语言?

  • Golang 是一种高级且通用的编程语言,提供垃圾收集和并发编程的支持,具有非常强的静态类型。
  • 在 Go 中,程序是通过使用有助于有效管理依赖关系的包构建的。它还使用编译链接模型从源代码生成可执行二进制文件。
  • Go 是一种语法简单的语言,具有优雅且易于理解的语法结构。
  • 它具有内置的强大标准库集合,可帮助开发人员解决很多问题,而无需第三方包。
  • Go 对并发具有一流的支持,能够有效地利用多核处理器架构和内存。

二、Golang 是区分大小写还是不区分大小写?

区分大小写。

三、Golang 中 make 和 new 的区别?

new的要求传入一个类型,然后申请一个该类型大小的内存空间,并会初始化为对应的零值,返回指向该内存空间的一个指针。

new是用来分配内存的:

比如我们进行下面这段代码,新建一个指针却不用new给他分配内存:

func main() {
	var p *int
	*p = 2
	fmt.Println(*p)
}
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x1 addr=0x0 pc=0x40b4f6]

就会panic,这是一个空指针。

如果我们给指针用new初始化一块内存:

func main() {
	var p = new(int)
    
	*p = 2
	fmt.Println(*p)
}

输出结果就是:

0
2

都是切片、Map、通道Channel都是引用类型,其存储结构比较复杂,不是用new()分配一块内存,并简单的分配零值就行了。所以要用make来分配和初始化引用类型。

	//Channel
	ch := make(chan string)

	go func() {
		fmt.Println(<-ch)
	}()
	ch <- "hello"
	close(ch)

	//Map
	m := make(map[string]int)
	m["Hello"] = 2
	fmt.Println(m)

	//Slice
	slice := make([]int, 2, 5)
	slice = append(slice, 1)
	fmt.Println(slice)

下面是运行结果:

hello
map[Hello:2]
[0 0 1]   

总的来说:

  • make和new都可用来分配内存,但是new只是将内存分配给变量,并给赋一个零值,而make不但分配了内存,还给引用类型初始化了一些相关属性(这里初始化这些相关属性时不再是简单的用默认零值来初始化了
  • make返回一个引用类型本身,而new返回一个指向零值内存空间的指针。
  • make只能用于分配和初始化slice、map、channel类型,而new可以给一切类型。

四、不同类型的变量,其零值是什么?

不同类型,其零值是不同的,我们可以通过下面的这个例子了解一下:

package main

import "fmt"

func main() {
	type stru struct {
		s string
	}
	var ap *[3]int
	var ip *int
	var sp *string
	var tp *stru

	ap = new([3]int)
	fmt.Println(*ap) //[0 0 0]
	ip = new(int)
	fmt.Println(*ip) // 0
	sp = new(string)
	fmt.Println(*sp) //
	tp = new(stru)
	fmt.Println(*tp) //{}
    
	var a [3]int
	var i int
	var s string
	var t stru

	fmt.Println(a, i, s, t) //[0 0 0] 0  {}
}

下面我们来详细看一下复合类型的零值,先来数组:

func main() {
	var array [5]int
	fmt.Printf("array: %p %#v \n", &array, array)
	var array_p = new([5]int)
	fmt.Printf("array_p: %p %#v \n", &array_p, array_p)
	(*array_p)[3] = 8
	fmt.Printf("array_p: %p %#v \n", &array_p, array_p)
}

我们先来看看运行结果:

array: 0xc00000a3f0 [5]int{0, 0, 0, 0, 0}
array_p: 0xc000006030 &[5]int{0, 0, 0, 0, 0} 
array_p: 0xc000006030 &[5]int{0, 0, 0, 8, 0} 

再来看看切片:

	var slice *[]int
	fmt.Printf("a: %p %#v \n", &slice, slice)
	sliceP := new([]int)
	fmt.Printf("av: %p %#v \n", &sliceP, sliceP)
	(*sliceP)[2] = 8
	fmt.Printf("av: %p %#v \n", &sliceP, sliceP)

输出结果如下:

a: 0xc000006028 (*[]int)(nil)
av: 0xc000006038 &[]int(nil)
panic: runtime error: index out of range [2] with length 0

趁热打铁,再来看看Map的:

package main

import "fmt"

func main() {
	var m map[string]string
	fmt.Printf("m: %p %#v \n", &m, m)
	mP := new(map[string]string)
	fmt.Printf("mv: %p %#v \n", &mP, mP)
	(*mP)["hello"] = "hello"
	fmt.Printf("mv: %p %#v \n", &mP, mP)
}

运行结果如下:

m: 0xc0000ce018 map[string]string(nil)
mv: 0xc0000ce028 &map[string]string(nil) 
panic: assignment to entry in nil map    

还有通道channel:

	cP := new(chan string)
	fmt.Printf("cv: %p %#v \n", &cP, cP)
	cP <- "hello"
invalid operation: cannot send to non-channel cP (variable of type *chan string)

为什么会报错呢?

这是因为切片、Map、通道Channel都是引用类型,其存储结构比较复杂,不是分配一块内存,并简单的给相关属性分配零值就行了(长度、指针…)。所以要用make来分配和初始化引用类型。

五、数组和切片的区别

  • 切片其实本质上是对数组的封装。它包含,底层数组的指针、切片的长度和容量。
  • 数组固定长度,而切片可扩容
  • 数组如果数组长度相等,可以进行比较。而切片只能和nil进行比较,本质是因为数组是值类型,切片是引用类型。
	var s []int
	if s == nil {
		fmt.Println("equal")
	} else {
		fmt.Println("not equal")
	}
	
	//输出结果为: equal
  • 数组作为函数参数是进行值传递,函数内部改变数组元素不会对函数外部数组的函数值。而切片作为函数的参数值是进行指针传递,函数内部改变切片的值会影响函数外部切片的值。

六、for range 的时候它的地址会发生变化么?for 循环遍历 slice 有什么问题?

看一看下面这段代码:

package main

import "fmt"

func main() {
	var slice = []int{1, 2, 3, 4, 5}
	for i, v := range slice {
		fmt.Printf("%d,%#v\n", i, &v)
	}
}

运行结果如下:

0,(*int)(0xc000016098)
1,(*int)(0xc000016098)
2,(*int)(0xc000016098)
3,(*int)(0xc000016098)
4,(*int)(0xc000016098)

我们再来看看下面:

func main() {
	var slice = []int{1, 2, 3, 4, 5}
	fmt.Printf("%#v%#v%#v%#v%#v\n", &slice[0], &slice[1], &slice[2], &slice[3], &slice[4])
	for i, v := range slice {
		fmt.Printf("%#v,%#v\n", &i, &v)
	}
	fmt.Printf("%#v%#v%#v%#v%#v\n", &slice[0], &slice[1], &slice[2], &slice[3], &slice[4])
}
(*int)(0xc00000a3f0)(*int)(0xc00000a3f8)(*int)(0xc00000a400)(*int)(0xc00000a408)(*int)(0xc00000a410)
(*int)(0xc0000160c0),(*int)(0xc0000160c8)
(*int)(0xc0000160c0),(*int)(0xc0000160c8)
(*int)(0xc0000160c0),(*int)(0xc0000160c8)
(*int)(0xc0000160c0),(*int)(0xc0000160c8)
(*int)(0xc0000160c0),(*int)(0xc0000160c8)
(*int)(0xc00000a3f0)(*int)(0xc00000a3f8)(*int)(0xc00000a400)(*int)(0xc00000a408)(*int)(0xc00000a410)

可以得知,for range的时候,不是读取slice,然后直接输出里面的值。而是在遍历时开辟两块内存以值覆盖的方式将遍历得到的数据放入。由于这个特性,我们如果在for range里面开协程,不能直接把i、v的地址传给协程,也不能直接在for range内保存i,v的地址。如果遍历一个指针切片:

	var slice = []int{1, 2, 3, 4, 5}
	var slice_p = make([]*int, 0, 5)
	for i, v := range slice {
		fmt.Println(i, v)
		slice_p = append(slice_p, &v)
	}
	fmt.Println(slice_p)
	for i, v := range slice_p {
		fmt.Println(i, *v)
		fmt.Println(v)
	}

那么结果将会是:

0 1
1 2                                                               
2 3                                                               
3 4                                                               
4 5                                                               
[0xc000016098 0xc000016098 0xc000016098 0xc000016098 0xc000016098]
0 5                                                               
0xc000016098                                                      
1 5                                                               
0xc000016098                                                      
2 5                                                               
0xc000016098                                                      
3 5                                                               
0xc000016098                                                      
4 5                                                               
0xc000016098    

那么解决办法是什么?使用一个临时变量即可。

	var slice = []int{1, 2, 3, 4, 5}
	var slice_p = make([]*int, 0, 5)
	for _, v := range slice {
		vv := v
		slice_p = append(slice_p, &vv)
	}
	fmt.Println(slice_p)
	for i, v := range slice_p {
		fmt.Println(i, *v)
		fmt.Println(v)
	}
[0xc0000aa058 0xc0000aa070 0xc0000aa078 0xc0000aa080 0xc0000aa088]
0 1         
0xc0000aa058
1 2         
0xc0000aa070
2 3         
0xc0000aa078
3 4         
0xc0000aa080
4 5         
0xc0000aa088

另外我们也可以发现,当切片是指针切片时,切片里面是什么,for range遍历时还是切片里面的地址,不需要另外开辟。

七、go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?

根据Golang官方文档描述,defer就像一个LIFO的栈,每次执行defer语句,都会将函数”压栈“,函数参数也会被保存下来;如果外层函数(非代码块)退出,最后的defer语句就会执行,也就是栈顶的函数或方法会被执行。

一条return语句,其实不是一条原子指令,其大概可以分为三条指令:

  • 返回值为xxx
  • 调用defer函数
  • 空的return
package main

import (
	"fmt"
)

func main() {
	fmt.Println("return:", banana()) 
}

func banana() (i int) {
	defer func() {
		i++
		fmt.Println("defer 2:", i) 
	}()
	defer func() {
		i++
		fmt.Println("defer 1:", i) 
	}()
	return i 
}

defer 1: 1
defer 2: 2
return: 2

这是有名返回值的情况,接下我们来看一看匿名返回值的情况:

package main

import (
	"fmt"
)

func main() {
	fmt.Println("return:", apple())
}

func apple() int {
	var i int
	defer func() {
		i++
		fmt.Println("defer 2:", i)
	}()
	defer func() {
		i++
		fmt.Println("defer 1:", i)
	}()
	return i
}

defer 1: 1
defer 2: 2
return: 0

上面这两段代码说明了:defer语句只能访问有名返回值,不能直接访问匿名返回值。

但是如果是下面这种情况:

package main

import (
	"fmt"
)

func main() {
	fmt.Println("return:", banana())
}

func banana() (i int) {
	defer func(i int) {
		i++
		fmt.Println("defer 2:", i)
	}(i)
	defer func(i int) {
		i++
		fmt.Println("defer 1:", i)
	}(i)
	return i
}

输出结果就为:

defer 1: 1
defer 2: 1
return: 0

这是因为传递给defer后面的匿名函数的是形参的一个复制值,不会影响实参i。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ReganYue

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值