IT技术栈:Golang面试攻略详细总结,有的坑,原来真的可以躲过去

IT技术栈:Golang面试攻略详细总结,有的坑,原来真的可以躲过去

首页角色扮演大侠行不行更新时间:2024-05-07
make与new的异同

相同点:

不同点:

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 }数组与切片的异同

相同点:

不同点:

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 1rune类型

在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切片扩容

  1. 初始容量(Capacity): 当创建一个切片时,可以选择指定初始容量,例如:make([]T, length, capacity)。初始容量表示底层数组的大小,即切片可以容纳的元素数量,但长度(Length)为0。容量通常用于优化性能,以减少频繁扩容的开销。
  2. 添加元素: 当向切片添加元素时,Go语言会检查切片的长度(len(slice))和容量(cap(slice))。
  3. 检查容量是否足够: 如果切片的长度小于容量,说明底层数组还有足够的空间来容纳新元素,这时不需要扩容。
  4. 容量不足时扩容: 如果切片的长度等于容量,表示底层数组已满。这时,Go语言会执行以下操作:
  5. 创建一个新的底层数组,通常容量会增加一倍(但最小会增加到原始容量的两倍,以避免小容量的切片频繁扩容)。
  6. 将原始数据复制到新的底层数组中。
  7. 更新切片的引用,使其指向新的底层数组。
  8. 释放旧的底层数组(垃圾回收)。
  9. 继续添加元素: 现在,切片有了更大的容量,可以继续添加元素,重复上述步骤,直到容量再次不足。

这个扩容机制的好处是,开发者无需关心切片的容量,可以专注于操作切片的长度。这简化了代码,并且避免了手动管理内存分配和复制数据的繁琐工作。

关于select

select 是用于处理多个通道操作的控制结构,实现 I/O 多路复用机制,它允许等待多个通道中的任何一个可以操作(发送或接收),并执行相应的操作。select 通常用于解决并发编程中的问题,例如等待多个任务中的一个完成,或者处理多个输入源的数据。

  1. 等待多个通道的数据到达: 通过将多个通道操作放入 select 语句中,程序可以同时等待多个通道的数据到达,无需一个一个等待。
  2. 处理超时操作: select 可以与 time.After 结合使用,以在特定时间内等待某个通道操作完成或处理超时操作。
  3. 实现非阻塞操作: select 可以在多个通道都没有数据可用时,执行默认操作,从而实现非阻塞的操作。
  4. 监听多个网络连接: 通过将多个 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,可以采用以下几种方式:

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) } }

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) } }

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底层原理GMP相关GC相关多返回值是如何实现的
  1. 栈帧: 在函数调用时,Go语言会为每个函数创建一个栈帧。栈帧是一个用于存储函数的局部变量、参数、返回值等信息的内存区域。每次函数调用都会创建一个新的栈帧,并将其压入调用栈。
  2. 返回值传递: 当一个函数需要返回多个值时,Go语言会将这些返回值按顺序依次存储在当前函数的栈帧中,通常是在栈帧的顶部。
  3. 调用方读取返回值: 调用方函数可以读取被调用函数的返回值,这是通过访问被调用函数的栈帧来完成的。根据返回值的数量和类型,调用方函数从栈帧中读取返回值,并将其用于后续操作。

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语言可以方便地返回多个相关的值,例如错误信息和结果,而不需要使用额外的数据结构来传递。

【申明:文章部分图片来源于网路,侵权,删除】

查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved