Go面试题及详细答案120题(0-20)
《前后端面试题
》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,SQL,Linux… 。
文章目录
- 一、本文面试题目录
- 1. Go语言的主要特点有哪些?与其他语言(如Java、Python)相比有何优势?
- 2. Go语言的变量声明方式有几种?分别是什么?
- 3. 简述Go中`var`和`:=`的区别。
- 4. Go的基本数据类型有哪些?各自的长度是多少?
- 5. 什么是值类型和引用类型?举例说明它们在Go中的区别。
- 6. 数组和切片的区别是什么?切片的底层实现原理是什么?
- 7. 如何初始化一个长度为5、容量为10的切片?
- 8. `make`和`new`的区别是什么?分别适用于哪些场景?
- 9. 简述Go中的map结构,如何判断一个键是否存在于map中?
- 10. Go中的字符串是值类型还是引用类型?字符串能否被修改?
- 11. 什么是指针?Go中指针的使用场景有哪些?
- 12. `defer`语句的作用是什么?它的执行顺序是怎样的?
- 13. `panic`和`recover`的作用是什么?如何使用它们处理错误?
- 14. Go中的函数可以返回多个值吗?如何处理函数返回的错误?
- 15. 什么是类型别名?它与自定义类型有何区别?
- 16. 简述Go中的接口,接口的“隐式实现”是什么意思?
- 17. 空接口(`interface{}`)可以表示什么类型?使用时需要注意什么?
- 18. 什么是类型断言?如何判断类型断言是否成功?
- 19. Go中的`for`循环有几种形式?如何用`for`实现`while`的功能?
- 20. `break`和`continue`在循环中的作用有何不同?如何跳出多层循环?
- 二、120道Go面试题目录列表
一、本文面试题目录
1. Go语言的主要特点有哪些?与其他语言(如Java、Python)相比有何优势?
Go语言(又称Golang)是由Google开发的静态强类型编程语言,其主要特点及与其他语言的优势如下:
主要特点:
- 简洁易学:语法简洁,去除了传统语言中的冗余特性(如继承、泛型早期缺失,后期已支持)。
- 并发原生支持:通过goroutine(轻量级线程)和channel实现高效并发,比Java线程更轻量。
- 内存安全:内置垃圾回收(GC),无需手动管理内存,比C/C++更安全。
- 静态类型:编译时类型检查,比Python等动态类型语言更早发现错误。
- 编译快速:编译速度远快于Java和C++,接近脚本语言的开发体验。
- 跨平台:支持交叉编译,可在一个平台为其他平台编译二进制文件。
- 丰富标准库:内置网络、加密、并发等功能,开箱即用。
与其他语言的优势:
- 对比Java:goroutine比线程更轻量(内存占用约几KB vs MB级),并发模型更简洁;编译速度更快,部署更简单(单二进制文件)。
- 对比Python:静态类型带来更好的性能和代码健壮性;原生支持高并发,适合后端服务。
示例:用goroutine实现简单并发(比Java线程更简洁)
package mainimport ("fmt""time"
)func main() {// 启动10个goroutinefor i := 0; i < 10; i++ {go func(n int) {fmt.Printf("goroutine %d running\n", n)time.Sleep(100 * time.Millisecond)}(i)}time.Sleep(1 * time.Second) // 等待所有goroutine执行
}
2. Go语言的变量声明方式有几种?分别是什么?
Go语言有4种变量声明方式:
-
使用
var
关键字声明(完整形式)
语法:var 变量名 类型 = 值
示例:var age int = 25 var name string = "Alice"
-
var
声明并自动推导类型
省略类型,编译器根据赋值自动推断类型:var height = 1.75 // 自动推导为float64 var isStudent = true // 自动推导为bool
-
var
批量声明
在一个var
块中声明多个变量:var (id int = 1001score float64 = 95.5pass bool = true )
-
短变量声明(
:=
)
只能在函数内部使用,省略var
和类型,通过赋值自动推导:func main() {count := 10 // 推导为intmessage := "Hi" // 推导为string }
注意::=
要求至少有一个变量是新声明的,否则会报错:
a := 10
a := 20 // 错误:no new variables on left side of :=
3. 简述Go中var
和:=
的区别。
var
和:=
是Go中两种变量声明方式,核心区别如下:
特性 | var | := (短变量声明) |
---|---|---|
使用范围 | 可在函数内、外使用 | 仅能在函数内部使用 |
类型指定 | 可显式指定类型或自动推导 | 必须通过赋值自动推导类型 |
初始化要求 | 可声明不初始化(有默认零值) | 必须初始化(右侧必须有值) |
多变量声明 | 支持批量声明(var 块) | 支持同时声明多个变量 |
重复声明 | 不可重复声明同一变量 | 允许部分变量重复(需至少一个新变量) |
示例对比:
// var的使用
var a int // 声明不初始化,默认0
var b = "test" // 自动推导类型// :=的使用(仅函数内)
func main() {c := 3.14 // 必须初始化,推导为float64d, e := 10, "hello" // 多变量声明d = 20 // 允许重新赋值d, f := 30, true // 合法:f是新变量
}
4. Go的基本数据类型有哪些?各自的长度是多少?
Go的基本数据类型分为四大类,长度如下(单位:字节):
-
整数类型
int
:与平台相关(32位系统4字节,64位系统8字节)int8
:1字节(范围:-128 ~ 127)int16
:2字节(-32768 ~ 32767)int32
:4字节(-2¹⁰ ~ 2¹⁰-1)int64
:8字节(-2³¹ ~ 2³¹-1)- 无符号整数:
uint
、uint8
(byte)、uint16
、uint32
、uint64
、uintptr
(指针类型,长度与平台相关)
-
浮点类型
float32
:4字节(精度约6位小数)float64
:8字节(精度约15位小数,默认浮点类型)
-
复数类型
complex64
:8字节(由两个float32组成)complex128
:16字节(由两个float64组成,默认复数类型)
-
其他基本类型
bool
:1字节(值为true
或false
)string
:长度不固定(存储UTF-8编码的字符串)byte
:uint8的别名(1字节,用于表示ASCII字符)rune
:int32的别名(4字节,用于表示Unicode码点)
示例:
package mainimport "fmt"func main() {var a int = 42var b float64 = 3.14159var c bool = truevar d string = "Hello"var e rune = '中' // Unicode码点,占4字节fmt.Printf("int: %d字节\n", sizeof(a))fmt.Printf("float64: %d字节\n", sizeof(b))fmt.Printf("rune: %d字节\n", sizeof(e))
}// 辅助函数:打印变量占用字节数
func sizeof(v interface{}) int {return int(unsafe.Sizeof(v))
}
5. 什么是值类型和引用类型?举例说明它们在Go中的区别。
值类型:变量直接存储值,赋值或传参时会复制整个值。
引用类型:变量存储的是值的内存地址(指针),赋值或传参时复制的是地址,多个变量指向同一块内存。
Go中的值类型:
- 基本类型:
int
、float
、bool
、string
、rune
、byte
- 复合类型:数组、结构体(
struct
)
Go中的引用类型:
- 切片(
slice
)、映射(map
)、通道(channel
)、指针(pointer
)、接口(interface
)
核心区别示例:
package mainimport "fmt"func main() {// 1. 值类型示例(int)a := 10b := a // 复制值b = 20fmt.Println(a, b) // 输出:10 20(a不受b影响)// 2. 引用类型示例(切片)s1 := []int{1, 2, 3}s2 := s1 // 复制引用(地址)s2[0] = 100fmt.Println(s1, s2) // 输出:[100 2 3] [100 2 3](共享底层数据)// 3. 数组(值类型)vs 切片(引用类型)arr1 := [3]int{1, 2, 3}arr2 := arr1 // 复制整个数组arr2[0] = 100fmt.Println(arr1, arr2) // 输出:[1 2 3] [100 2 3](arr1不受影响)
}
总结:值类型操作的是副本,修改不影响原变量;引用类型操作的是同一份数据,修改会影响所有引用该地址的变量。
6. 数组和切片的区别是什么?切片的底层实现原理是什么?
数组和切片的区别:
特性 | 数组(Array) | 切片(Slice) |
---|---|---|
长度 | 固定,声明时必须指定 | 可变,可动态扩容 |
类型 | 包含长度(如[3]int 与[4]int 是不同类型) | 不包含长度(如[]int ) |
初始化 | 需指定长度或通过元素数量推断 | 可通过make() 或数组/切片派生 |
传递方式 | 值传递(复制整个数组) | 引用传递(复制底层数组指针) |
零值 | 各元素为对应类型零值 | nil (长度和容量为0,无底层数组) |
切片的底层实现原理:
切片是对数组的抽象,其内部结构包含三个字段(源码定义):
type slice struct {array unsafe.Pointer // 指向底层数组的指针len int // 切片长度(当前元素个数)cap int // 切片容量(底层数组的长度)
}
关键特性:
- 切片本身不存储数据,而是引用底层数组。
- 当切片长度超过容量时,会触发扩容(创建新数组,复制原数据)。
- 多个切片可引用同一底层数组,修改切片元素会影响共享该数组的其他切片。
示例:
package mainimport "fmt"func main() {// 数组(长度固定)arr := [5]int{1, 2, 3, 4, 5}// 切片(引用数组的一部分)s := arr[1:3] // 长度2,容量4(从索引1到数组末尾)fmt.Printf("s: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // [2 3], 2, 4// 修改切片元素会影响原数组s[0] = 200fmt.Println(arr) // [1 200 3 4 5]// 切片扩容(超过容量时)s = append(s, 6, 7, 8) // 原容量4,添加3个元素后总长度5,触发扩容fmt.Printf("扩容后: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // [200 3 6 7 8], 5, 8
}
7. 如何初始化一个长度为5、容量为10的切片?
在Go中,可通过以下两种方式初始化长度为5、容量为10的切片:
-
使用
make()
函数
语法:make(切片类型, 长度, 容量)
示例:s := make([]int, 5, 10) fmt.Printf("len: %d, cap: %d\n", len(s), cap(s)) // 输出:len: 5, cap: 10 fmt.Println(s) // 输出:[0 0 0 0 0](初始值为int的零值)
-
通过数组或切片派生
先创建容量足够的数组/切片,再截取指定长度:// 方式1:从数组派生 arr := [10]int{} s1 := arr[:5] // 截取前5个元素,长度5,容量10// 方式2:从更大的切片派生 s2 := make([]int, 10) s3 := s2[:5] // 长度5,容量10
注意:初始化后,切片的前5个元素可直接访问(默认零值),而访问索引5~9会导致越界错误,需通过append()
扩展长度后使用:
s := make([]int, 5, 10)
s[0] = 1 // 合法
// s[5] = 6 // 错误:index out of range [5] with length 5s = append(s, 6, 7, 8) // 长度变为8,仍在容量范围内
s[5] = 6 // 此时合法
8. make
和new
的区别是什么?分别适用于哪些场景?
make
和new
都是Go中用于内存分配的内置函数,但用途和行为不同:
特性 | make | new |
---|---|---|
作用 | 初始化引用类型(创建并初始化) | 分配值类型内存(仅分配,不初始化) |
返回值 | 返回类型本身(已初始化) | 返回指向类型的指针(*T ) |
适用类型 | 切片(slice )、映射(map )、通道(channel ) | 所有类型(主要用于值类型,如int 、struct 等) |
零值处理 | 初始化为类型的“可用零值”(如切片的空结构) | 初始化内存为类型的零值(如int 的0) |
make
的使用场景:
用于创建需要预初始化的引用类型,确保其可以直接使用:
// 切片(指定长度和容量)
s := make([]int, 5, 10)// 映射(必须用make初始化才能使用)
m := make(map[string]int)
m["age"] = 25 // 直接使用,无需额外初始化// 通道
ch := make(chan int, 5) // 带缓冲的通道
new
的使用场景:
用于分配值类型的内存,返回指针,适用于需要指针的场景:
// 基本类型
num := new(int)
*num = 10 // 需要解引用赋值// 结构体
type Person struct {Name stringAge int
}
p := new(Person)
p.Name = "Alice" // 结构体指针可直接访问字段(语法糖)
注意:new
对引用类型的作用有限,例如new([]int)
会返回*[]int
(指向nil切片的指针),仍需手动初始化:
sPtr := new([]int)
// *sPtr = make([]int, 5) // 必须手动初始化才能使用
9. 简述Go中的map结构,如何判断一个键是否存在于map中?
Go中的map结构:
map是一种无序的键值对(key-value)集合,类似其他语言中的字典或哈希表。其特点如下:
- 键必须是支持相等运算符(
==
、!=
)的类型(如int
、string
、指针等),切片、map、函数等不可作为键。 - 值可以是任意类型。
- 底层通过哈希表实现,查找、插入、删除的平均时间复杂度为O(1)。
- map是引用类型,赋值或传参时传递的是引用(地址)。
声明与初始化:
map必须初始化后才能使用(通常用make()
):
// 声明并初始化
m1 := make(map[string]int)// 字面量初始化
m2 := map[string]int{"apple": 5,"banana": 3,
}
判断键是否存在:
通过map访问键时,可返回两个值:value, ok := m[key]
,其中ok
是布尔值,表示键是否存在:
package mainimport "fmt"func main() {m := map[string]int{"apple": 5, "banana": 3}// 判断"apple"是否存在if val, ok := m["apple"]; ok {fmt.Printf("apple exists: %d\n", val) // 输出:apple exists: 5}// 判断"orange"是否存在if _, ok := m["orange"]; !ok {fmt.Println("orange does not exist") // 输出:orange does not exist}
}
注意:如果直接访问不存在的键,会返回值类型的零值(如int
返回0),因此不能通过返回值是否为零值来判断键是否存在。
10. Go中的字符串是值类型还是引用类型?字符串能否被修改?
Go中的字符串是值类型,但具有特殊的存储机制:
- 字符串的底层是一个只读的字节数组(
[]byte
),字符串变量存储的是该数组的指针和长度。 - 赋值或传参时,复制的是指针和长度(而非整个字节数组),因此效率较高。
- 虽然复制成本低,但字符串本身仍是值类型(变量存储的是“值”——指针和长度)。
字符串不可修改:
Go中的字符串是只读的,不能直接修改其内容。任何看似修改的操作,实际上都会创建一个新的字符串:
package mainimport "fmt"func main() {s := "hello"// s[0] = 'H' // 错误:cannot assign to s[0](字符串不可修改)// 若需修改,需先转换为字节切片,修改后再转回字符串b := []byte(s)b[0] = 'H's = string(b) // 创建新字符串fmt.Println(s) // 输出:Hello
}
原理:字符串的底层字节数组被设计为只读,多个字符串可共享同一份底层数据(如字符串切片):
s1 := "hello world"
s2 := s1[0:5] // s2与s1共享底层字节数组
这种设计既保证了字符串的安全性(不可修改),又兼顾了性能(避免不必要的复制)。
11. 什么是指针?Go中指针的使用场景有哪些?
指针是存储另一个变量内存地址的变量。通过指针可以间接访问或修改所指向变量的值。
Go中指针的声明方式为*类型
,取地址用&
,解引用用*
:
var a int = 10
var p *int = &a // p是指向int的指针,存储a的地址
fmt.Println(*p) // 解引用,输出:10
*p = 20 // 通过指针修改a的值
fmt.Println(a) // 输出:20
Go中指针的使用场景:
-
修改函数外部变量
函数参数默认是值传递,通过指针可让函数修改外部变量:func increment(p *int) {*p++ }func main() {x := 5increment(&x)fmt.Println(x) // 输出:6 }
-
传递大型数据结构
对于结构体等大型数据,传递指针可避免值复制的性能开销:type LargeStruct struct {Data [10000]int }// 传递指针,避免复制整个结构体 func process(s *LargeStruct) {s.Data[0] = 100 }
-
实现数据共享
多个指针可指向同一变量,实现数据共享(如链表、树等数据结构):type Node struct {Val intNext *Node // 指向另一个Node的指针 }
-
区分“存在零值”和“未设置”
对于可能有零值的类型(如int
的0),可用指针的nil
表示“未设置”:func getValue() *int {// 某些条件下返回nil,表示未找到值return nil }
注意:Go不支持指针运算(如p++
),比C/C++的指针更安全。
12. defer
语句的作用是什么?它的执行顺序是怎样的?
defer
语句的作用:
用于延迟执行函数调用,通常用于释放资源、关闭连接等清理操作,确保代码在函数退出前(无论正常返回还是异常 panic)执行。
常见使用场景:
- 关闭文件
- 释放锁
- 关闭网络连接
示例:
package mainimport "fmt"func main() {fmt.Println("start")defer fmt.Println("deferred 1") // 延迟执行defer fmt.Println("deferred 2") // 延迟执行fmt.Println("end")
}
输出:
start
end
deferred 2
deferred 1
执行顺序:
defer
语句在声明时会立即计算函数参数,但不会执行函数体。- 多个
defer
按“后进先出”(LIFO)顺序执行,即最后声明的defer
最先执行。 defer
在函数返回前执行,无论函数是正常返回、panic
还是被return
语句终止。
进阶示例(参数即时计算):
func main() {i := 0defer fmt.Println(i) // 参数i在声明时计算(值为0)i = 10fmt.Println(i) // 输出:10
}
// 输出:
// 10
// 0
defer
与匿名函数结合:
可用于捕获函数返回值或修改返回值:
func f() int {i := 5defer func() { i++ }() // 延迟执行匿名函数return i // 返回5(i的值在return时确定)
}func main() {fmt.Println(f()) // 输出:5(匿名函数在return后执行,不影响返回值)
}
13. panic
和recover
的作用是什么?如何使用它们处理错误?
panic
:用于触发程序异常(类似其他语言的“抛出异常”),会立即终止当前函数执行,并沿调用栈向上传播,直到被recover
捕获或导致程序退出。
recover
:用于捕获panic
引发的异常,只能在defer
语句中使用,返回panic
传递的值,若没有panic
则返回nil
。
使用场景:
处理不可恢复的错误(如程序逻辑错误),或在顶层捕获异常以避免程序崩溃。
基本用法示例:
package mainimport "fmt"func riskyOperation() {panic("something went wrong") // 触发异常
}func main() {defer func() {if err := recover(); err != nil {// 捕获panic,打印错误信息fmt.Printf("recovered from: %v\n", err)}}()riskyOperation()fmt.Println("this line will not execute") // panic后不会执行
}
// 输出:recovered from: something went wrong
注意事项:
recover
必须在defer
语句中使用,否则无效。panic
会终止当前函数,但会先执行该函数中的所有defer
语句。- 不推荐用
panic
/recover
处理预期错误(如文件不存在),这类错误应通过函数返回值处理。
多层调用中的panic
传播:
func a() {defer fmt.Println("a's defer")b()
}func b() {defer fmt.Println("b's defer")panic("error in b")
}func main() {defer func() { recover() }()a()
}
// 输出:
// b's defer
// a's defer
14. Go中的函数可以返回多个值吗?如何处理函数返回的错误?
Go中的函数支持返回多个值,这是Go的特色功能之一,常用于同时返回结果和错误信息。
基本语法:
// 声明返回多个值的函数
func divide(a, b float64) (float64, error) {if b == 0 {return 0, fmt.Errorf("division by zero")}return a / b, nil
}
处理返回的错误:
Go推荐通过返回值显式处理错误(而非异常),通常将错误作为最后一个返回值,约定nil
表示无错误:
package mainimport "fmt"func main() {result, err := divide(10, 2)if err != nil {// 错误处理fmt.Println("Error:", err)return}// 无错误,使用结果fmt.Println("Result:", result) // 输出:Result: 5// 测试错误情况result, err = divide(5, 0)if err != nil {fmt.Println("Error:", err) // 输出:Error: division by zeroreturn}
}
命名返回值:
函数可声明返回值的名称,在函数体内直接使用,return时可省略返回值列表:
func calculate(a, b int) (sum, product int) {sum = a + bproduct = a * breturn // 隐式返回sum和product
}
总结:多返回值使Go的错误处理清晰直观,通过判断错误返回值是否为nil
,可明确处理成功和失败的情况,避免了其他语言中try/catch块的嵌套问题。
15. 什么是类型别名?它与自定义类型有何区别?
类型别名是给已存在的类型起一个新名字,语法为:type 别名 = 原类型
。
自定义类型是创建一个全新的类型,语法为:type 新类型 原类型
。
区别对比:
特性 | 类型别名(Type Alias) | 自定义类型(Custom Type) |
---|---|---|
本质 | 原类型的另一个名字,与原类型等价 | 全新的类型,与原类型不同 |
兼容性 | 可与原类型直接转换(无需显式转换) | 与原类型不兼容,需显式转换 |
方法绑定 | 不能为类型别名定义方法(方法属于原类型) | 可直接为自定义类型定义方法 |
示例:
package mainimport "fmt"// 自定义类型:创建全新类型MyInt
type MyInt int// 类型别名:IntAlias是int的别名
type IntAlias = int// 可为自定义类型定义方法
func (m MyInt) Double() MyInt {return m * 2
}func main() {var a int = 10var b MyInt = 20var c IntAlias = 30// 类型别名与原类型兼容a = c // 合法:IntAlias与int等价c = a // 合法// 自定义类型与原类型不兼容(需显式转换)// a = b // 错误:cannot use b (variable of type MyInt) as int value in assignmenta = int(b) // 合法:显式转换// 调用自定义类型的方法fmt.Println(b.Double()) // 输出:40
}
类型别名的典型用途:
- 简化复杂类型(如
type IntSlice = []int
) - 在不同包之间兼容类型
- 重构时平滑过渡类型
自定义类型的典型用途:
- 封装数据和行为(面向对象风格)
- 区分语义不同但底层类型相同的值(如
type Meter int
和type Foot int
)
16. 简述Go中的接口,接口的“隐式实现”是什么意思?
Go中的接口是一种抽象类型,定义了一组方法签名(只有方法声明,没有实现),用于描述某个对象的行为。接口不关心实现者的具体类型,只关心其是否具备特定方法。
接口定义语法:
// 定义接口
type Shape interface {Area() float64 // 计算面积的方法Perimeter() float64 // 计算周长的方法
}
“隐式实现”的含义:
Go中实现接口无需显式声明(如Java的implements
关键字),只需类型实现了接口的所有方法,就自动视为实现了该接口。这种特性称为“隐式实现”或“鸭子类型”(“如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子”)。
示例:
package mainimport "fmt"// 接口定义
type Shape interface {Area() float64
}// 圆类型(隐式实现Shape接口)
type Circle struct {Radius float64
}// 实现Shape接口的Area方法
func (c Circle) Area() float64 {return 3.14 * c.Radius * c.Radius
}// 矩形类型(隐式实现Shape接口)
type Rectangle struct {Width, Height float64
}// 实现Shape接口的Area方法
func (r Rectangle) Area() float64 {return r.Width * r.Height
}// 接收Shape接口的函数
func PrintArea(s Shape) {fmt.Printf("Area: %.2f\n", s.Area())
}func main() {c := Circle{Radius: 5}r := Rectangle{Width: 4, Height: 6}// Circle和Rectangle都隐式实现了Shape,可直接传递PrintArea(c) // 输出:Area: 78.50PrintArea(r) // 输出:Area: 24.00
}
优势:
隐式实现降低了接口与实现者之间的耦合,使代码更灵活,便于扩展和重构。
17. 空接口(interface{}
)可以表示什么类型?使用时需要注意什么?
空接口(interface{}
) 是没有定义任何方法的接口,因此所有类型都隐式实现了空接口,可以表示任意类型的值。
使用场景:
- 存储任意类型的数据(如
map[string]interface{}
) - 实现通用函数(可接收任意类型参数)
- 作为函数返回值(返回任意类型)
示例:
package mainimport "fmt"// 接收空接口参数(可接收任意类型)
func printAny(v interface{}) {fmt.Println(v)
}func main() {// 空接口变量可存储任意类型var i interface{}i = 42 // 存储inti = "hello" // 存储stringi = []int{1, 2} // 存储切片// 通用函数printAny(100) // 输出:100printAny("world") // 输出:worldprintAny(map[int]string{1: "one"}) // 输出:map[1:one]// 空接口切片(可包含多种类型)data := []interface{}{1, "two", 3.14, true}fmt.Println(data) // 输出:[1 two 3.14 true]
}
使用空接口的注意事项:
- 类型安全:空接口会丢失类型信息,直接操作可能导致运行时错误,需通过类型断言恢复类型。
- 性能开销:空接口的底层实现包含类型信息和值指针,操作时可能有额外的性能开销。
- 避免过度使用:滥用空接口会降低代码的可读性和安全性,应优先使用具体类型或泛型。
错误示例(未进行类型断言):
var i interface{} = "hello"
// fmt.Println(i + " world") // 错误:invalid operation: i + " world" (mismatched types interface{} and string)
18. 什么是类型断言?如何判断类型断言是否成功?
类型断言是用于将空接口(interface{}
)转换为具体类型的操作,语法为:value, ok := 接口变量.(目标类型)
。
作用:
空接口可存储任意类型,但使用时需知道其具体类型才能正确操作,类型断言就是用于“恢复”具体类型的机制。
判断类型断言是否成功:
类型断言返回两个值:
- 第一个值是转换后的具体类型的值(若成功)或目标类型的零值(若失败)。
- 第二个值(
ok
)是布尔值,true
表示断言成功,false
表示失败。
示例:
package mainimport "fmt"func main() {var i interface{} = "hello"// 成功的类型断言if s, ok := i.(string); ok {fmt.Printf("'%s' is a string\n", s) // 输出:'hello' is a string}// 失败的类型断言if num, ok := i.(int); ok {fmt.Printf("%d is an int\n", num)} else {fmt.Println("i is not an int") // 输出:i is not an int}// 对非空接口使用类型断言var s interface{} = 100// 直接断言(不判断ok,失败会panic)num := s.(int)fmt.Println(num + 50) // 输出:150
}
类型断言与switch
结合:
使用type switch
可高效判断空接口的具体类型:
func checkType(v interface{}) {switch t := v.(type) {case int:fmt.Printf("It's an int: %d\n", t)case string:fmt.Printf("It's a string: %s\n", t)case bool:fmt.Printf("It's a bool: %v\n", t)default:fmt.Printf("Unknown type: %T\n", t)}
}func main() {checkType(42) // 输出:It's an int: 42checkType("hi") // 输出:It's a string: hicheckType(true) // 输出:It's a bool: truecheckType(3.14) // 输出:Unknown type: float64
}
注意:若不判断ok
而直接进行类型断言,失败时会触发panic
,因此建议始终使用ok
判断。
19. Go中的for
循环有几种形式?如何用for
实现while
的功能?
Go中只有for
一种循环语句,但支持三种形式,可替代其他语言的for
、while
、do-while
:
-
基本形式(类似C的for)
语法:for 初始化; 条件; 后处理 { ... }
示例:for i := 0; i < 5; i++ {fmt.Println(i) }
-
条件循环(类似while)
语法:for 条件 { ... }
(省略初始化和后处理)
示例(实现while功能):i := 0 for i < 5 {fmt.Println(i)i++ }
-
无限循环(类似for(;😉)
语法:for { ... }
(无任何条件),需配合break
退出
示例:i := 0 for {if i >= 5 {break}fmt.Println(i)i++ }
-
遍历循环(for range)
用于遍历数组、切片、map、字符串、通道等:// 遍历切片 nums := []int{1, 2, 3} for index, value := range nums {fmt.Printf("index: %d, value: %d\n", index, value) }// 遍历map m := map[string]int{"a": 1, "b": 2} for key, val := range m {fmt.Printf("key: %s, val: %d\n", key, val) }
用for
实现while
功能:
Go中没有while
关键字,直接使用for 条件
形式即可实现等效功能:
// 实现while (condition) { ... }
count := 0
for count < 3 {fmt.Println("count:", count)count++
}
// 输出:
// count: 0
// count: 1
// count: 2
实现do-while
功能:
通过无限循环+条件判断实现(先执行一次,再判断条件):
// 实现do { ... } while (condition)
i := 0
for {fmt.Println(i)i++if i >= 3 {break}
}
// 输出:
// 0
// 1
// 2
20. break
和continue
在循环中的作用有何不同?如何跳出多层循环?
break
和continue
的区别:
break
:立即终止当前循环,跳出循环体,执行循环后的代码。continue
:跳过当前循环的剩余语句,直接进入下一次循环的判断条件。
示例对比:
package mainimport "fmt"func main() {// break示例:遇到3终止循环fmt.Println("break example:")for i := 0; i < 5; i++ {if i == 3 {break}fmt.Println(i)}// 输出:0 1 2// continue示例:跳过3fmt.Println("\ncontinue example:")for i := 0; i < 5; i++ {if i == 3 {continue}fmt.Println(i)}// 输出:0 1 2 4
}
跳出多层循环的方法:
Go中可通过标签(label) 配合break
跳出多层循环:
- 在外层循环前定义标签(
label:
)。 - 在需要跳出的地方使用
break label
。
示例:
package mainimport "fmt"func main() {// 定义外层循环标签outerLoop:for i := 0; i < 3; i++ {for j := 0; j < 3; j++ {fmt.Printf("i=%d, j=%d\n", i, j)if i == 1 && j == 1 {break outerLoop // 跳出外层循环}}}fmt.Println("Loop exited")
}
// 输出:
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// i=1, j=1
// Loop exited
注意:continue
也可配合标签使用,用于跳过外层循环的当前迭代,进入下一次外层循环:
outerLoop:
for i := 0; i < 3; i++ {for j := 0; j < 3; j++ {if j == 1 {continue outerLoop // 跳过外层循环的当前迭代}fmt.Printf("i=%d, j=%d\n", i, j)}
}
// 输出:
// i=0, j=0
// i=1, j=0
// i=2, j=0
二、120道Go面试题目录列表
文章序号 | Go面试题120道 |
---|---|
1 | Go面试题及详细答案120道(01-20) |
2 | Go面试题及详细答案120道(21-40) |
3 | Go面试题及详细答案120道(41-60) |
4 | Go面试题及详细答案120道(61-80) |
5 | Go面试题及详细答案120道(81-100) |
6 | Go面试题及详细答案120道(101-120) |