Go 面试题集锦
Go 中赋值的几种形式?
s := "" var s string var s = "" var s string = ""
Go 如何运用
++ \ --
操作运算符?Go 中的
++ \ --
操作运算符与其它语言有本质区别(例如C语言),Go 中定义的++
或者--
操作并不是表达式,所以无法 实现类似C 语言这样的j = i++
操作, 并且 Go语言中没有--i
这样的前置操作Go 中指针操作是如何实现的?
Go 语言虽然使用的是值传递方式,但是并不代表它不支持指针操作,针对自定义的或者大数据量的数据结构,如果采用值传递,将造成大量的内存交换浪费以及时间浪费,值传递需要对对象进行拷贝的。所以针对此种情况Go 支持指针操作,通过
&
可以返回一个变量的内存地址,*
此操作可以获取指针指向的变量内容。并且 Go 中没有指针运算,不能针对指针进行加减运算Go 中如何定义变量,以及变量如何被外部使用?
Go 针对变量操作和其他语言相同,存在作用域限制,如果变量定义在函数内部, 那么只能在函数内部使用,函数外不可见, 如果变量在函数外定义, 那么整个包都可变(Go 么有类的概念), 如果变量采用的是大写字母开头的, 那么次变量可以在包外被访问(Go 默认所有的大写开头内容都是可导出的,可被包外部访问的)
Go 中常见的几种类型声明?
Go 中定义了几种类型声明方式,分别对应不同的类型
var
,const
,type
,func
Go 如何定义变量?
参考上边Go 语言是强类型语言吗, 是否在定义变量时必须指定变量类型?
Go 语言是强类型语言,变量在定义时虽然不需要指定类型,但是Go 会根据变量的值进行类型推断,如果第一次推断后,变量类型确定,之后如果给变量赋值不同类型值,那么将无法通过编译
Go 中
slice, map, chan ,接口
分别对应的默认值是什么?go 中每种类型都有其对应的默认值,在声明变量时,默认进行初始化,以上几种类型的默认值都为
nil
。 并且Go语言中不存在没有初始化的变量Go 中定义的变量是如何初始化的? 包级别变量与局部变量初始化时机是否相同?
Go中如果定义在包级别的变量,所有变量会在
main
入口函数执行前完成初始化,此处涉及到 go 中包初始化的过程,详细的会在包初始化过程中介绍。 局部变量会在声明语句执行前完成初始化操作 。两种变量的初始化时机是完全不相同的针对
:=
操作, Go 是如何定义的?:=
是快速进行变量声明和初始化的方式,如果这个变量在当前作用域中已经声明过了,那么次操作就是针对此变量的赋值操作,并且使用:=
操作时,必须有一个变量是新的没有被声明过的f, err = os.Open(infile) out, err = os.Create(outfile) // 第二步操作时对 err 的重新赋值并且,声明了一个新的变量 out
使用此操作时,必须有一个变量是新声明的,不然是无法使用的,并且其只针对当前作用域下的存在的变量起赋值作用,如果没有存在,将重新声明新的变量
New 函数操作了解吗?其在使用时是否有需要注意的地方?
new
函数返回的是一个指针,new
可以创建任意类型变量,例如:p := new(int)
. 每次调用new
函数都会返回新的变量地址(不论什么数据类型),new
只是一个预定义函数,并不是一个关键字,可以对其进行重新定义操作
__如果两个类型都为空的,类型大小为0(struct{}或者[]int)等,有可能返回相同的地址。尽量不要使用类型大小为0的,,容易出现runtime.SetFinalizer
错误变量的生命周期是怎样的?垃圾回收机制是如何实现的? 在何种情况下会存在变量逃逸问题?
对于包一级的变量,其生命周期和整个程序生命周期是相同的。并且在代码实现上,如果存在换行操作,但是并不是语句结束需要在其尾部添加
,
以避免编译器自动添加换行符
垃圾回收机制 : 是通过从包级变量和每个当前运行函数的局部变量开始,通过指针或引用的访问路径遍历是否可以找到该变量,如果不存在路径就说明当前运行程序没有对其进行引用,此变量的销毁与否与当前运行程序无影响,垃圾回收机制可以对其进行回收
变量逃逸: go语言中将变量分配在堆或者栈上并不是通过new关键字来区分的,当函数运行完毕,其中局部变量有可能并不会释放,此种情况称为变量逃逸,这种变量一般在堆上创建。type Integer int 与 int 是否是相同类型?
通过
type Integer int
声明一个新的类型Integer
其底层类型是 int 类型,不过,需要注意,虽然与 int 类型的底层类型是相同的,不过在go 中,两者代表的是不同的类型,其并不能进行兼容,所以两者是不同类型包是如何进行初始化的?
包的初始化首先是解决包级变量的依赖顺序,然后按照包级声明出现进行初始化。其每个包都可以有
init
方法,在包初始化时,默认进行执行,所以可以通过定义此方法来针对复杂的初始化过程进行简化。并且go 中针对init
的实现没有任何限制,每个包都可以有多个init
函数以下代码中,其中是否存在不当的地方?
var cwd string func main(){ cwd , err := os.Getwd() if err != nil { } }
以上代码 存在 两个
cwd
变量,一个是全局的,一个是局部变量,以上是:=
操作实现,不过忘记了作用域的问题存在,,在 main 函数 内部 是会重新创建cwd
变量的Go 中存在几种数据类型?
- 基础类型 (数字, 字符, 布尔)
- 复合类型 (数组, 结构体, 字符串)
- 数组类型 是一种值类型,虽然可以修改,但是其作为参数传递时,采用的是复制形式处理
- 字符串底层是字节数组,但字符串复制只是复制了数据地址和对应长度,不会导致底层数据的复制
- 引用类型 (指针,切片,字典,函数,通道)
- 切片 切片底层也是数据类型的数组,但是每个切片有独立的长度和容量信息,切片复制和函数传参也是讲切片头信息部分按传值方式处理(切片头包含底层数据的指针,所以复制也不会导致底层数据的复制)
- 接口类型
除了闭包函数以引用的方式对外部变量访问之外, 其他赋值和函数传参都是以传值的方式处理。
数组有几种常用的定义方式?
var a [3]int // 3个元素, 都为 0 var b = [...]int{1,2,3} //3个元素,1, 2, 3 var c = [...]int{2:3,1:2} //3个元素,第一个元素默认的 0 ,第二个元素 2, 第三个元素为3 var d = [...]int{1,2,4:5,6} //6个元素,1,2,0,0,5,6
字符串数组, 结构体数组,函数数组,接口数组,管道数组
切片的定义方式?
a []int //nil 切片,和 nil 相等,表示不存在的切片 b = []int{} //空切片 c = []int{1,2,3} //是哪个元素,len和cap 都为3 d = c[:2] //两个元素,len为2. cap 为3 e = c[0:2:cap(c)] //两个元素,len 为2, cap 为3 f = c[:0] //0个元素,len为 0, cap 为3 g = make([]int, 3) //三个元素,len和cap 都为 3 h = make([]int,2,3) //2个元素,len为2, cap 为3 i = make([]int, 0,3) //0 个元素,len为0, cap 为3 // cap 表示容量
在针对切片操作时,是进行的指针操作还是值操作?
在对切片操作时,和数组相同进行的是值操作,并不是使用的指针操作,虽然只是操作的切片的头信息,底层数据并没有进行复制操作
在切片头追加内容会不会有性能为题?
针对切片头追加会导致整个切片的内存重新分配,导致所有元素全部被复制1次,所以会影响性能
针对切片的删除是如何实现的呢?是否使用delete实现?
针对切片的删除操作,系统并没有给定特定的方法实现,不过可以通过
append
或者copy
放法来实现
不过在针对切片删除时,需要注意内存管理的问题,删除切片的内容时需要特别注意内存的引用问题,可以先将对应的内存至为 nil ,然后将需要的不分转换为新的切片var a []*int{...} a[len(a)- 1] = nil a = a[:len(a) - 1]
Go 中切片的类型转换是如何实现的?
var a = []float64{3,4,6,7,8,88} func SortFloat64FastV1(a []float64){ //强制类型转换 var b[]int = ((*[1<<20]int)(unsafe.Pointer(&a[0])))[:len(a):cap(a)] } func SortFloat64FastV2(a []float64){ //通过 更新切片头部信息实现转换 var c []int aHdr := (*reflect.SliceHeader)(unsafe.Pointer(&a)) cHdr := (*reflect.SliceHeader)(unsafe.Pointer(&c)) *cHdr = *aHdr }
Go 是否针对数据等进行了大小限制,如果限制,那么最大是多少?
Go中非0大小数组的长度不得超过 2 GB, 需要针对数组元素的类型大小进行计算数组的最大范围 ([]uint8最大2GB,[]uint16最大1GB,以此类推,[]struct{} 数组长度可以超过2GB)
函数的几种声明方式
//匿名函数 var Add = func(a,b int) int { return a + b } func f(i, j, m int, s, k string)
Go中在使用递归时,是否存在栈溢出问题?
go 采用的是可变栈, 其每个线程分配的栈是可变的,在其他语言中大部分都是分配 64kb-2m 不等的栈大小。go 独特的采用动态栈采用,其会在调用过程中如果栈不够用,系统会动态调整,最大可达到 1~2GB
为什么go中采用的是值复制形式传递,但是针对数组和切片等只传递头信息,不会复制底层的数据,是否存在矛盾?
两者不会产生矛盾,数组切片等数据类型,并不是简单的数据类型,其内部包含的数据并不只有基本的存储数据,其还有自身需要的数据,比如数组的大小,数组的头信息等内容。值传递在这里体现的并不是真个数组的复制,而只是复制了其基本的数据头信息。而指针传递是传递的整个数组的指针。所以本质上还是有很大区别的
go中针对包初始化时有一个init函数是在main 函数执行前执行的,如果在 init 函数中启用 goroutine,那么这个goroutine 是如何执行的?
在main 函数没有执行前,go 程序会对包进行初始化, 初始化包中的变量内容,执行init 函数,这些操作都是在通一个 goroutine中,不过 如果 init 中使用
go
开启一个新的goroutine,新的 goroutine 并不会立即执行,而是会等待 main 函数执行后才会启动执行。go针对错误的处理方式?
一个良好的程序,永远不应该发生 panic 异常
针对错误的5种处理方式- 传播错误
子函数将错误信息传播给父函数,错误向上传递,将部分错误进行加工为有意义的后再向上传递 - 重新尝试失败操作
针对部分偶然性或者不可预知的错误,可以通过重试操作,不过要设置重试事件间隔和次数 - 输出信息结束程序
如果错误导致程序无法进行,可以输出错误信息,结束程序。一般只针对main 函数中的错误,如果是库函数,那么一般是将错误向上传递 - 只输出错误信息
通过log 包只输出错误信息或者标准输出流中输出错误信息log.Printf("xxxxxxxx", err) fmt.Fprintf(os.Stderr, "xxdnkagnkgna", err)
- 忽略错误信息
对错误不进行处理,直接忽略
- 传播错误
go 中可能引起 panic 的操作
- 针对 nil 函数值进行调用
是否了解匿名函数以及函数值?这两者在使用时有哪些要求?
go 中函数可以作为返回值或者参数; 函数拥有类型,和其他类型一样,函数的零值为 nil; 函数值可以和 nil 进行比较,但是两个函数之间是不能比较的; 并且不能作为map 的key 存在; 函数值是引用类型 ; go 采用闭包的形式实现函数值
匿名函数只能在包级语法声明; 在函数内部定义的匿名函数可以引用该函数的变量; 在函数内部定义匿名函数,不仅仅是一串代码,其还可以记录函数内部状态,可以操作函数内部的局部变量;
是否对可变参数了解?
go 支持可变参数
func sum(vals ...int){ }
可变参数本质是创建了一个数组,然后将参数复制到数组中,再讲数组的切片传递给函数(所以拥有可变参数的函数,实际传递的是一个切片)
那么可变参数本质是一个切片,那么针对原始数据就是一个切片时是如何处理?
针对原始数据已经是一个切片时,go 采用如下方式进行处理values := []int{1,2,4,5} sum(values...) //通过在操作时,穿如变量,并且在其后跟 ... 实现
针对可变参数和切片参数的函数的定义:
func f(...int){} func g([]int){}
虽然可变参数内部实现的是一个切片传递,但是和直接传递切片为参数的是不同的
go 中的 defer 了解吗? 如何使用? 常用的场景?
go 中的defer 语句代码会被延迟执行,其在退出函数体之前执行,并且其实先出现后执行。可以将 defer 的执行顺序理解为一个栈结构,先进后出;并且可以在一个函数中执行多个 defer语句
一般用defer 来关闭文件打开,以及网络连接等操作;获取函数执行时间;以及观察函数返回值如何获取堆栈信息?
func main(){ defer printStack() xxxxxx } func printStack(){ var buf [4096]byte n := runtime.Stack(buf[:], false) os.Stdout.Write(buf[:]) }
go 中的异常处理了解吗? 是如何实现的?
go 语言中并不是使用 try - catch 来实现异常处理的,使用
panic
和recover
结合来实现异常处理
一般程序遇到panic 异常,程序会立即中断执行,并且执行延迟函数以及输出日志信息
panic 是一个内置函数,直接调用此函数也会引发panic异常,此函数接收任意类型值参数
_尽量少使用 panic 机制,应该用错误机制代替 panic 机制revcoer 会使程序从 panic 中恢复,并返回 panic value. 导致 panic的函数不会继续执行,但能正常返回; 如果在未发生 panic时调用 recover, revocer 会返回nil
go中方法定义时如何实现的,有什么需要注意的地方?
方法和函数相同,只不过函数名前添加接收者就变为了方法
在结构体中如果字段名和方法名相同,将在调用中出错
定义类型方法必须和类型在同一个包中,并且每个方法名是唯一的,方法和函数不支持重载
go 可以给任意类型定义方法(除了指针和interface) go 函数会对每个参数进行拷贝,可以通过指针操作避免默认拷贝操作(默认值传递)//更新接收者的属性值 func (p * Point)ScaleBy(factor float64){ p.X *= factor }
一般一个结构体有Hige指针作为接收器的方法, 那么所有的方法都应该是指针接收器的(定义应该统一)
go 中方法与函数的区别?
方法与函数没有本质区别,都是对代码块的封装。在函数名前添加变量就是方法; 方法必须有一个载体,其添加的那个变量就是载体(接收器)
//函数 func Distance(p, q Point) float64 { } //方法 func (p Paint)Distance(p, q Point) float64{ }
go 如何实现方法到函数的转换?
通过方法表达式可以将方法转换为普通函数
var CloseFile = (*File).Close f, _ := OpenFile("foo.dat") CloseFile(f)