go 面试题
go 里的多态
面向对象中的 “多态” 指的同一个行为具有多种不同表现形式或形态的能力,具体是指一个类实例(对象)的相同方法在不同情形有不同表现形式。
多态也使得不同内部结构的对象可以共享相同的外部接口,也就是都是一套外部模板,内部实际是什么,只要符合规格就可以。在 Go 语言中,多态是通过接口来实现的:
type AnimalSounder interface {
MakeDNA()
}
func MakeSomeDNA(animalSounder AnimalSounder) { // 参数是AnimalSounder接口类型
animalSounder.MakeDNA()
}
java 和go 的区别
1. 基础对比
| 特性 | Go | Java |
|---|---|---|
| 语言定位 | 系统编程 + 高并发服务 | 企业级应用 + 跨平台开发 |
| 类型系统 | 静态类型,简洁 | 静态类型,严格 |
| 编译方式 | 直接编译成机器码(无 JVM) | 编译成字节码,运行在 JVM 上 |
| 运行效率 | 接近 C,启动快 | 有 JVM 开销,启动稍慢 |
| 语法复杂度 | 简洁,语法元素少 | 语法丰富,关键字和特性多 |
2. 面向对象
| 特性 | Go | Java |
|---|---|---|
| 类 (class) | 没有 class,用 struct | 有 class |
| 继承 | 不支持继承,强调组合 | 支持继承(单继承,多接口) |
| 多态 | 接口(隐式实现) | 接口(显式实现)+ 继承 |
| 封装 | 通过首字母大小写控制可见性 | public / private / protected |
3. 并发模型
| 特性 | Go | Java |
|---|---|---|
| 并发模型 | goroutine + channel(CSP 模型) | 线程 + 线程池(共享内存+锁) |
| 线程开销 | goroutine 超轻量(KB 级) | Java 线程较重(MB 级) |
| 原生支持 | 内置 go 关键字和 channel |
通过 Thread、ExecutorService |
| 并发编程难度 | 简单直观 | 需要锁、同步工具类 |
👉 Go 天生为高并发服务优化,而 Java 并发需要更多代码。
4. 内存 & GC
| 特性 | Go | Java |
|---|---|---|
| 内存管理 | 自动 GC,无手动释放 | 自动 GC |
| GC 特性 | 追求低延迟(适合服务端) | 高吞吐(可调 GC 策略) |
| 指针 | 有指针,但安全(不能运算) | 没有指针,完全引用 |
5. 开发生态
| 特性 | Go | Java |
|---|---|---|
| 标准库 | 内置网络、并发支持强 | 丰富的 API,覆盖广 |
| 框架 | 偏轻量(gin, gRPC) | 大量成熟框架(Spring, Hibernate) |
| 社区 | 云原生 / 微服务强 | 企业级应用主流 |
6. 应用场景
- Go:高并发服务器、微服务、云原生、容器(Docker、K8s)、区块链、DevOps 工具
- Java:企业级系统、金融、电商、大型 Web 应用、Android
7. 总结一句话
- Go:快、轻量、并发强 → 更适合 云原生、高并发服务
- Java:成熟、完整、生态丰富 → 更适合 企业级应用、大型系统
go 里基本数据类型包括哪些
Go 的基本数据类型可以分为几大类:
1. 布尔类型
-
bool
- 取值:
true或false - 默认值:
false
- 取值:
2. 数值类型
整型
-
有符号整数:
int8,int16,int32,int64,int -
无符号整数:
uint8,uint16,uint32,uint64,uint -
特殊整型:
byte→uint8的别名rune→int32的别名,表示 Unicode 码点uintptr→ 足够存放指针的无符号整数类型
浮点型
float32,float64
复数类型
complex64(由两个float32组成)complex128(由两个float64组成)
3. 字符串类型
-
string
- 由 UTF-8 编码的字节序列组成
- 不可变,一旦创建不能修改
4. 派生/复合类型(常用但不是最基础的)
- 数组 (
[n]T) - 切片 (
[]T) - 字典(map) (
map[K]V) - 结构体(struct)
- 指针(*T)
- 通道(chan T)
- 接口(interface)
- 函数类型(func)
go 语言遍历字符串的方式
1. 按 字节(byte) 遍历
s := "Hello, 世界"
for i := 0; i < len(s); i++ {
fmt.Printf("index: %d, byte: %x\n", i, s[i])
}
s[i]取到的是 单个字节 (uint8)。- 如果字符串里有中文、Emoji 这种多字节字符,遍历出来的只是 UTF-8 编码的每个字节。
2. 按 字符(rune,Unicode 码点) 遍历
s := "Hello, 世界"
for i, r := range s {
fmt.Printf("index: %d, rune: %c\n", i, r)
}
range会自动把字符串解码成 Unicode 码点 (rune,本质是int32)。i是该字符在原始字符串里的 字节索引。r是解码后的 Unicode 码点。- 推荐这种方式来正确处理中文、Emoji。
3. 转换成 []rune 后遍历
s := "Hello, 世界"
runes := []rune(s)
for i, r := range runes {
fmt.Printf("index: %d, rune: %c\n", i, r)
}
- 和
range类似,但能按 字符索引(而不是字节索引)访问。 - 更方便做字符串切割、反转等操作。
byte 和 rune 类型的区别
1. 类型定义
-
byte:uint8的别名,本质上是一个 无符号的 8 位整数,范围是0~255。
常用于表示 原始数据 或者 单个 ASCII 字符。 -
rune:int32的别名,本质上是一个 有符号的 32 位整数,范围是-2^31 ~ 2^31-1。
通常用来表示 一个 Unicode 码点(Unicode Code Point)。
2. 在字符串中的作用
Go 的字符串底层是 只读的 []byte。
- 如果用
byte遍历字符串,拿到的是 UTF-8 编码后的字节序列,不一定能正确对应到“字符”。 - 如果用
rune遍历字符串,Go 会自动解码 UTF-8,拿到的是 完整的 Unicode 字符(码点)。
3. 示例代码
package main
import (
"fmt"
)
func main() {
s := "你好Go"
// 按 byte 遍历
fmt.Println("按 byte 遍历:")
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i])
}
fmt.Println()
// 按 rune 遍历
fmt.Println("按 rune 遍历:")
for _, r := range s {
fmt.Printf("%c ", r)
}
}
输出:
按 byte 遍历:
ä ½ ¥ å ¥½ G o
按 rune 遍历:
你 好 G o
byte遍历时,UTF-8 编码的多字节字符(中文)被拆开,打印结果是乱码。rune遍历时,能正确得到你、好。
4. 总结对比表
| 特性 | byte |
rune |
|---|---|---|
| 本质 | uint8 |
int32 |
| 表示范围 | 0–255 | -2^31 ~ 2^31-1(足够放下所有 Unicode) |
| 适用场景 | 原始字节操作,ASCII 字符 | Unicode 字符,文本处理 |
| 遍历字符串 | 拆开 UTF-8 字节,可能乱码 | 正确解码 UTF-8,得到字符 |
GMP 代表什么 好,这个问题直接切到 Go 调度器的核心 👍
在 Go 的 GMP 调度模型里:
1. 三个核心概念
-
G(Goroutine)
- 就是 Go 里的协程。
- 每个 G 包含执行栈、状态、任务函数等信息。
- G 只是运行的任务单元,本身不会执行。
-
M(Machine)
- 操作系统的线程(内核线程)。
- 真正执行代码的实体。
- M 在执行时必须绑定一个 P,才能运行 goroutine。
-
P(Processor)
- 调度器中的逻辑处理器,负责调度 G 到 M 上运行。
- 每个 P 维护一个本地队列,存放等待执行的 G。
- P 的数量决定了 Go 程序的最大并行度(默认等于 CPU 核数,可用
GOMAXPROCS设置)。
GMP 调度流程
✅ 调度流程
-
创建 goroutine
- 执行
go func()时,Go 运行时会把一个新的 G 对象放入 当前 P 的本地队列。
- 执行
-
创建 Goroutine:
- 当通过 go func() 创建新的 Goroutine 时,G 会首先被加入到与当前 P 关联的本地队列中。
- 如果 P 的本地队列已满(超过 256 个 G),则新的 G 会被放入全局队列。
-
** 调度与执行 **:
- 每个 M 与一个 P 绑定,M 从 P 的本地队列中获取一个 G 来执行。
- 如果 P 的本地队列为空,M 会尝试从全局队列或其他 P 的本地队列中偷取(work stealing)任务执行。
-
系统调用与阻塞:
- 当 G 执行过程中发生阻塞或系统调用,M 也会被阻塞。这时,P 会解绑当前的 M,并尝试寻找或创建新的 M 来继续执行其他 G。阻塞结束后,原来的 M 会尝试重新绑定一个 P 继续执行。
这个问题是 Go 面试/学习里常考的点 👍。我帮你系统整理一下:
go 是值传递还是 引用传递
1. Go 的核心结论
Go 里所有传参本质上都是值传递,没有 C++/Java 那种“引用传递”。
区别在于:有些类型的“值”本身就包含一个指针,所以看起来像“引用传递”。
2. 哪些是典型的值传递
以下类型在赋值、传参时,都会 完整拷贝 值本身:
- 基本数据类型:
int,float,bool,string(注意 string 是不可变的,但传递时 header 还是值拷贝) - 数组
[N]T(会拷贝整个数组内容) - struct(会拷贝整个结构体的字段)
例子:
func changeInt(x int) {
x = 100
}
func main() {
a := 1
changeInt(a)
fmt.Println(a) // 1,没变,因为是值拷贝
}
3. 哪些是“看似引用传递”
这些类型在赋值或传参时,拷贝的是 一个小的 header 结构,里面包含指针字段,所以多个变量共享底层数据:
- slice(包含指针、len、cap)
- map(底层是指针)
- chan(底层是指针)
- interface(包含类型信息和数据指针)
- func(包含代码指针和闭包上下文指针)
例子:
func changeSlice(s []int) {
s[0] = 100
}
func main() {
a := []int{1, 2, 3}
changeSlice(a)
fmt.Println(a) // [100 2 3],改了,因为 slice header 拷贝后仍指向同一个底层数组
}
4. 对比直观理解
| 类型 | 传递方式 | 是否拷贝底层数据 | 表现 |
|---|---|---|---|
| int/float | 值传递 | 是(直接拷贝) | 不会影响外部 |
| array | 值传递 | 是(整个数组) | 不会影响外部 |
| struct | 值传递 | 是(所有字段) | 不会影响外部 |
| string | 值传递 | 否(header 拷贝) | 不可变 |
| slice | 值传递 | 否(header 拷贝,指向同一底层数组) | 会影响外部 |
| map | 值传递 | 否(内部是指针) | 会影响外部 |
| chan | 值传递 | 否(内部是指针) | 会影响外部 |
| func | 值传递 | 否(内部有指针) | 闭包共享环境 |
| interface | 值传递 | 否(内部有指针) | 共享底层对象 |
go 里有哪些数据类型
Go 的 基本数据类型 可以分成几大类来看,面试时常问 👍:
📌 1. 布尔型
bool:取值true/false,默认值false。
📌 2. 数值类型
🔹 整型
-
有符号整数:
int8、int16、int32、int64、int -
无符号整数:
uint8、uint16、uint32、uint64、uint -
特殊:
byte(uint8的别名,表示单个字节)rune(int32的别名,表示一个 Unicode 码点)
🔹 浮点型
float32、float64
🔹 复数
complex64(实部和虚部都是float32)complex128(实部和虚部都是float64)
📌 3. 字符串
string:一串不可变的 UTF-8 字节序列。
📌 4. 派生/复合类型(严格来说不是“基本类型”,但常和基本类型一起讲)
- 指针(
*T) - 数组(
[N]T) - 切片(
[]T) - 映射(
map[K]V) - 通道(
chan T) - 结构体(
struct{...}) - 接口(
interface{...}) - 函数类型(
func(...) ...)
总结
Go 的基本数据类型主要包括:
- 布尔型:
bool - 数值型:整型(
int、uint、int8…)、浮点型(float32、float64)、复数型(complex64、complex128) - 字符串:
string
结论:Go 中的 string 是不可变的(immutable)。
1. 结构层面
Go 的 string 本质上是一个只读的 字节切片视图,定义大致如下(runtime 内部):
type stringStruct struct {
data *byte // 指向底层只读字节数组
len int // 长度
}
data指向底层的只读内存区域。len表示长度。- 没有
cap,也没有写操作的方法。
这意味着 一旦构造出来,string 的底层数据不能被修改。
2. 为什么不可变?
-
安全性
Go 的字符串常常来自只读的常量池(类似 C 语言里的字符串字面量),如果允许修改,会破坏常量池共享。 -
高效性
多个字符串变量可能共享相同底层数据(比如切片、子串操作)。不可变保证了共享时不会出现数据篡改问题。 -
简化内存管理
如果允许修改,需要做 copy-on-write,GC 和 runtime 都会更复杂。
3. 示例
s := "hello"
s[0] = 'H' // ❌ 编译错误: cannot assign to s[0]
b := []byte(s) // ✅ 转换成字节切片后可修改
b[0] = 'H'
s2 := string(b)
fmt.Println(s2) // "Hello"
要修改字符串,必须转换成 []byte 或 []rune,修改后再转回 string。
4. 小结
- Go 的
string是 只读字节序列,不可变。 - 不可变是为了 安全、性能和简化实现。
- 修改字符串的唯一方法:先转
[]byte或[]rune,修改,再转回string。
要不要我帮你画一个 string 和 []byte 内存结构对比图,这样你能更直观地看到为什么 string 不可变?

浙公网安备 33010602011771号