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


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

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

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

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

    区分大小写。

    三、Golang 中 make 和 new 的区别?

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

    new是用来分配内存的:

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

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

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

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

    func main() {
    	var p = new(int)
        
    	*p = 2
    	fmt.Println(*p)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出结果就是:

    0
    2
    
    • 1
    • 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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    下面是运行结果:

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

    总的来说:

    • 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  {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

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

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们先来看看运行结果:

    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} 
    
    • 1
    • 2
    • 3

    再来看看切片:

    	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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    输出结果如下:

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

    趁热打铁,再来看看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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    运行结果如下:

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

    还有通道channel:

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

    为什么会报错呢?

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

    五、数组和切片的区别

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

    六、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)
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    运行结果如下:

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

    我们再来看看下面:

    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])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    (*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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以得知,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)
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    那么结果将会是:

    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    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    	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)
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    [0xc0000aa058 0xc0000aa070 0xc0000aa078 0xc0000aa080 0xc0000aa088]
    0 1         
    0xc0000aa058
    1 2         
    0xc0000aa070
    2 3         
    0xc0000aa078
    3 4         
    0xc0000aa080
    4 5         
    0xc0000aa088
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    另外我们也可以发现,当切片是指针切片时,切片里面是什么,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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    上面这两段代码说明了: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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    输出结果就为:

    defer 1: 1
    defer 2: 1
    return: 0
    
    • 1
    • 2
    • 3

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

  • 相关阅读:
    【英语:发音基础】A4.基础词汇-常用动词
    【Typora】解决单词爆红问题
    Python 0/1背包问题
    使用Psycopg2连接openGauss 3.0(python3)
    数论练习题
    MySQL表的内连和外连
    Kotlin高仿微信-第34篇-支付-向商家付款(二维码)
    application.properties和bootstrap.properties的加载时机
    【django2.0之Rest_Framework框架三】rest_framework认证,权限,限流,过滤,排序,分页,异常处理,生成文档介绍
    对已知数组排序
  • 原文地址:https://blog.csdn.net/qq_36045898/article/details/126689865