golang常见面试题-基础篇
文章目录
- 常见面试题
- go基础
- go数据类型
- Go nil切片和空切片的区别
- go为什么没有枚举类型
- 什么是协程goroutine?
- Golang检查变量类型
- strings.Builder
- go switch
- go 空接口 interface{}
- go函数返回值,多值返回情况
- go切片前、后、中间添加元素
- i++ 和 i--
- golang中的三个点 '...' 的用法
- Go语言中函数的参数传递方式
- golang 如何利用多核的
- go——协程调度 GMP模型
- Go 当中同步锁有什么特点?作用是什么
- Go 语言当中 Channel(通道)有什么特点,需要注意什么?
- Go 语言当中 Channel 缓冲有什么特点?Go channel的底层实现原理?
- go 切片,cap和len的区别
- Go 语言当中 new 的作用是什么?
- Go 语言中 make 的作用是什么?
- Go语言中数组和slice的区别
- Go 语言当中值传递和地址传递(引用传递)如何运用?
- defer
- for range循环迭代切片:range 创建了每个元素的副本,而不是直接返回对该元素的引用
- select什么情况走default分支
- go多重赋值之不借助中间变量,交换2个变量数据。
- 为什么Go语言不支持重载?
常见面试题
go基础
go数据类型
值类型:
- 字符串类型
- 数值类型
- 布尔类型
引用类型:
- 切片(slice)
- 信道(channel)
- 接口(interface)
- 函数(func)
- 映射(map)
Go nil切片和空切片的区别
在Go语言中,nil 切片和空切片是两种不同的空状态,它们之间具有一些重要的区别。
nil 切片
当我们声明一个切片类型的变量但并不进行初始化时,在没有任何赋值的情况下,它的默认值就是 nil。在逻辑上,你可以将 nil 切片视为一个不存在的切片。
var s []int // s 现在是 nil
对 nil 切片进行 len() 和 cap() 操作,结果都将返回 0。你也可以将 nil 切片添加到任何切片的末尾,这等同于什么都没做。但是,与空切片相比,nil 切片在做 append() 操作时有一个重要的区别,那就是当 nil 切片通过 append() 函数追加元素后,它就会产生一个长度和容量都为1的新切片。
空切片
空切片是已经分配了内存空间但是并没有包含任何元素的切片。我们可以通过 make() 函数或者字面量方式像这样创建一个空切片:
s1 := []int{} // s1 是一个空切片,通过字面量创建
s2 := make([]int, 0) // s2 也是一个空切片,通过 make 创建
空切片的 len() 和 cap() 都返回 0,这同 nil 切片相同。你也可以像 nil 切片一样处理空切片。但是一个被分配了内存的空切片与 nil 切片在底层实现上是有区别的。特别的是,对于一个空切片,即使紧接着进行 append() 操作,其地址也不会发生改变,因为切片在创建时就已经预分配了一定的内存空间。
go为什么没有枚举类型
参考URL: https://blog.csdn.net/a1014981613/article/details/130243171
枚举类型的值本质上是常量,因此我们可以使用 Go 语言中的常量来实现类似枚举类型的功能,例如:
type WeekDay int
const (
Sunday WeekDay = 1
Tuesday WeekDay = 2
Wednesday WeekDay = 3
Thursday WeekDay = 4
Friday WeekDay = 5
Saturday WeekDay = 6
Monday WeekDay = 7
)
通过前面的例子不难发现,当我们需要定义多个枚举值时,手动指定每个枚举常量的值会变得十分麻烦。为了解决这个问题,我们可以使用 iota 常量生成器,它可以帮助我们生成连续的整数值。
type WeekDay int
const (
Sunday WeekDay = iota
Tuesday
Wednesday
Thursday
Friday
Saturday
Monday
)
为了能让我们实现的 “枚举类型” 更加具备枚举类型的特征,我们可以为其添加类似 Java 等其他语言中的枚举方法。
Name()
返回枚举值的名称。
// Name 返回枚举值的名称
func (w WeekDay) Name() string {
if w < Sunday || w > Monday {
return "Unknown"
}
return [...]string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}[w]
}
String()
实现 String 方法,用于打印输出。
// 将枚举值转成字符串,便于输出
func (w WeekDay) String() string {
return w.Name()
}
什么是协程goroutine?
参考本人博客链接
Golang检查变量类型
Go提供几种方法检查变量的类型:
- 在字符串格式化标识
%T
fmt.Printf("variable count=%v is of type %T \n", count, count)
- 反射方式:reflect.TypeOf, reflect.ValueOf.Kind
使用reflect
包的TypeOf
和ValueOf().Kind
函数
var days int = 42
fmt.Printf("variable days=%v is of type %v \n", days, reflect.TypeOf(days))
- 使用类型断言switch case方式
func typeofObject(variable interface{}) string {
switch variable.(type) {
case int:
return "int"
case float32:
return "float32"
case bool:
return "boolean"
case string:
return "string"
default:
return "unknown"
}
}
fmt.Println("Using type assertions")
fmt.Println(typeofObject(count))
fmt.Println(typeofObject(message))
fmt.Println(typeofObject(isCheck))
fmt.Println(typeofObject(amount))
这种方法的优点是可以对类型进行分组,例如,我们可以将所有int32、int64、uint32、uint64类型标识为“int”。
//OUTPUT
Using type assertions
int
string
boolean
float64
strings.Builder
strings.Builder 是 Go 语言中用于高效地构建字符串的类型。它提供了一组方法,允许我们按顺序逐步构建字符串,而无需进行频繁的字符串拼接操作。
strings.Builder 是 Go 语言标准库中的一个类型,用于高效地构建字符串。它提供了一种性能更好的方式来拼接字符串,相比传统的字符串拼接方法(如使用 + 运算符)。
主要优点:
- 内存分配更高效
- 避免频繁的字符串拷贝
- 减少临时字符串的创建
- 性能更好,尤其是在需要拼接大量字符串时
// 使用 + 拼接(低效)
func concatenateWithPlus(n int) string {
result := ""
for i := 0; i < n; i++ {
result += strconv.Itoa(i)
}
return result
}
// 使用 strings.Builder(高效)
func concatenateWithBuilder(n int) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString(strconv.Itoa(i))
}
return builder.String()
}
注意:strings.Builder 是一个类型,而不是一个表达式,因此不能直接赋值给变量。正确的做法是使用 strings.Builder{} 来创建一个 strings.Builder 对象,然后将其赋值给 result 变量。
主要方法
// 写入字符串
builder.WriteString("text")
// 写入字节
builder.Write([]byte("bytes"))
// 写入单个字符
builder.WriteByte('a')
// 写入 rune
builder.WriteRune('国')
// 获取当前长度
length := builder.Len()
// 重置 Builder
builder.Reset()
// 转换为字符串
str := builder.String()
WriteByte 是 strings.Builder 类型的一个方法,用于向构建器中追加一个字节(byte)。
var builder strings.Builder
builder.WriteByte('H')
builder.WriteByte('e')
builder.WriteByte('l')
builder.WriteByte('l')
builder.WriteByte('o')
result := builder.String()
fmt.Println(result) // 输出:Hello
在这个例子中,我们通过多次调用 WriteByte
方法,将字母逐个追加到 strings.Builder
对象中。最后,我们使用 String 方法将构建器对象转换为字符串,并打印输出结果。
go switch
注意一下go中switch和java中的swtich语法上的区别:
- go 中
switch
条件判断表达式部分不能加括号。 - 默认case自带break语句(当然也可以显式给出break)。
分支逻辑自动结束:当一个case分支匹配成功后,Go会自动结束switch语句,不需要显式地使用break来跳出。
支持多值匹配:一个case可以同时匹配多个值,这些值使用逗号分隔。
go 空接口 interface{}
参考本人文章: golang语言 []interface{}和interface{}
go函数返回值,多值返回情况
在函数有多个返回值时:
- 只要有⼀个返回值有命名,其他的也必须命名。
- 如果有多个返回值必须加上括号();
- 如果只有⼀个返回值且命名也需要加上括号()。
- 命名返回值可以在函数体中直接赋值,并可以通过 return 直接返回
go切片前、后、中间添加元素
append 函数的基本语法如下:
newSlice := append(slice, element1, element2, ...)
slice:要追加元素的切片。
element1, element2, …:要追加到切片的元素。
append 函数可以追加一个或多个元素到切片中。如果追加的元素超过了切片的容量,append 函数会自动分配更大的底层数组来容纳新的元素。
关于 append 函数的重要特点和注意事项:
- append 函数返回一个新的切片,而不会修改原始的切片。因此,通常需要将返回的新切片赋值给原始切片。
- 如果原始切片的容量足够,append 函数将直接向原始切片追加元素。这种情况下,新切片和原始切片将共享相同的底层数组。
- 如果原始切片的容量不足,append 函数将创建一个新的底层数组,并将原始切片的元素复制到新数组中,然后再追加新的元素。这种情况下,新切片将使用新的底层数组。
- 当使用 append 函数追加切片到另一个切片时,可以使用 … 语法将切片打散为单独的元素。例如:newSlice := append(slice1, slice2…)。
- append 函数还可以用于创建一个新的切片,而不需要给定初始切片。例如:
newSlice := append([]int{}, 1, 2, 3)
。
后添加:
/ 初始化切片
s := []int{1,2,3}
// 通过append函数向切片中追加数据
s = append(s,5,6,7)
fmt.Println(s)
中间添加:
通过copy + append 实现,通过 copy和append组合 可以避免创建中间的临时切片
删除切片元素
Go语言中并没有删除切片元素的专用方法,可以使用切片本身的特性来删除元素:
func main() {
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
}
总结:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]…)
i++ 和 i–
i++
和 i--
在 Go 语⾔中是语句
,不是表达式,因此不能赋值给另外的变量。此外没有 ++i
和 --i
golang中的三个点 ‘…’ 的用法
‘…’ 其实是go的一种语法糖。
它的第一个用法主要是用于函数有多个不定参数的情况,可以接受多个不确定数量的参数。
第二个用法是slice可以被打散进行传递。
var strss= []string{
"qwr",
"234",
"yui",
}
var strss2= []string{
"qqq",
"aaa",
"zzz",
"zzz",
}
strss=append(strss,strss2...) //strss2的元素被打散一个个append进strss
fmt.Println(strss)
Go语言中函数的参数传递方式
通过数组名传递参数方式并不能修改原数组,而通过切片方式传递能完成数组修改,这是因为,和其他语言不同,go语言在将数组名作为函数参数的时候,参数传递即是对数组的复制。在形参中对数组元素的修改都不会影响到数组元素原来的值。 而在使用slice作为函数参数时,进行参数传递将是一个地址拷贝,即将底层数组的内存地址复制给参数slice。这时,对slice元素的操作就是对底层数组元素的操作。采用指针方式进行传递就是传递的引用,对这个引用指向的地址的内容进行修改也会影响原数组的值。
**总结:在Go语言中函数的参数有两种传递方式,按值传递和按引用传递。Go默认使用按值传递来传递参数,也就是传递参数的副本。**在函数中对副本的值进行更改操作时,不会影响到原来的变量。按引用传递其实也可以称作"按值传递",只不过该副本是一个地址的拷贝,通过它可以修改这个值所指向的地址上的值。
Go语言中,在函数调用时,引用类型(slice、map、interface、channel)都默认使用引用传递。
golang 如何利用多核的
golang默认使用单核单线程,可以通过调整或设置运行参数设置多核多线程支持
runtime.GOMAXPROCS(int)
runtime.GOMAXPROCS(runtime.NumCPU())
直接设置环境变量$GOMAXPROCS
Go从1.5版本开始,默认采用多核执行,默认是你的CPU核心数,以前版本默认为1
go——协程调度 GMP模型
go——协程调度
参考URL: https://blog.csdn.net/qq_52563729/article/details/126112513
Go 当中同步锁有什么特点?作用是什么
同步锁的特点:
1.Mutex:
当一个Goroutine(协程)获得了Mutex后,其他Gorouline(协程)就只能进入等待之中,除非该gorouline释放了该Mutex。
2.RWMutex(读写锁):
RWMutex在读锁占用的情况下,会阻止写,但不阻止读;RWMutex在写锁占用情况下,会阻止任何其他goroutine(读和写)进来,整个锁相当于由该goroutine(协程)独占。
同步锁的作用:
保证资源在使用时的独有性,不会因为并发而导致数据错乱,保证系统并发时的稳定性。
Go 语言当中 Channel(通道)有什么特点,需要注意什么?
如果给一个 nil 的 channel 发送数据,会造成永远阻塞。
如果从一个 nil 的 channel 中接收数据,也会造成永久阻塞。
给一个已经关闭的 channel 发送数据, 会引起 panic
从一个已经关闭的 channel 接收数据, 如果缓冲区中为空,则返回一个零
值
Go 语言当中 Channel 缓冲有什么特点?Go channel的底层实现原理?
无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的。
go 切片,cap和len的区别
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
我们在进行切片赋值、传参、截断时,其实是复制了一个slice结构体,只不过底层的数组是同一个。这就导致无论是在复制的切片中修改值,还是在修改形参切片的值,都会影响到原来的切片。
切片拥有长度和容量。
切片的长度是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。
package main
import "fmt"
func main() {
test1 := make([]int,10,20)
fmt.Printf("len is %d\n",len(test1))
fmt.Printf("cap is %d", cap(test1))
}
没有指定cap的大小的时候len和cap是相等的,定义cap后,len和cap的值就不相等了。但是cap定义的值要大于len的值,否则就会报错。
默认情况下切片cap值小于1024的时候是成倍数增长的。比如有1变为2在变为4……但是超过1024后就变为原切片的1.25倍。
Go 语言中 cap 函数可以作用于的类型有:
⚫ array(数组)
⚫ slice(切片)
⚫ channel(通道)
Go 语言当中 new 的作用是什么?
在 Go 语言中,new 函数用于动态地分配内存,返回一个指向新分配的零值的指针。它的语法如下:
func new(Type) *Type
所谓零值,是指 Go 语言中变量在声明时自动赋予的默认值。对于基本类型来说,它们的零值如下:
- 布尔型:false
- 整型:0
- 浮点型:0.0
- 复数型:0 + 0i
- 字符串:“”(空字符串)
- 指针:nil
- 接口:nil
- 切片、映射和通道:nil
package main
import (
"fmt"
)
func main() {
var p *int
p = new(int)
fmt.Println(*p) //0
}
new关键字是用来分配内存的函数,new(Type)作用是为T类型分配并清零一块内存,并将这块内存地址作为结果返回。也就是说new(T)会为类型为T的新项分配已置零的内存空间,并返回它的地址。
在go中,new返回一个指针,指针指向新分配的内存,类型为T类型的零值。
new的使用场景
- new用来初始化结构体,返回结构体类型指针。
package main
type User struct {
user string
password string
}
func main() {
//第一种方式
u1 := new(User)//创建User类型的指针
u1.user = "root"
u1.password = "passwd"
//第二种方式
u2 := &User{}
u2.user = "root"
u2.password = "passwd"
//第一种方法等效于第二种方式
}
Go 语言中 make 的作用是什么?
make 的作用是为 slice, map or chan 的初始化 然后返回引用。
make(T, args)函数的目的和 new(T)不同 仅仅用于创建 slice, map, channel
而且返回类型是实例。
Slice:
如果传入两个参数,第二个参数 size 指定了它的长度,它的容量和长度相同,比如make([]int,5)。
如果传入三个参数,第三个参数来指定不同的容量值,但必须不能比长度值小,比如 make([]int, 0, 10)。
//创建一个初始元素个数为5的数组切片,元素初始值为0
a := make([]int, 5) // len(a)=5
//创建一个初始元素个数为5的数组切片,元素初始值为0,容量为10
b := make([]int, 5, 10) // len(b)=5, cap(b)=10
Go语言中数组和slice的区别
Go语言中数组是具有固定长度而且拥有零个或者多个相同或相同数据类型元素的序列。由于数组长度固定,所以在Go语言比较少直接使用。而slice长度可增可减,使用场合比较多。
更深入的区别在于:数组在使用的过程中都是值传递,将一个数组赋值给一个新变量或作为方法参数传递时,是将源数组在内存中完全复制了一份,而不是引用源数组在内存中的地址。
其实不只是数组,go语言中的大多数类型在函数中当作参数传递都是值语义的。也就是任何值语义的一种类型当作参数传递到调用的函数中,都会经过一次内容的copy,从一个方法栈中copy到另一个方法栈。
数组和slice长的很像,操作方式也都差不多,并且slice包含了数组的基本的操作方式,如下标、range循环,还有一些如len()则是多种类型共用,所以根据操作根本搞不清数组和切片的区别,能够看出区别的地方主要看如何声明的。
slice表示一个拥有相同类型元素的可变长度序列。slice通常被写为[]T,其中元素的类型都是T;它看上去就像没有长度的数组类型。
slice初始化的方式:
s1 := []int{1, 2, 3} //注意与数组初始化的区别,在内存中构建一个包括有3个元素的数组,然后将这个数组的应用赋值给s这个Slice
a := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0} //a是数组
s2 := a[2:8] //从数组中切片构建Slice
s3 := make([]int, 10, 20) //make函数初始化,len=10,cap=20
数组的声明方式很单一,通常就是下面这样:
array1 := [5]int{1, 2, 3, 4, 5}
array2 := [5]int{}
Go 语言当中数组和切片在传递的时候的区别是什么?
- 数组是值传递
- 切片是引用传递
Go 语言当中值传递和地址传递(引用传递)如何运用?
- 值传递只会把参数的值复制一份放进对应的函数,两个变量的地址不同,
不可相互修改。 - 地址传递(引用传递)会将变量本身传入对应的函数,在函数中可以对该变
量进行值内容的修改。
defer
参考本人文章:
golang的defer的理解- defer的函数一定会执行吗?
for range循环迭代切片:range 创建了每个元素的副本,而不是直接返回对该元素的引用
https://docs.oldtimes.me/c.biancheng.net/view/4118.html
range 创建了每个元素的副本,而不是直接返回对该元素的引用。
select什么情况走default分支
select 语句会根据已经准备好的 case 来选择一个分支执行,如果多个 case 同时满足条件,那么 Go 的运行时会在这些 case 中随机选择一个执行。因此,如果想要在没有其他 case 满足条件时执行特定操作,可以使用 default 分支。
select 语句不会阻塞等待,除非所有的 case 中都没有准备好的操作。select 会立即执行,检查每个 case 是否满足条件,然后根据条件执行相应的分支。如果所有的 case 都没有准备好,且没有 default 分支,那么 select 语句会阻塞等待,直到至少有一个 case 准备好。
如果没有 default 分支,而且没有任何 case 准备好,select 就会阻塞。但是通常在使用 select 时,至少会有一个 case 准备好执行操作,这使得 select 非阻塞。
总结:
有case,有 default情况:
- 如果能匹配到case 就 执行 case
- 匹配不到case,就执行default
- 有 default,就代表了不会阻塞
有case,无default:
- 会阻塞 一直等到case匹配上
以下是一些可能会导致 select 语句走到 default 分支的情况:
- 所有通道都没有准备好:如果所有的 case 表达式中的通道都没有数据可读取,也没有通道可以写入数据,那么 select 语句会走到 default 分支。
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
select {
case data := <-ch:
fmt.Println(data)
default:
fmt.Println("default")
}
}
-
没有指定超时:如果没有在任何一个 case 中使用 time.After 或者其他超时机制,而且其他 case 中的通道都没有准备好,那么 select 语句也会走到 default 分支。
-
通道缓冲区已满:如果向某个通道写入数据,但通道的缓冲区已满,导致写入操作阻塞,那么 select 语句可能会走到 default 分支。
go多重赋值之不借助中间变量,交换2个变量数据。
在Go语言中,可以使用多重赋值的方式来交换两个变量的值,而不需要借助第三方变量。
str1, str2 = str2, str1
str1, str2 = str2, str1
这一行代码实际上是将str2的值赋给str1,同时将str1的值赋给str2,完成了两个变量值的交换。
这种多重赋值的方式是Go语言中的一个特性,它允许同时对多个变量进行赋值。在赋值时,Go语言会先计算等号右侧的表达式,然后按照从左至右的顺序依次将结果赋给等号左侧的变量。所以,在这个例子中,首先会计算str2, str1这个表达式,然后将str2的值赋给str1,再将str1的值赋给str2,实现了两个变量值的交换。
这种多重赋值的方式简洁而高效,避免了使用第三方变量的额外开销,是Go语言中常用的一个技巧。
在 Go 语言中,多重赋值的操作是同时进行的,而不是按照从左至右的顺序逐个赋值。这意味着在进行多重赋值时,右侧表达式的计算会在任何变量的赋值之前完成,然后将结果同时赋给左侧的变量。
这种多重赋值的特性使得交换两个变量的值变得简洁而高效。它可以直接在不使用第三方变量的情况下交换两个变量的值
为什么Go语言不支持重载?
什么是多态?多态意味着‘同名的多种形式’。方法的功能在不同的场景中表现不同。
我们可以对比Java/C++这些语言,它们是支持重载的。这些语言支持运行时多态和编译时多态:
1, 运行时多态:运行时解析对重写方法的函数调用。
- 重写方法(Method Overriding): 比如当派生子类重写了基类的某个成员函数时。
2, 编译时多态:编译器扫描代码时能够知道特定的call将执行哪些确定的函数。
- 方法重载(Method Overloading): 当有多个相同名称但不同参数的函数时。
Golang只支持运行时多态(使用接口实现),对于编译时多态并不支持。Golang对方法的调度并不需要进行类型匹配,而只是按照函数名称匹配,它是简化了Golang的设计。
Go不支持重载的这个问题困扰了很多从面向对象语言转到Go的开发者。
官方解答:
Experience with other languages told us that having a variety of methods with the same name but different signatures was occasionally useful but that it could also be confusing and fragile in practice.
官方有明确提到两个观点:
- 函数重载:拥有各种同名但不同签名的方法有时是很有用的,但在实践中也可能是混乱和脆弱的。
- 参数默认值:操作符重载,似乎更像是一种便利,不是绝对的要求。没有它,程序会更简单。
Go语言的设计者之所以没有在Go中实现方法的重载,并没有复杂的理由,核心原则就是:让Go保持足够的简单。
其实这和设计理念,和对程序的理解有关系。说白了,就是你喜欢 “显式”,还是 “隐喻”。Go 语言的设计理念就是 “显式大于隐喻”,追求明确,显式。
函数重载和参数默认值,其实是不好的行为。调用者只看函数名字,可能没法知道,你这个默认值,又或是入参不同,会调用的东西,会产生怎么样的后果?
当然如果非要较真的话,我们或许可以在Go中声明方法的时候将参数写成interface{} 或者 … 切片的方式。在传进来参数的时候做一步校验,判断参数的类型和个数,然后分别处理之。