首页 > 编程知识 正文

go面试题目,golang初级面试题

时间:2023-05-05 01:29:54 阅读:178704 作者:2638

简述对Golang方法有什么特别之处的函数的定义声明中没有收件人。

的声明与函数相似。 他们的不同之处在于,方法在定义时会在func和方法名称之间添加参数。 此参数为接收者,这样我们定义的此方法绑定到收件人,称为此收件人的方法。

o语言包括值接收者和指针接收者

值类型接收者中定义的方法在调用时为使用的其实是值接收者的一个副本,因此对该值的任何操作都将为不会影响原来的类型变量。 -------相当于形式参数

使用指针作为接收者时,指针接收者会传递指向原始值的指针副本。 指针副本指向原始类型的值,因此更改时为同时也会影响原来类型变量的值

2. Golang可变参数函数方法可以有任意数量的参数。 这样的参数称为可变参数。 例如,我们常用的fmt.Println ()这样的函数可以接收可变参数。

可以有任意数量的变量。 我们也可以自己定义可变参数。 可变参数的定义,在类型前面加上省略号…就可以了。

func main () print (,v:=rangea ) fmt.Println )、v: ) {for _,v:=rangea ) fmt.Println ) v

变量参数本质上是数组,因此使用它的方式与使用数组的方式相同,如示例中的for range循环。

3 .为什么3. Golang导入软件包时可能会使用“_”/“.”进行导入? 例如,如果在包之前导入下划线_包,则会执行该包下文件中的所有init函数,但您可能不希望仅使用init函数来导入整个包。 不使用包中的其他函数。

每个软件包可以包含任意数量的init函数,这些init函数在main函数之前执行。 init函数通常用于初始化变量、软件包设置或其他需要在程序运行之前启动的任务。 例如,需要使用_null标记来部署软件包是为了执行此软件包中的init函数。

以数据库的驱动程序为例,为了统一对数据库的访问,Go语言使用databases/sql抽象出数据库的操作,并能对MYSQL、Postgre等数据库进行操作。 这样,不管使用这些数据库的哪个驱动程序,编码操作都是一样的,想更改驱动程序的时候,不更改具体的代码就可以直接更改。

这些数据库驱动程序的实现很具体,任何人都可以实现。 其原理是定义init函数,在程序运行之前,在sql包中注册实现的驱动程序,并使用它来操作数据库。

package MySQL import (database/SQL ) (func init ) ) SQL.register,MySQLDriver{}因为我只是想运行此MySQL包的init方法

import ' database/SQL ' import _ ' github.com/go-SQL-driver/MySQL ' db,err:=SQL.open(MySQL ',' USQL )

导入此包后,调用此包的函数时,此点操作的含义是前缀中的包名称(println,省略之前调用的fmt.println("helloworld " ) )

4. Slice与数组对比与Go中隐式使用c数组变量作为指针不同,Go数组是值类型,赋值和函数参数操作复制整个数组数据。 如果每次传递参数时都使用数组,则每次都会复制数组。 如果数组大小为100万,则64位计算机需要约800W字节或8MB的内存。 这样会消耗大量的内存。

因此,有人考虑过函数传递数组的指针。 通过以这种方式更高效地利用内存,性能也比以前有所提高。

但是,传递指针有弊端。 万一原始数组中的指针方向发生更改,函数中的指针方向也会更改。

切片的优势也将显现出来。 通过分片传递数组参数,既可以达到节约内存的目的,也可以合理处理共享内存问题。 打印结果的第二行是切片,切片指针与原始数组的指针不同。

并非所有情况下都适合使用切片代替数组。 因为切片下的数组可以为堆分配内存,而小数组对堆栈上副本的消耗不一定大于make。

5. Golang Slice的底层安装片是基于数组安装的,其基础是数组,本身非常小,是基于底层数组的抽象数组安装的,所以其底层http://www

效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化

切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一个只读对象,其工作机制类似数组指针的一种封装。

切片对象非常小,是因为它是只有3个字段的数据结构:

指向底层数组的指针切片的长度切片的容量

这3个字段,就是Go语言操作底层数组的元数据。

6. Golang Slice的扩容机制,有什么注意点?

Go 中切片扩容的策略是这样的:

首先判断,如果新申请容量大于2倍的旧容量,最终容量就是新申请的容量否则判断,如果旧切片的长度小于1024,则最终容量就是旧容量的两倍否则判断,如果旧切片长度大于等于1024,则最终容量从旧容量开始循环增加原来的 1/4, 直到最终容量大于等于新申请的容量如果最终容量计算值溢出,则最终容量就是新申请容量

伪代码:

if cap > 2*oldCap {newCap = cap} else if cap < 1024 {newCap = 2*oldCap} else {newCap = oldCapwhile (newCap < cap && !overflow) {newcap += newcap/4}if overflow {newCap = cap}}

扩容前后的Slice是否相同?

情况一:
原数组还有容量可以扩容(实际容量没有填充完),这种情况下,扩容以后的数组还是指向原来的数组,对一个切片的操作可能影响多个指针指向相同地址的Slice。

情况二:
原来数组的容量已经达到了最大值,再想扩容, Go 默认会先开一片内存区域,把原来的值拷贝过来,然后再执行 append() 操作。这种情况丝毫不影响原数组。

要复制一个Slice,最好使用Copy函数。

7. Golang的参数传递、引用类型

Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型指针、map、slice、chan等这些),这样就可以修改原内容数据。

Golang的引用类型包括 slice、map 和 channel。它们有复杂的内部结构,除了申请内存外,还需要初始化相关属性。内置函数 new 计算类型大小,为其分配零值内存,返回指针。而 make 会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针。

8. Golang Map底层实现

Golang中map的底层实现是一个散列表,因此实现map的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫hmap(a header for a go map),一个叫bmap(a bucket for a Go map,通常叫其bucket)。hmap如下所示:

图中有很多字段,但是便于理解map的架构,你只需要关心的只有一个,就是标红的字段:buckets数组。Golang的map中用于存储的结构是bucket数组。而bucket(即bmap)的结构是怎样的呢?

bucket:

相比于hmap,bucket的结构显得简单一些,标红的字段依然是“核心”,我们使用的map中的key和value就存储在这里。“高位哈希值”数组记录的是当前bucket中key相关的“索引”,稍后会详细叙述。还有一个字段是一个指向扩容后的bucket的指针,使得bucket会形成一个链表结构

整体的结构应该是这样的:

Golang把求得的哈希值按照用途一分为二:高位和低位。低位用于寻找当前key属于hmap中的哪个bucket,而高位用于寻找bucket中的哪个key。

需要特别指出的一点是:map中的key/value值都是存到同一个数组中的。这样做的好处是:在key和value的长度不同的时候,可以消除padding带来的空间浪费。

Map的扩容:
当Go的map长度增长到大于加载因子所需的map长度时,Go语言就会将产生一个新的bucket数组,然后把旧的bucket数组移到一个属性字段oldbucket中。
注意:并不是立刻把旧的数组中的元素转义到新的bucket当中,而是,只有当访问到具体的某个bucket的时候,会把bucket中的数据转移到新的bucket中。

9. Golang接口接收规则

实体类型以值接收者实现接口的时候,不管是实体类型的值,还是实体类型值的指针,都实现了该接口。

实体类型以指针接收者实现接口的时候,只有指向这个类型的指针才被认为实现了该接口

Methods ReceiversValues(t T)T and *T(t *T)*T10. Context 11. wait / waitGroup / wait.Until 12. Goroutine / chan 13. defer

向 defer 关键字传入的函数会在函数返回之前运行。假设我们在 for 循环中多次调用 defer 关键字,会倒序执行所有向 defer 关键字中传入的表达式。

defer 传入的函数不是在退出代码块的作用域时执行的,它只会在当前函数和方法返回之前被调用。

预计算参数

Go 语言中所有的函数调用都是传值的,defer 虽然是关键字,但是也继承了这个特性。假设我们想要计算 main 函数运行的时间,可能会写出以下的代码:

func main() {startedAt := time.Now()defer fmt.Println(time.Since(startedAt))time.Sleep(time.Second)}$ go run main.go0s

然而上述代码的运行结果并不符合我们的预期,这个现象背后的原因是什么呢?经过分析,我们会发现调用 defer 关键字会立刻对函数中引用的外部参数进行拷贝,所以 time.Since(startedAt) 的结果不是在 main 函数退出之前计算的,而是在 defer 关键字调用时计算的,最终导致上述代码输出 0s。

想要解决这个问题的方法非常简单,我们只需要向 defer 关键字传入匿名函数.

底层实现

runtime._defer 结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表。

defer 关键字的实现主要依靠编译器和运行时的协作:

编译期;
将 defer 关键字被转换 runtime.deferproc;
在调用 defer 关键字的函数返回之前插入 runtime.deferreturn;运行时:
runtime.deferproc 会将一个新的 runtime._defer 结构体追加到当前 Goroutine 的链表头;
runtime.deferreturn 会从 Goroutine 的链表中取出 runtime._defer 结构并依次执行;

我们在前面提到的两个现象在这里也可以解释清楚了:

后调用的 defer 函数会先执行:
后调用的 defer 函数会被追加到 Goroutine _defer 链表的最前面;
运行 runtime._defer 时是从前到后依次执行;函数的参数会被预先计算;
调用 runtime.deferproc 函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算; 14. 锁 15. 反射 16. sync.Once

sync.Once 是 Golang package 中使方法只执行一次的对象实现,作用与 init 函数类似。但也有所不同。

init 函数是在文件包首次被加载的时候执行,且只执行一次
sync.Onc 是在代码运行中需要的时候执行,且只执行一次
当一个函数不希望程序在一开始的时候就被执行的时候,我们可以使用 sync.Once 。

package mainimport ("fmt""sync")func main() {var once sync.OnceonceBody := func() {fmt.Println("Only once")}done := make(chan bool)for i := 0; i < 10; i++ {go func() {once.Do(onceBody)done <- true}()}for i := 0; i < 10; i++ {<-done}}# Output:Only once 17. Go 并发示例-Pool资源池

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。