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 通过 ThreadExecutorService
并发编程难度 简单直观 需要锁、同步工具类

👉 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

    • 取值:truefalse
    • 默认值:false

2. 数值类型

整型

  • 有符号整数int8, int16, int32, int64, int

  • 无符号整数uint8, uint16, uint32, uint64, uint

  • 特殊整型

    • byteuint8 的别名
    • runeint32 的别名,表示 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. 类型定义

  • byteuint8 的别名,本质上是一个 无符号的 8 位整数,范围是 0~255
    常用于表示 原始数据 或者 单个 ASCII 字符

  • runeint32 的别名,本质上是一个 有符号的 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 调度流程

✅ 调度流程

  1. 创建 goroutine

    • 执行 go func() 时,Go 运行时会把一个新的 G 对象放入 当前 P 的本地队列
  2. 创建 Goroutine

    • 当通过 go func() 创建新的 Goroutine 时,G 会首先被加入到与当前 P 关联的本地队列中。
    • 如果 P 的本地队列已满(超过 256 个 G),则新的 G 会被放入全局队列。
  3. ** 调度与执行 **:

    • 每个 M 与一个 P 绑定,M 从 P 的本地队列中获取一个 G 来执行。
    • 如果 P 的本地队列为空,M 会尝试从全局队列或其他 P 的本地队列中偷取(work stealing)任务执行。
  4. 系统调用与阻塞:

    • 当 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. 数值类型

🔹 整型

  • 有符号整数:int8int16int32int64int

  • 无符号整数:uint8uint16uint32uint64uint

  • 特殊:

    • byteuint8 的别名,表示单个字节)
    • runeint32 的别名,表示一个 Unicode 码点)

🔹 浮点型

  • float32float64

🔹 复数

  • complex64(实部和虚部都是 float32
  • complex128(实部和虚部都是 float64

📌 3. 字符串

  • string:一串不可变的 UTF-8 字节序列。

📌 4. 派生/复合类型(严格来说不是“基本类型”,但常和基本类型一起讲)

  • 指针(*T
  • 数组([N]T
  • 切片([]T
  • 映射(map[K]V
  • 通道(chan T
  • 结构体(struct{...}
  • 接口(interface{...}
  • 函数类型(func(...) ...

总结
Go 的基本数据类型主要包括:

  • 布尔型:bool
  • 数值型:整型(intuintint8…)、浮点型(float32float64)、复数型(complex64complex128
  • 字符串:string

结论:Go 中的 string 是不可变的(immutable)


1. 结构层面

Go 的 string 本质上是一个只读的 字节切片视图,定义大致如下(runtime 内部):

type stringStruct struct {
    data *byte // 指向底层只读字节数组
    len  int   // 长度
}
  • data 指向底层的只读内存区域。
  • len 表示长度。
  • 没有 cap,也没有写操作的方法。

这意味着 一旦构造出来,string 的底层数据不能被修改


2. 为什么不可变?

  1. 安全性
    Go 的字符串常常来自只读的常量池(类似 C 语言里的字符串字面量),如果允许修改,会破坏常量池共享。

  2. 高效性
    多个字符串变量可能共享相同底层数据(比如切片、子串操作)。不可变保证了共享时不会出现数据篡改问题。

  3. 简化内存管理
    如果允许修改,需要做 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 不可变?

泛型的底层实现原理

posted @ 2025-09-26 22:49  不报异常的空指针  阅读(16)  评论(0)    收藏  举报