Go面试题
一、Go语言基础
1. GOROOT,GOPATH,GOBIN?
- GOROOT: Go语言安装路径
- GOPATH: 我们自己定义的工作空间,多个工作区目录的路径
- GOBIN:Go程序生成可执行文件的路径
2. 命令源码文件(main.go), 库源码文件?
- 命令源码文件:命令源码文件是程序的运行入口,是每个可独立运行的程序必须拥有的
- 库源码文件:Go标准库和项目依赖包文件
3. 包函数权限?
- 相同包:函数可以小写(私有函数)
- 不同包:函数必须大写才能调用(公用函数)
- internal:包级权限
4. 声明变量有几种方式?
- 直接声明(var name string)
- 赋值声明(var name = “hello”)
- 类型推断声明(name := “hello”)
5. Go 语言的类型推断可以带来哪些好处?
- 通过类型推断体验动态类型语言一部分优势,提升程序灵活性
- Python是牺牲代码维护性和运行效率换来的
- Go 语言是静态类型的,初始化变量就确定类型,避免程序维护问题
- 类型确定在编译期完成,不会影响运行效率
6. 怎样判断一个变量的类型?
- 使用类型断言表达式(name.(string))
- 使用反射(reflect.TypeOf(name))
7. 基本数据结构?
- 值类型:数字(int,float),字符串(string, byte, rune), 布尔类型(true, false), array, 结构体(struct)
- 引用类型 :slice, map(map[string]bool),interface,channel,函数,指针
8. 数组,切片(array, slice)
- 数组长度固定,切片是可变长的
- 怎样正确估算切片的长度和容量?
- var i; i++, cap = 2 * len
- i := 1024; i++, cap = 1.25 * len
- var i; 2 * i, cap > 2 * len
9. 字典(map)
- Go语言字典的键类型不可以是函数类型、字典类型和切片类型(无法hash类型)
- 用var声明的值为nil的map,读,删除都不报错,新增报错,需要初始化
10. 结构体(struct)
- 嵌入字段,没有类型(结构体嵌入结构体)
- 结构体字段大写才能暴露
11. 通道(channel)(**)
- 对通道的发送和接收操作都有哪些基本的特性?
- 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的
- 发送操作和接收操作中对元素值的处理都是不可分割的
- 发送操作在完全完成之前会被阻塞。接收操作也是如此
- 单向通道有什么应用价值?
- channel 的主要组成?
- 一个环形数组实现的队列,用于存储消息元素
- 两个链表实现的 goroutine 等待队列,用于存储阻塞在 recv 和 send 操作上的 goroutine
- 一个互斥锁,用于各个属性变动的同步
- channel 的关闭注意事项:
- 关闭一个未初始化(nil) 的 channel 会产生 panic
- 重复关闭同一个 channel 会产生 panic
- 向一个已关闭的 channel 中发送消息会产生 panic
- 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭
- 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
- 无缓冲channel和有缓冲channel的区别?
a. 无缓存的 channel
从无缓存的 channel 中读取消息会阻塞,直到有 goroutine 向该 channel 中发送消息;同理,向无缓存的 channel 中发送消息也会阻塞,直到有 goroutine 从 channel 中读取消息。
b. 有缓存的 channel
- 有缓存的 channel 的声明方式为指定 make 函数的第二个参数,该参数为 channel 缓存的容量。
- 有缓存的 channel 类似一个阻塞队列(采用环形数组实现)。当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他 goroutine 从中读取消息;相应的,当 channel 中消息不为空时,读取消息不会出现阻塞,当 channel 为空时,读取操作会造成阻塞,直到有 goroutine 向 channel 中写入消息。
12. 函数(func)
- 高阶函数
- 接受其他的函数作为参数传
- 把其他的函数作为结果返回
- Go函数参数是值复制,引用类型拷贝本身
13. 接口(interface)
- 只声明而不初始化,或者显式地赋给它nil,否则接口变量的值就不会为nil
- 变量类型不是nil,值是nil,与nil不等
14. 指针
- Go语言中的哪些值是不可寻址的吗?
- 不可变的
- 临时结果(例外: 切片字面量的索引结果值是可寻址的)
- 不安全的
15. go程(goroutine)
- 主go程起子goroutine中for打印,无任何结果,当前go程不会打印数据
- 应用场景
- IO耗时操作,读写日志
- 需要异步执行的任务,函数(发送邮件,短信)
16. 错误处理(error)
- 对于具体错误的判断,Go 语言中都有哪些惯用法?
- 已知范围错误,类型断言表达式或switch判断
- 已有相同错误变量,判等
- 未知错误,使用错误信息字符串判断
17. panic, recover, defer函数
- panic报错过程?
- defer执行顺序?
18. Go语言测试(go test)
- Go 语言对测试函数的名称和签名都有哪些规定
- 对于功能测试函数来说,其名称必须以Test为前缀,并且参数列表中只应有一个*testing.T类型的参数声明
- 对于性能测试函数来说,其名称必须以Benchmark为前缀,并且唯一参数的类型必须是*testing.B类型的
- 对于示例测试函数来说,其名称必须以Example为前缀,但对函数的参数列表没有强制规定
二、Go语言标准包(standard package)
1. archive
2. bufio(2*)$$
- bufio.Reader类型值中的缓冲区起着怎样的作用?
- bufio.Reader类型的值(以下简称Reader值)内的缓冲区,其实就是一个数据存储中介,它介于底层读取器与读取方法及其调用方之间
- 所谓的底层读取器,就是在初始化此类值的时候传入的io.Reader类型的参数值
- Reader值的读取方法一般都会先从其所属值的缓冲区中读取数据
- 同时,在必要的时候,它们还会预先从底层读取器那里读出一部分数据,并暂存于缓冲区之中以备后用
3. builtin
4. bytes(2*)$$
- bytes.Buffer类型的值记录的已读计数,在其中起到了怎样的作用?
- 读取内容时,相应方法会依据已读计数找到未读部分,并在读取后更新计数
- 写入内容时,如需扩容,相应方法会根据已读计数实现扩容策略
- 截断内容时,相应方法截掉的是已读计数代表索引之后的未读部分
- 读回退时,相应方法需要用已读计数记录回退点
- 重置内容时,相应方法会把已读计数置为0
- 导出内容时,相应方法只会导出已读计数代表的索引之后的未读部分
- 获取长度时,相应方法会依据已读计数和内容容器的长度,计算未读部分的长度并返回
5. compress
6. container(2*)$$
- heap(最小堆)
- list(双向链表)
- ring(环)
- 可以把自己生成的Element类型值传给链表吗?
- 不会接受,这些方法将不会对链表做出任何改动
- 因为我们自己生成的Element值并不在链表中,所以也就谈不上“在链表中移动元素”
- 更何况链表不允许我们把自己生成的Element值插入其中
7. context(***)
- Index
- func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
- func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
- func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
- type CancelFunc
- type Context
- 怎样使用context包中的程序实体,实现一对多的 goroutine 协作流程?
- 在这个函数体中,我先后调用了context.Background函数和context.WithCancel函数
- 并得到了一个可撤销的context.Context类型的值(由变量cxt代表)
- 以及一个context.CancelFunc类型的撤销函数(由变量cancelFunc代表)
- context用途?
- RPC调用,WithCancel取消通知其他go程,sync.errorGroup
- PipeLine,使用switch处理了case <-ctx.Done()
- 超时请求,timerCtx。具体实例化的方法是 WithDeadline 和 WithTimeout。
- HTTP服务器的request互相传递数据,valueCtx,存储中间件函数,数据
8. crypto
9. database(*)
10. debug
11. embed
12. encoding(*)
13. errors(2*)$$
14. expvar
15. flag(2*)
16. fmt(2*)$$
- Index
- func Errorf(format string, a …interface{}) error
- func Fprintf(w io.Writer, format string, a …interface{}) (n int, err error)
- func Fscanf(r io.Reader, format string, a …interface{}) (n int, err error)
- func Printf(format string, a …interface{}) (n int, err error)
- func Sprintf(format string, a …interface{}) string
- func Scanf(format string, a …interface{}) (n int, err error) (stdin)
17. go
18. hash
19. html
20. image
21. index
22. io(2*)$$
- 在io包中,io.Reader的扩展接口和实现类型都有哪些?它们分别都有什么功用?
- 扩展接口
- io.ReadWriter
- io.ReadCloser
- io.ReadWriteCloser
- io.ReadSeeker
- io.ReadWriteSeeker
- 实现类型
- *io.LimitedReader
- *io.SectionReader
- *io.teeReader
- io.multiReader
- io.pipe
- io.PipeReader
23. log(*)
24. math
25. mime
26. net(2*)
- net.Dial函数的第一个参数network有哪些可选值?
- net.Dial函数会接受两个参数,分别名为network和address,都是string类型的
- 参数network常用的可选值一共有 9 个。这些值分别代表了程序底层创建的 socket 实例可使用的不同通信协议
- tcp, tcp4, tcp6, udp, udp4, udp6, unix, unixgram, unixpacket
- http.Client类型中的Transport字段代表着什么?
- 向网络服务发送 HTTP 请求,并从网络服务接收 HTTP 响应的操作过程
- 也就是说,该字段的方法RoundTrip应该实现单次HTTP 事务(或者说基于 HTTP 协议的单次交互)需要的所有步骤
27. os(2*)$$
- os.File类型都实现了哪些io包中的接口?
- os.File类型拥有的都是指针方法,所以除了空接口之外,它本身没有实现任何接口
- 而它的指针类型则实现了很多io代码包中的接口
- 对于io包中最核心的 3 个简单接口io.Reader、io.Writer和io.Closer,*os.File类型都实现了它们
- 该类型还实现了另外的 3 个简单接口,即:io.ReaderAt、io.Seeker和io.WriterAt
28. path
29. plugin
30. reflect(*)
31. regexp
32. runtime(2*)
- 怎样让程序对 CPU 概要信息进行采样?
- 需要用到runtime/pprof包中的 API
- 在我们想让程序开始对 CPU 概要信息进行采样的时候,需要调用这个代码包中的StartCPUProfile函数
- 而在停止采样的时候则需要调用该包中的StopCPUProfile函数
33. sort(*)
34. strconv(*)
35. strings(2*) $$
- 与string值相比,strings.Builder类型的值有哪些优势?
- 已存在的内容不可变,但可以拼接更多的内容
- 减少了内存分配和内容拷贝的次数
- 可将内容重置,可重用值
36. sync(2*)$$
- Index
- type Cond(变量)
- type Loker (锁,解锁接口)
- type Map(并发安全字典)
- type Mutex(互斥锁)
- type Once(只做一次)
- type Pool
- type RWMutex(读写互斥锁)
- type WaitGroup(等待多个go程完成)
- errgroup(同步,错误传递,上下文取消)
- semaphore
- singleflight
- syncmap
- atomic
- 我们使用互斥锁时有哪些注意事项?(sync.Mutex与sync.RWMutex)
- 不要重复锁定互斥锁
- 不要忘记解锁互斥锁,必要时使用defer语句
- 不要对尚未锁定或者已解锁的互斥锁解锁
- 不要在多个函数之间直接传递互斥锁
- 条件变量(sync.Cond)怎样与互斥锁配合使用?
- 条件变量的初始化离不开互斥锁,并且它的方法有的也是基于互斥锁的
- sync/atomic包中提供了几种原子操作?可操作的数据类型又有哪些?
- 加法(add)、比较并交换(compareand swap,简称 CAS)、加载(load)、存储(store)和交换(swap)
- int32、int64、uint32、uint64、uintptr,以及unsafe包中的Pointer
- 针对unsafe.Pointer类型,该包并未提供进行原子加法操作的函数
- sync.WaitGroup类型值中计数器的值可以小于0吗?
- 不可以,这样会引发一个 panic
- 先统一Add,再并发Done,最后Wait
- 为什么说临时对象池(sync.Pool)中的值会被及时地清理掉?
Go 语言运行时系统中的垃圾回收器,所以在每次开始执行之前,都会对所有已创建的临时对象池中的值进行全面地清除
- 并发安全字典(sync.Map)对键的类型有要求吗?
键的实际类型不能是函数类型、字典类型和切片类型
- Go如何保证gorountine执行完毕后继续执行(一个goroutine执行完后另一个开始执行)?
a. 使用sync.WaitGroup
b. 使用channel(无缓冲channel,取消息阻塞)
c. 使用time.sleep
37. syscall
38. testing(2*)
39. text
40. time(2*)
- Timer的常见使用场景和接口
- time.NewTimer(d)创建一个Timer
- timer.Stop()停掉当前Timer
- timer.Reset(d)重置当前Timer
- time.After, time.AfterFunc等待指定时间
41. unicode(2*) $$
42. unsafe(*)
三、Go语言相关原理
1. Don’t communicate by sharing memory; share memory by communicating.
* 不要通过共享内存来通信,而应该通过通信来共享内存
2. GPM(goroutine)(***)
* Goroutine:就是咱们常用的用go关键字创建的执行体,它对应一个结构体g,结构体里保存了goroutine的堆栈信息
* Machine:表示操作系统的线程
* Processor:表示处理器,有了它才能建立G、M的联系
* 三者的关系
1. 默认最多启动GOMAXPROCS线程和处理器,相互绑定,线程和处理器数量一致,节省切换线程开销
2. 创建一个goroutine,进行函数体地址、参数起始地址、参数长度等信息以及调度相关属性更新,放到一个空闲处理器P的私有队列中
3. 再创建一个goroutine,轮流放在处理器P私有队列中(优先空闲)【窗口】
4. 当所有处理器P的私有队列满了,再创建一个goroutine,放到全局队列中【候车大厅】
5. 线程M疯狂取goroutine,先取绑定处理器P私有队列goroutine,再取全局队列goroutine,再从其他处理器P取goroutine
6. 如果没有要执行的goroutine,线程M与处理器P断开连接,进入休眠
7. 如果执行goroutine发生IO阻塞住了,线程M不会等待,找其他可执行goroutine
8. 系统调用
9. sysmon:监控线程M,回收垃圾、回收长时间系统调度阻塞的P、向长时间运行的G发出抢占调度
3. CSP(channel)
* 全称Communicating Sequential Processes,意为通讯顺序进程,它是七大并发模型中的一种,它的核心观念是将两个并发执行的实体通过通道channel连接起来,所有的消息都通过channel传输
四、Go语言框架
1. Gin
- 支持多种HTTP请求方式
- GET
- 路径参数:/user/:name
- 查询参数:
- 默认参数:firstname := c.DefaultQuery(“firstname”, “Guest”) // /v1/user?firstname=“Smith”
- 动态参数:lastname := c.Query(“lastname”) // /v1/user?lastname=“jack”
- POST
- x-www-form-urlencoded:
- 默认: message := c.DefaultPostForm(“message”)
- 动态:message := c.PostForm(“message”)
- 字典:names := c.PostFormMap(“names”)
- 上传文件(form-data):
- 单文件:file, _ := c.FormFile(“file”)
- 多文件:files := form.File[“files”]
- 保存文件:c.SaveUploadedFile(file, dst)
- 分组路由:v1 := router.Group("/v1")
- HTTP Server和中间件:
- 无中间件Server:r := gin.New()
- 默认中间件Server:r := gin.Default(":8080") // 默认包含,gin.Logger,gin.Recovery中间件
- 自定义HTTP Server
- Gin运行多个HTTP Server:g.Go()
- 中间件使用:r.Use(gin.Recovery())
- 中间件使用go程,需要复制*gin.Context:cCp := c.Copy()
- 自定义中间件:c.Next()
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
c.Set("example", "12345")
c.Next()
latency := time.Since(t)
log.Print(latency)
status := c.Writer.Status()
log.Println(status)
}
}
- 日志
- 模型绑定和验证
- Must bind:强制绑定,无法很好的自定义控制(请求码)
- Should bind:应该绑定,开发者负责HTTP请求错误处理,自定义校验函数
type PostParams struct {
Name string `json:"name" uri:"name" form:"name"`
Age int `json:"age" uri:"age" form:"age" binding:"required,mustBig"`
Sex bool `json:"sex" uri:"sex" form:"sex"`
}
var p PostParams
err := c.ShouldBindJSON(&p)
- 请求返回和数据渲染
id := c.Param("id")
user := c.DefaultPostForm("user", "ft")
pwd := c.PostForm("pwd")
c.JSON(http.StatusOK, gin.H{
"id": id,
"user": user,
"pwd": pwd,
"method": c.Request.Method,
})
- 正常关机或重启
2. GORM v2
- 连接数据库
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}
- CRUD 接口
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
result := db.Create(&user)
db.Select("Name", "Age", "CreatedAt").Create(&user)
db.Omit("Name", "Age", "CreatedAt").Create(&user)
var users = []User{{Name: "jinzhu1"}, {Name: "jinzhu2"}, {Name: "jinzhu3"}}
db.Create(&users)
db.CreateInBatches(users, 100)
var user User
db.First(&user)
db.Select("name", "age").Find(&users)
db.Where("name = ?", "jinzhu").First(&user)
)
db.Order("age desc, name").Find(&users)
db.Limit(10).Offset(5).Find(&users)
db.Select("name, sum(age) as total").Group("name").Having("name = ?", "group").Find(&user)
type result struct {
Name string
Email string
}
db.Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&result{})
db.Where("id = ?", 1).First(&User{}).Update("name", "jack")
db.Where("id = ?", 1).First(&User{}).Updates(User{
Name: "jack",
Age: 26,
})
db.Where("id in (?)", []int{1, 2}).Delete(&User{})
db.Where("id in (?)", []int{1, 2}).Unscoped().Delete(&User{})
type Result struct {
ID int
Name string
Age int
}
var result Result
db.Raw("SELECT id, name, age FROM users WHERE id = ?", 3).Scan(&result)
db.Exec("DROP TABLE users")
- 关联
type User struct {
gorm.Model
Name string
CompanyID int
Company Company
}
type Company struct {
ID int
Name string
}```
* Has One
```go
type User struct {
gorm.Model
CreditCard CreditCard
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
type User struct {
gorm.Model
CreditCards []CreditCard
}
type CreditCard struct {
gorm.Model
Number string
UserID uint
}
type User struct {
gorm.Model
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
gorm.Model
Name string
}
- 关联模式(自动创建,更新)
- 预加载(Preload)
- 教程
- Context
- 错误处理
- 链式操作
- Session
- 钩子:在新增,修改之前,之后调用方法
- 事务:db.Transaction,在事务中执行一些 db 操作(从这里开始,您应该使用 ‘tx’ 而不是 ‘db’)
- 迁移:db.AutoMigrate(&User{}) // User用户表
- 日志
- 通用数据库接口:*gorm.DB返回一个通用的数据库接口 *sql.DB
- 性能
- 禁用默认事务(&gorm.Config)
- 缓存预编译语句
- 带 PreparedStmt 的 SQL 生成器(原生SQL)
- 查询选择字段
- 数据量大进行批量新增,更新
- 使用索引优化加速查询
- 读写分离
- 自定义数据类型:Scanner / Valuer
- 约定
- 设置
- 高级主题
- Dataase Resolver
- Prometheus
- 提示
- 索引
- 约束
- 复合主键
- 安全
- GORM配置
- 编写插件
- 编写驱动
3. Redis
- Getting started
- Redis Cluster
- Redis Sentinel
- Redis Ring
- Tracing
4. gRPC
1. RPC(remote procedure call 远程过程调用)
框架实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。
2. gRPC
高性能,开源的通用RPC框架
3. ProtoBuf
- 概念
ProtoBuf实际是一套类似JSON或者XML的数据传输格式和规范,用于不同应用或进程之间进行通信时使用。通信时所传递的信息是通过Protobuf定义的message数据结构进行打包,然后编译成二进制的码流再进行传输或者存储。
- 优点
a. 足够简单
b. 序列化后体积很小:消息大小只需要XML的1/10 ~ 1/3
c. 解析速度快:解析速度比XML快20 ~ 100倍
d. 多语言支持
c. 更好的兼容性,Protobuf设计的一个原则就是要能够很好的支持向下或向上兼容
- ProtoBuf与JSON的区别?
a. ProtoBuf编码解码性能比JSON高,ProtoBuf编码成二进制,传输数据量驾校(高性能)
b. gRPC可以通过ProtoBuf来定义接口,从而可以有更加严格的接口约束条件(安全性)
c. JSON数据格式对用户更加友好,方便理解
5. Cron
6. kit