IT技术栈:Golang面试攻略详细总结,有的坑,原来真的可以躲过去
IT技术栈:Golang面试攻略详细总结,有的坑,原来真的可以躲过去
make与new的异同
相同点:
- 都是用来给变量分配内存的
不同点:
- new一般给值类型的变量,例如:string、int、arr分配内存,make给slice、channel、map等引用类型的变量分配内存
- 返回值的类型不一样,new返回指向这个变量的指针,make返回的是一个初始化后的引用类型。
package main
import "fmt"
func main() {
// 使用 new 创建一个整数的指针
var numPtr *int
numPtr = new(int)
*numPtr = 42
fmt.Println("Value of numPtr:", *numPtr) // 输出: Value of numPtr: 42
// 使用 make 创建一个切片
slice := make([]int, 3, 5) // 创建一个长度为3,容量为5的切片
slice[0] = 1
slice[1] = 2
slice[2] = 3
fmt.Println("Slice:", slice) // 输出: Slice: [1 2 3]
// 使用 make 创建一个映射
m := make(map[string]int)
m["apple"] = 5
m["banana"] = 3
fmt.Println("Map:", m) // 输出: Map: map[apple:5 banana:3]
// 使用 make 创建一个通道
ch := make(chan int)
go func() {
ch <- 42
}()
value := <-ch
fmt.Println("Channel value:", value) // 输出: Channel value: 42
}
数组与切片的异同
相同点:
- 只能存放相同类型的变量
- 都是通过下标来访问,并且有容量长度,长度通过 len 获取,容量通过 cap 获取
不同点:
- 数组的大小是固定的,定义时需要指定数组的长度,且无法更改。
- 切片的大小是可变的,可以根据需要动态增加或减少元素,切片是基于数组实现的。
- 数组是值类型,当将一个数组赋值给另一个变量或作为函数参数传递时,会复制整个数组的值,对一个副本的修改不会影响原始数组。
- 切片是引用类型,赋值或传递切片时,实际上传递的是底层数组的引用,多个切片可以共享同一个底层数组,对一个切片的修改会影响到其他共享底层数组的切片。
- 数组的长度是固定的,不能更改。
- 切片有长度和容量两个属性,长度表示当前切片中的元素数量,容量表示底层数组中可以容纳的元素数量。切片的容量可以在创建时指定,或者使用 append 函数来动态增加。
- 数组的零值是一个具有所有元素为零值的数组,例如,var arr [3]int 的零值是 [0 0 0]。
- 切片的零值是 nil,表示一个未分配底层数组的空切片。
package main
import "fmt"
func main() {
// 声明一个数组
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
// 打印数组
fmt.Println("Array:", arr) // 输出: Array: [1 2 3]
// 尝试修改数组的值
modifiedArr := arr
modifiedArr[0] = 100
fmt.Println("Modified Array:", modifiedArr) // 输出: Modified Array: [100 2 3]
fmt.Println("Original Array:", arr) // 输出: Original Array: [1 2 3](原始数组未受影响)
// 声明一个切片
slice := []int{1, 2, 3}
// 打印切片
fmt.Println("Slice:", slice) // 输出: Slice: [1 2 3]
// 尝试修改切片的值
modifiedSlice := slice
modifiedSlice[0] = 100
fmt.Println("Modified Slice:", modifiedSlice) // 输出: Modified Slice: [100 2 3]
fmt.Println("Original Slice:", slice) // 输出: Original Slice: [100 2 3](原始切片受到影响)
// 使用切片的 append 函数动态增加元素
slice = append(slice, 4, 5)
fmt.Println("Updated Slice:", slice) // 输出: Updated Slice: [100 2 3 4 5]
}
对切片或数组进行for range 的时候它的地址会发生变化么?
迭代变量的地址不会发生变化。每次迭代都会创建一个新的迭代变量,该变量的值是切片或数组中的元素,但地址不同。这是因为Go在每次迭代中会重新分配内存来存储迭代变量的副本。
package main
import "fmt"
func main() {
slice := []int{1, 2, 3}
for index, value := range slice {
fmt.Printf("Index: %d, Value: %d, Address: %p\n", index, value, &value)
}
}
for range 循环迭代了切片 slice,并打印了每个元素的索引、值以及值的地址。会发现,每次迭代中 value 的地址都不同,这表明每次迭代都创建了一个新的变量。
因此,在 for range 循环中,不要依赖迭代变量的地址来保持状态,因为它们会在每次迭代中重新分配。如果需要保持某个迭代变量的状态,可以将其复制到一个新的变量中。
Defer的原理
用于延迟执行函数调用。当使用 defer 时,它会将函数调用推迟到包含 defer 语句的函数即将返回之前执行。defer 常用于清理操作,如关闭文件、释放资源等,以确保在函数执行完毕时执行这些操作。
defer 的原理是通过一个栈(defer stack)来实现的,使用链表实现,将新的defer插入头节点,结束时,依次从头部取出。每次遇到 defer 语句,它会将要执行的函数及其参数入栈,但不会立即执行。当函数即将返回时,会按照后进先出(LIFO)的顺序执行栈中的 defer 函数调用。
package main
import "fmt"
func main() {
fmt.Println("Start")
defer fmt.Println("Deferred 1")
defer fmt.Println("Deferred 2")
defer fmt.Println("Deferred 3")
fmt.Println("End")
}
#Start
#End
#Deferred 3
#Deferred 2
#Deferred 1
rune类型
在Go语言中,rune是一种数据类型,用于表示Unicode字符。Unicode是一种字符编码标准,它包含了世界上几乎所有的字符,包括常见字符(如字母、数字、标点符号)以及各种特殊字符(如表情符号、非拉丁字母等)。
rune类型实际上是int32类型的别名,用于表示Unicode字符的整数值。每个rune代表一个字符,无论字符的编码有多大。这使得Go语言能够处理各种不同字符集的文本数据。
golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8。
package main
import "fmt"
func main() {
// 创建一个包含Unicode字符的字符串
str := "Hello, 世界!"
// 使用 for range 迭代字符串中的每个字符
for i, r := range str {
fmt.Printf("Character %d: %c (Unicode: %U)\n", i, r, r)
}
}
Character 0: H (Unicode: U+0048)
Character 1: e (Unicode: U+0065)
Character 2: l (Unicode: U+006C)
Character 3: l (Unicode: U+006C)
Character 4: o (Unicode: U+006F)
Character 5: , (Unicode: U+002C)
Character 6: (Unicode: U+0020)
Character 7: 世 (Unicode: U+4E16)
Character 10: 界 (Unicode: U+754C)
Character 13: ! (Unicode: U+FF01)
tag的实现原理
可以使用反射来解析结构体字段的标记(tag)。反射是Go语言的一种特性,它允许程序在运行时检查和操作变量、方法、结构体等程序结构信息。通过反射,可以获取结构体字段的标记信息,以及动态访问、修改这些字段的值。
要解析结构体字段的标记,需要使用reflect包,该包提供了一些函数和类型,用于处理反射操作。
package main
import (
"fmt"
"reflect"
)
// 定义一个结构体并添加标记
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address string `json:"address"`
}
func main() {
// 创建一个示例结构体
p := Person{
Name: "Alice",
Age: 30,
Address: "123 Main St",
}
// 获取结构体类型
t := reflect.TypeOf(p)
// 遍历结构体的字段
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// 获取字段名和标记值
fieldName := field.Name
tagValue := field.Tag.Get("json")
fmt.Printf("Field: %s, Tag: %s\n", fieldName, tagValue)
}
}
Field: Name, Tag: name
Field: Age, Tag: age
Field: Address, Tag: address
切片扩容
- 初始容量(Capacity): 当创建一个切片时,可以选择指定初始容量,例如:make([]T, length, capacity)。初始容量表示底层数组的大小,即切片可以容纳的元素数量,但长度(Length)为0。容量通常用于优化性能,以减少频繁扩容的开销。
- 添加元素: 当向切片添加元素时,Go语言会检查切片的长度(len(slice))和容量(cap(slice))。
- 检查容量是否足够: 如果切片的长度小于容量,说明底层数组还有足够的空间来容纳新元素,这时不需要扩容。
- 容量不足时扩容: 如果切片的长度等于容量,表示底层数组已满。这时,Go语言会执行以下操作:
- 创建一个新的底层数组,通常容量会增加一倍(但最小会增加到原始容量的两倍,以避免小容量的切片频繁扩容)。
- 将原始数据复制到新的底层数组中。
- 更新切片的引用,使其指向新的底层数组。
- 释放旧的底层数组(垃圾回收)。
- 继续添加元素: 现在,切片有了更大的容量,可以继续添加元素,重复上述步骤,直到容量再次不足。
这个扩容机制的好处是,开发者无需关心切片的容量,可以专注于操作切片的长度。这简化了代码,并且避免了手动管理内存分配和复制数据的繁琐工作。
关于select
select 是用于处理多个通道操作的控制结构,实现 I/O 多路复用机制,它允许等待多个通道中的任何一个可以操作(发送或接收),并执行相应的操作。select 通常用于解决并发编程中的问题,例如等待多个任务中的一个完成,或者处理多个输入源的数据。
- 等待多个通道的数据到达: 通过将多个通道操作放入 select 语句中,程序可以同时等待多个通道的数据到达,无需一个一个等待。
- 处理超时操作: select 可以与 time.After 结合使用,以在特定时间内等待某个通道操作完成或处理超时操作。
- 实现非阻塞操作: select 可以在多个通道都没有数据可用时,执行默认操作,从而实现非阻塞的操作。
- 监听多个网络连接: 通过将多个 net.Conn 对象的读取操作放入 select,可以同时监听多个客户端连接,响应它们的请求。
package main
import (
"fmt"
"time"
)
func main() {
// 创建两个通道
ch1 := make(chan string)
ch2 := make(chan string)
// 启动两个并发的 goroutine,分别向通道发送数据
go func() {
time.Sleep(2 * time.Second)
ch1 <- "Hello from goroutine 1"
}()
go func() {
time.Sleep(1 * time.Second)
ch2 <- "Hello from goroutine 2"
}()
// 使用 select 来等待多个通道操作
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout: No data received")
}
// 关闭通道
close(ch1)
close(ch2)
}
怎么处理对 map 进行并发访问
处理并发访问map时需要注意,因为map不是线程安全的,多个goroutine同时对同一个map进行读写操作可能会导致数据竞态问题。为了安全地并发访问map,可以采用以下几种方式:
- 使用互斥锁(Mutex): 使用sync包中的Mutex来保护map,确保在任何时刻只有一个goroutine可以对map进行读写操作。
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
m := make(map[int]int)
// 启动多个goroutine并发写入map
for i := 0; i < 5; i++ {
go func(i int) {
mu.Lock()
defer mu.Unlock()
m[i] = i * 2
}(i)
}
// 等待所有goroutine完成
for i := 0; i < 5; i++ {
go func(i int) {
mu.Lock()
defer mu.Unlock()
fmt.Println(m[i])
}(i)
}
}
- 使用读写锁(RWMutex): 如果大多数操作是读取操作,而写入操作较少,可以使用sync包中的RWMutex,它允许多个goroutine同时读取map,但写入操作仍然需要互斥。
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.RWMutex
m := make(map[int]int)
// 启动多个goroutine并发写入map
for i := 0; i < 5; i++ {
go func(i int) {
mu.Lock()
defer mu.Unlock()
m[i] = i * 2
}(i)
}
// 启动多个goroutine并发读取map
for i := 0; i < 5; i++ {
go func(i int) {
mu.RLock()
defer mu.RUnlock()
fmt.Println(m[i])
}(i)
}
}
- 使用并发安全的sync.Map: Go 1.9及以上版本引入了sync.Map,它是一种并发安全的映射,可以直接在多个goroutine中进行并发读写操作。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// 启动多个goroutine并发写入map
for i := 0; i < 5; i++ {
go func(i int) {
m.Store(i, i*2)
}(i)
}
// 启动多个goroutine并发读取map
for i := 0; i < 5; i++ {
go func(i int) {
if value, ok := m.Load(i); ok {
fmt.Println(value)
}
}(i)
}
}
context的使用
context 是Go语言标准库中的一个包,用于在多个goroutine之间传递上下文信息和取消信号。它的设计旨在解决在并发环境中管理请求范围的值、控制goroutine的生命周期以及处理取消请求的问题。context 在处理HTTP请求、数据库查询、RPC等场景中非常有用。
原理
context 的核心概念是创建一个上下文(context)对象,它包含了一个取消通道(Done)、截止时间(Deadline)、上下文值(Value)等信息。当需要在多个goroutine之间传递上下文信息或取消请求时,可以将这个上下文对象传递给相关的goroutine,从而实现跨goroutine的信息传递和控制。
使用场景
控制goroutine的生命周期: context 可以用于在父goroutine中控制子goroutine的生命周期。当父goroutine取消上下文时,所有从该上下文派生的子goroutine都会收到取消信号并退出。
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Worker: Context canceled")
return
default:
fmt.Println("Worker: Working...")
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
time.Sleep(3 * time.Second)
cancel() // 取消上下文,停止worker
time.Sleep(1 * time.Second)
}
传递请求范围的值: context 可以用于在多个goroutine之间传递请求范围的值,如请求ID、用户信息等。这些值可以在整个请求范围内传递,而不需要在每个函数参数中传递。
package main
import (
"context"
"fmt"
)
func logRequestID(ctx context.Context) {
if reqID, ok := ctx.Value("requestID").(string); ok {
fmt.Println("Request ID:", reqID)
} else {
fmt.Println("Request ID not found")
}
}
func main() {
ctx := context.WithValue(context.Background(), "requestID", "12345")
logRequestID(ctx)
}
处理超时和取消: context 可以用于设置超时和处理取消请求。通过设置截止时间,可以确保某个操作在指定的时间内完成,否则会自动取消。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("Operation completed")
case <-ctx.Done():
fmt.Println("Operation canceled or timed out")
}
}
context 是Go语言中处理并发操作的强大工具,可以用于控制goroutine的生命周期、传递请求范围的值以及处理超时和取消请求。根据具体的应用场景,可以使用不同的context类型,如context.Background()、context.WithCancel()、context.WithTimeout()等。这样可以确保Go程序在并发操作中更加健壮和可控。
channel底层原理
IT技术栈:程序员面试宝典之Golang的Channel(管道)使用与原理
GMP相关
GC相关
多返回值是如何实现的
- 栈帧: 在函数调用时,Go语言会为每个函数创建一个栈帧。栈帧是一个用于存储函数的局部变量、参数、返回值等信息的内存区域。每次函数调用都会创建一个新的栈帧,并将其压入调用栈。
- 返回值传递: 当一个函数需要返回多个值时,Go语言会将这些返回值按顺序依次存储在当前函数的栈帧中,通常是在栈帧的顶部。
- 调用方读取返回值: 调用方函数可以读取被调用函数的返回值,这是通过访问被调用函数的栈帧来完成的。根据返回值的数量和类型,调用方函数从栈帧中读取返回值,并将其用于后续操作。
package main
import "fmt"
func multiReturn() (int, string) {
return 42, "Hello, World!"
}
func main() {
// 调用 multiReturn 函数并获取返回值
result1, result2 := multiReturn()
// 处理返回值
fmt.Println("Result 1:", result1)
fmt.Println("Result 2:", result2)
}
在这个示例中,multiReturn 函数返回两个值:一个整数和一个字符串。当 multiReturn 被调用时,这两个返回值按顺序存储在栈帧中。然后,调用方函数 main 通过多重赋值操作从栈帧中读取这两个返回值,并进行后续的处理。
总的来说,Go语言的多返回值原理是基于栈帧的机制,它允许函数返回多个值,并由调用方函数负责读取和处理这些返回值。这种机制使得Go语言可以方便地返回多个相关的值,例如错误信息和结果,而不需要使用额外的数据结构来传递。