1. Go基础
1.1 核心知识点
数据类型
- 基本类型:bool, string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128
- 复合类型:array, slice, map, struct, pointer, function, interface, channel
- 零值:每种类型都有默认的零值
变量声明
var name string = "hello"
var age int = 25
var isActive bool = true
短变量声明(只能在函数内使用):
name := "hello"
age := 25数组与切片
数组:固定长度
var arr [5]int
arr := [5]int{1, 2, 3, 4, 5}
切片:动态长度
var slice []int
slice := []int{1, 2, 3, 4, 5}
slice := make([]int, 5, 10) // len=5, cap=10Map
m := make(map[string]int)
m["key1"] = 1
value, ok := m["key1"] // 检查key是否存在
delete(m, "key1")函数
func add(a, b int) int {
return a + b
}
多返回值:
func divmod(a, b int) (int, int) {
return a / b, a % b
}
可变参数:
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
闭包:
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}错误处理
if err != nil {
return err
}
自定义错误:
type MyError struct {
Msg string
}
func (e *MyError) Error() string {
return e.Msg
}
panic/recover:
func recoverFromPanic() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
panic("something went wrong")
}1.2 常见面试题
Q1: Go的变量声明有哪些方式?
解题思路:
- 列举Go中变量声明的几种方式
- 说明每种方式的适用场景
- 对比不同方式的优缺点
代码实现:
package main
import "fmt"
func main() {
var name string = "方式1:完整声明" // 完整声明:显式指定类型 string
var age = 25 // var声明省略类型:Go自动推断为 int
var isActive bool = true // 完整声明:显式指定类型 bool
city := "方式2:短变量声明" // 短变量声明:只能在函数内使用,自动推断类型
var ( // 批量声明:可以省略类型,Go自动推断
country = "China" // 自动推断为 string
province = "Beijing" // 自动推断为 string
)
fmt.Println(name)
fmt.Println(age)
fmt.Println(isActive)
fmt.Println(city)
fmt.Println(country, province)
}常见错误分析:
- 错误1:在函数外使用短变量声明
:= - 错误2:声明变量但不使用(Go不允许未使用的变量)
- 错误3:重复声明变量
Q2: 数组和切片的区别是什么?
解题思路:
- 解释数组的基本特性(固定长度、值类型)
- 解释切片的基本特性(动态长度、引用类型)
- 说明切片的底层实现(指向数组的指针、长度、容量)
- 演示切片的操作(append、copy、切片表达式)
代码实现:
package main
import "fmt"
func main() {
// 数组:固定长度、值类型、内存连续
// 声明一个长度为5的数组,包含元素 [1, 2, 3, 4, 5]
arr := [5]int{1, 2, 3, 4, 5}
// 切片:动态长度、引用类型、底层指向数组
// 切片结构:{指向数组的指针, 长度len, 容量cap}
// arr[1:3] 表示从索引1开始到索引3(不包含3),即 [2, 3]
// slice 指向 arr[1] 和 arr[2],容量从索引1到数组末尾,即 cap=4
slice := arr[1:3]
fmt.Printf("Array: %v, len=%d, cap=%d\n", arr, len(arr), cap(arr))
fmt.Printf("Slice: %v, len=%d, cap=%d\n", slice, len(slice), cap(slice))
// 修改切片元素:因为切片是引用类型,修改会影响底层数组
// slice[0] 实际上修改的是 arr[1]
slice[0] = 10
fmt.Printf("After modification - Array: %v, Slice: %v\n", arr, slice)
// 输出:Array: [1 10 3 4 5], Slice: [10 3]
// 可以看到 arr[1] 被修改为 10
// append操作:如果容量足够,直接在底层数组上追加
// 如果容量不足,会创建新的底层数组,并复制原有元素
// 这里 slice 的容量是4,当前长度是2,追加2个元素后长度变为4,容量足够
newSlice := append(slice, 6, 7)
fmt.Printf("New slice: %v, len=%d, cap=%d\n", newSlice, len(newSlice), cap(newSlice))
// 输出:New slice: [10 3 6 7], len=4, cap=4
// 注意:因为容量足够,newSlice 仍然指向原来的底层数组 arr
// 所以 arr 也会被修改为 [1 10 3 6 7]
// 演示扩容机制:当 len == cap 时,append 会触发扩容
fmt.Println("\n--- 扩容机制演示 ---")
smallSlice := make([]int, 2, 2) // len=2, cap=2
fmt.Printf("Before append: slice=%v, len=%d, cap=%d\n", smallSlice, len(smallSlice), cap(smallSlice))
fmt.Printf("Slice pointer: %p\n", &smallSlice[0])
// 追加一个元素,触发扩容
expandedSlice := append(smallSlice, 3)
fmt.Printf("After append: slice=%v, len=%d, cap=%d\n", expandedSlice, len(expandedSlice), cap(expandedSlice))
fmt.Printf("New slice pointer: %p\n", &expandedSlice[0])
// 输出可以看到指针地址变了,说明创建了新的底层数组
// 扩容规则(Go 1.18+):
// 1. 如果新容量 < 256,新容量 = 旧容量 * 2
// 2. 如果新容量 >= 256,新容量 = 旧容量 + 旧容量/4
// 3. 还会考虑内存对齐等因素
fmt.Println("\n--- 扩容倍数演示 ---")
s := make([]int, 0)
oldCap := 0
for i := 0; i < 20; i++ {
s = append(s, i)
if cap(s) != oldCap {
fmt.Printf("len=%d, cap=%d (扩容了)\n", len(s), cap(s))
oldCap = cap(s)
}
}
}常见错误分析:
- 错误1:误以为切片是值拷贝,实际上切片是引用类型
- 错误2:不理解切片扩容机制,导致性能问题
- 错误3:切片操作越界
Q3: Go的map是线程安全的吗?如何实现线程安全的map?
解题思路:
- 说明Go的map不是线程安全的
- 解释并发读写map会导致panic
- 提供几种线程安全的解决方案:
- 使用sync.Mutex
- 使用sync.RWMutex
- 使用sync.Map(适合读多写少的场景)
代码实现:
package main
import (
"fmt"
"sync"
)
type SafeMap struct {
mu sync.RWMutex
data map[string]int
}
func NewSafeMap() *SafeMap {
return &SafeMap{
data: make(map[string]int),
}
}
func (sm *SafeMap) Get(key string) (int, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
val, ok := sm.data[key]
return val, ok
}
func (sm *SafeMap) Set(key string, value int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.data[key] = value
}
func (sm *SafeMap) Delete(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.data, key)
}
func main() {
sm := NewSafeMap()
sm.Set("key1", 100)
if val, ok := sm.Get("key1"); ok {
fmt.Println("key1:", val)
}
sm.Delete("key1")
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
sm.Set(fmt.Sprintf("key%d", n), n)
}(i)
}
wg.Wait()
fmt.Println("Safe map size:", len(sm.data))
}常见错误分析:
- 错误1:在多个goroutine中并发读写map导致panic
- 错误2:过度使用锁导致性能下降
- 错误3:死锁问题(忘记解锁或锁顺序不一致)
方案选择指南:
场景对比
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 读多写少 | sync.RWMutex | 读操作并发,性能最优 |
| 读写都多 | sync.Mutex | 避免写操作饥饿 |
| 读少写多 | sync.Mutex | 读写锁反而更慢 |
| 类型固定 | 手动锁 | 类型安全,性能好 |
| 类型动态 | sync.Map | 灵活性高,适合缓存 |
类型固定场景(90%的业务代码)
// 场景:用户服务、订单服务、配置管理等
type UserService struct {
users map[int64]*User // 类型固定:key是int64,value是*User
mu sync.RWMutex
}
type User struct {
ID int64
Name string
}
func (s *UserService) GetUser(id int64) (*User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[id]
return user, ok
}
// 优点:
// 1. 类型安全,编译时检查
// 2. 性能更好
// 3. 代码清晰,易于维护类型动态场景(10%的基础设施代码)
// 场景:通用缓存、动态配置、插件系统等
var cache sync.Map
// 可以存储任意类型的数据
cache.Store("user:1", User{ID: 1, Name: "John"})
cache.Store("config:timeout", 30)
cache.Store("session:abc", SessionData{...})
cache.Store(123, "numeric key")
// 优点:
// 1. 灵活性高,可以存储任意类型
// 2. 适合缓存场景
// 3. 内置并发安全
// 缺点:
// 1. 需要类型断言,容易出错
// 2. 可能导致运行时panic
// 3. 性能不如手动锁快速决策指南
你的map类型固定吗?
├─ 是 → 使用手动锁(sync.Mutex 或 sync.RWMutex)
│ ├─ 读多写少 → sync.RWMutex
│ └─ 读写都多 → sync.Mutex
│
└─ 否 → 使用 sync.Map
└─ 适合缓存、动态配置等场景
实际应用示例
业务代码(推荐手动锁):
type UserRepository struct {
users map[int64]*User
mu sync.RWMutex
}
func (r *UserRepository) GetUser(id int64) (*User, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
user, ok := r.users[id]
return user, ok
}缓存代码(推荐sync.Map):
var globalCache sync.Map
func GetFromCache(key interface{}) (interface{}, bool) {
return globalCache.Load(key)
}
func SetCache(key, value interface{}) {
globalCache.Store(key, value)
}同步map实现通用缓存
方案1:泛型版本(Go 1.18+,类型安全)
package cache
import "sync"
type Cache[K comparable, V any] struct {
mu sync.RWMutex
data map[K]V
}
func New[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{
data: make(map[K]V),
}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache[K, V]) Set(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *Cache[K, V]) Delete(key K) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
}
func (c *Cache[K, V]) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.data)
}
// 使用示例
func ExampleGenericCache() {
// 字符串缓存
stringCache := New[string, string]()
stringCache.Set("key1", "value1")
if val, ok := stringCache.Get("key1"); ok {
fmt.Println(val) // value1
}
// 整数缓存
intCache := New[string, int]()
intCache.Set("count", 100)
if val, ok := intCache.Get("count"); ok {
fmt.Println(val) // 100
}
// 结构体缓存
type User struct {
ID int64
Name string
}
userCache := New[int64, *User]()
userCache.Set(1, &User{ID: 1, Name: "John"})
if user, ok := userCache.Get(1); ok {
fmt.Println(user.Name) // John
}
}方案2:interface{}版本(灵活但需要类型断言)
package cache
import "sync"
type GenericCache struct {
mu sync.RWMutex
data map[interface{}]interface{}
}
func NewGenericCache() *GenericCache {
return &GenericCache{
data: make(map[interface{}]interface{}),
}
}
func (c *GenericCache) Get(key interface{}) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *GenericCache) Set(key, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *GenericCache) Delete(key interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
}
func (c *GenericCache) Size() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.data)
}
// 使用示例
func ExampleGenericCache() {
cache := NewGenericCache()
// 可以存储任意类型
cache.Set("string_key", "string_value")
cache.Set(123, 456)
cache.Set(3.14, "pi")
// 获取值(需要类型断言)
if val, ok := cache.Get("string_key"); ok {
str := val.(string)
fmt.Println(str) // string_value
}
if val, ok := cache.Get(123); ok {
num := val.(int)
fmt.Println(num) // 456
}
}方案3:带过期时间的缓存
package cache
import (
"sync"
"time"
)
type CacheItem struct {
Value interface{}
Expiration int64
}
type ExpiringCache struct {
mu sync.RWMutex
data map[string]*CacheItem
}
func NewExpiringCache() *ExpiringCache {
cache := &ExpiringCache{
data: make(map[string]*CacheItem),
}
go cache.cleanupExpired()
return cache
}
func (c *ExpiringCache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = &CacheItem{
Value: value,
Expiration: time.Now().Add(ttl).UnixNano(),
}
}
func (c *ExpiringCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, ok := c.data[key]
if !ok {
return nil, false
}
// 检查是否过期
if time.Now().UnixNano() > item.Expiration {
return nil, false
}
return item.Value, true
}
func (c *ExpiringCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
}
func (c *ExpiringCache) cleanupExpired() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
c.mu.Lock()
now := time.Now().UnixNano()
for key, item := range c.data {
if now > item.Expiration {
delete(c.data, key)
}
}
c.mu.Unlock()
}
}
// 使用示例
func ExampleExpiringCache() {
cache := NewExpiringCache()
// 设置缓存,5秒后过期
cache.Set("user:1", User{ID: 1, Name: "John"}, 5*time.Second)
// 获取缓存
if user, ok := cache.Get("user:1"); ok {
u := user.(User)
fmt.Println(u.Name) // John
}
// 等待5秒后,缓存已过期
time.Sleep(5 * time.Second)
if _, ok := cache.Get("user:1"); !ok {
fmt.Println("Cache expired")
}
}方案4:带统计功能的缓存
package cache
import (
"sync"
"sync/atomic"
)
type CacheStats struct {
Hits uint64
Misses uint64
Sets uint64
Deletes uint64
}
type StatsCache struct {
mu sync.RWMutex
data map[string]interface{}
stats CacheStats
}
func NewStatsCache() *StatsCache {
return &StatsCache{
data: make(map[string]interface{}),
}
}
func (c *StatsCache) Get(key string) (interface{}, bool) {
c.mu.RLock()
val, ok := c.data[key]
c.mu.RUnlock()
if ok {
atomic.AddUint64(&c.stats.Hits, 1)
} else {
atomic.AddUint64(&c.stats.Misses, 1)
}
return val, ok
}
func (c *StatsCache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
atomic.AddUint64(&c.stats.Sets, 1)
}
func (c *StatsCache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
atomic.AddUint64(&c.stats.Deletes, 1)
}
func (c *StatsCache) GetStats() CacheStats {
return CacheStats{
Hits: atomic.LoadUint64(&c.stats.Hits),
Misses: atomic.LoadUint64(&c.stats.Misses),
Sets: atomic.LoadUint64(&c.stats.Sets),
Deletes: atomic.LoadUint64(&c.stats.Deletes),
}
}
func (c *StatsCache) HitRate() float64 {
stats := c.GetStats()
total := stats.Hits + stats.Misses
if total == 0 {
return 0
}
return float64(stats.Hits) / float64(total) * 100
}
// 使用示例
func ExampleStatsCache() {
cache := NewStatsCache()
cache.Set("key1", "value1")
cache.Set("key2", "value2")
cache.Get("key1") // Hit
cache.Get("key2") // Hit
cache.Get("key3") // Miss
stats := cache.GetStats()
fmt.Printf("Hits: %d, Misses: %d\n", stats.Hits, stats.Misses)
fmt.Printf("Hit Rate: %.2f%%\n", cache.HitRate())
// 输出:
// Hits: 2, Misses: 1
// Hit Rate: 66.67%
}方案对比
| 方案 | 类型安全 | 性能 | 灵活性 | 适用场景 |
|---|---|---|---|---|
| 泛型缓存 | ✅ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 业务代码,类型固定 |
| interface{}缓存 | ❌ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 通用缓存,类型灵活 |
| 过期缓存 | ❌ | ⭐⭐⭐ | ⭐⭐⭐⭐ | 临时数据,会话管理 |
| 统计缓存 | ❌ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 需要监控缓存的场景 |
实际应用:用户缓存服务
package service
import (
"time"
"yourproject/cache"
)
type User struct {
ID int64
Name string
Email string
}
type UserService struct {
cache *cache.Cache[int64, *User] // 泛型缓存
db *Database
}
func NewUserService(db *Database) *UserService {
return &UserService{
cache: cache.New[int64, *User](),
db: db,
}
}
func (s *UserService) GetUser(id int64) (*User, error) {
// 先从缓存获取
if user, ok := s.cache.Get(id); ok {
return user, nil
}
// 缓存未命中,从数据库查询
user, err := s.db.QueryUser(id)
if err != nil {
return nil, err
}
// 写入缓存
s.cache.Set(id, user)
return user, nil
}
func (s *UserService) UpdateUser(user *User) error {
// 更新数据库
if err := s.db.UpdateUser(user); err != nil {
return err
}
// 更新缓存
s.cache.Set(user.ID, user)
return nil
}Q4: Go的defer有什么特性?
解题思路:
- 解释defer的基本概念(延迟执行)
- 说明defer的执行顺序(LIFO)
- 解释defer的参数求值时机(声明时求值)
- 演示defer的常见应用场景(资源释放、错误处理)
代码实现:
package main
import (
"fmt"
"os"
)
func main() {
defer fmt.Println("defer 1")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3")
fmt.Println("main function")
for i := 0; i < 3; i++ {
defer fmt.Printf("defer in loop: %d\n", i)
}
result := calculate()
fmt.Println("Result:", result)
}
func calculate() (result int) {
defer func() {
result += 10
}()
result = 5
return
}
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
return nil
}常见错误分析:
- 错误1:不理解defer的执行顺序
- 错误2:在循环中使用defer导致资源泄漏
- 错误3:defer中修改命名返回值导致逻辑错误
Q5: Go的error处理机制有什么特点?
解题思路:
- 说明Go的error是一个接口类型
- 解释Go的error处理哲学(显式处理、不使用异常)
- 演示自定义error的实现
- 介绍errors包的使用
- 讲解错误包装和错误链
- 说明错误处理的最佳实践
核心概念:
1. Error的本质
error是一个接口类型,只有一个方法Error() string:
type error interface {
Error() string
}2. Go的错误处理哲学
- 显式处理:错误必须显式检查和处理
- 不使用异常:没有try-catch-finally机制
- 错误即值:error是一个普通的值,可以传递、存储、比较
- 快速失败:遇到错误立即返回,避免继续执行
代码实现:
基础错误处理
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}自定义Error类型
type ValidationError struct {
Field string
Message string
}
func (ve *ValidationError) Error() string {
return fmt.Sprintf("validation error on field %s: %s", ve.Field, ve.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Message: "age cannot be negative",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Message: "age cannot be greater than 150",
}
}
return nil
}
func main() {
err := validateAge(-5)
if err != nil {
if ve, ok := err.(*ValidationError); ok {
fmt.Printf("Validation failed: %s - %s\n", ve.Field, ve.Message)
} else {
fmt.Println("Unknown error:", err)
}
}
}错误包装和错误链(Go 1.13+)
import (
"errors"
"fmt"
)
func readFile(filename string) ([]byte, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", filename, err)
}
return data, nil
}
func main() {
data, err := readFile("nonexistent.txt")
if err != nil {
fmt.Printf("Error: %v\n", err)
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist")
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path: %s\n", pathErr.Path)
}
}
}定义错误变量
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidInput = errors.New("invalid input")
)
func getUser(id int64) (*User, error) {
user, err := db.QueryUser(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
return nil, err
}
return user, nil
}
func main() {
user, err := getUser(1)
if errors.Is(err, ErrUserNotFound) {
fmt.Println("User not found")
}
}Panic和Recover
func safeExecute() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
panic("something went wrong")
}
func main() {
if err := safeExecute(); err != nil {
fmt.Println("Recovered:", err)
}
}最佳实践:
- 总是检查错误
file, err := os.Open("file.txt")
if err != nil {
return err
}
defer file.Close()- 添加上下文信息
return fmt.Errorf("failed to get user %d: %w", id, err)- 定义明确的错误类型
var ErrUserNotFound = errors.New("user not found")- 避免过度包装
return fmt.Errorf("failed: %w", err)常见错误分析:
- 错误1:忽略error返回值
file, _ := os.Open("file.txt") // 错误 - 错误2:过度使用panic/recover
panic("division by zero") // 错误,应该返回error - 错误3:错误信息不够详细,难以调试
return errors.New("error") // 错误,缺少上下文 - 错误4:错误包装后丢失原始错误
return fmt.Errorf("failed: %s", err) // 错误,应该用%w