变量和常量 - Golang基础面试题
本章包含Golang变量和常量相关的核心面试题和知识点。
1:变量声明的几种方式
Go语言提供了多种变量声明方式:
1. 使用var关键字
go
// 声明单个变量
var name string
var age int
var isActive bool
// 声明多个变量
var name, email string
var age, score int
// 声明并初始化
var name string = "Alice"
var age int = 30
// 类型推断
var name = "Alice" // 自动推断为string类型
var age = 30 // 自动推断为int类型
// 批量声明
var (
name string = "Alice"
age int = 30
email string = "alice@example.com"
)2. 短变量声明(:=)
:= 不能在包级别使用,包级别只能用var
go
// 短变量声明(只能在函数内使用)
name := "Alice"
age := 30
isActive := true
// 多变量声明
name, email := "Alice", "alice@example.com"
x, y, z := 1, 2, 32. new函数和make函数
核心区别对比表
| 特性 | new | make |
|---|---|---|
| 核心目的 | 分配内存,返回地址 | 初始化复杂结构,立即可用 |
| 适用类型 | 所有类型 | 仅slice、map、channel |
| 返回值 | 指针(*T) | 类型本身(T) |
| 初始化 | 零值初始化 | 完整初始化 |
| 是否可用 | 引用类型不可用(nil),需要使用(*v) | 立即可用 |
new
- 对于slice类型
- 因为Data是内置指针类型,所以为nil,导致不能直接使用
- 但是len和cap是0,可以查到,但是直接使用索引不会空指针,这是slice的安全保障
- 且append之后也能正常使用
- 对于map和chan,完全不可用
make
- slice:分配+初始化,返回可用slice
- map:创建hash表结构
- chan:创建通道+缓冲区+锁
快速判断口诀:
go
// ✅ 想要指针 → 用 new (但结构体更推荐 &T{})
p := new(int)
// ✅ 想要立即可用的 slice/map/channel → 必须用 make
s := make([]int, 0)
m := make(map[string]int)
ch := make(chan int)go
// new的设计目的:统一的零值内存分配
// 优点:
// 1. 语义清晰:分配内存并返回指针
// 2. 适用于所有类型
// 3. 保证返回的内存已零值初始化
package main
import "fmt"
func demonstrateNew() {
// new为类型分配零值内存,返回指向该类型的指针
// 基本类型
intPtr := new(int)
fmt.Printf("new(int): %T, 值: %d, 地址: %p\n", intPtr, *intPtr, intPtr)
// 输出: new(int): *int, 值: 0, 地址: 0xc000014098
// 结构体
type Person struct {
Name string
Age int
}
personPtr := new(Person)
fmt.Printf("new(Person): %T, 值: %+v\n", personPtr, *personPtr)
// 输出: new(Person): *Person, 值: {Name: Age:0}
// slice(注意:new创建的slice是nil),因为其中的Data字段是nil,所以无法直接使用
slicePtr := new([]int)
fmt.Printf("new([]int): %T, 值: %v, 是否为nil: %t\n",
slicePtr, *slicePtr, *slicePtr == nil)
// map(注意:new创建的map是nil),hash结构未初始化,无法使用
mapPtr := new(map[string]int)
fmt.Printf("new(map): %T, 值: %v, 是否为nil: %t\n",
mapPtr, *mapPtr, *mapPtr == nil)
}go
// make的设计目的:初始化引用类型的内部数据结构
// slice、map、channel是引用类型,它们有复杂的内部结构:
// - slice: 需要初始化指向底层数组的指针、长度、容量
// - map: 需要初始化hash表、bucket等数据结构
// - channel: 需要初始化缓冲区、锁等同步机制
package main
import "fmt"
func demonstrateMake() {
// make用于创建slice、map、channel,返回初始化后的类型本身
// 1. slice
// make([]T, len, cap)
slice1 := make([]int, 5) // 长度5,容量5
slice2 := make([]int, 5, 10) // 长度5,容量10
fmt.Printf("slice1: len=%d, cap=%d, 值=%v\n",
len(slice1), cap(slice1), slice1)
// 输出: slice1: len=5, cap=5, 值=[0 0 0 0 0]
fmt.Printf("slice2: len=%d, cap=%d, 值=%v\n",
len(slice2), cap(slice2), slice2)
// 输出: slice2: len=5, cap=10, 值=[0 0 0 0 0]
slice1[0] = 100 // 可以直接使用
fmt.Println("修改后的slice1:", slice1)
// 2. map
// make(map[K]V, initialCapacity)
map1 := make(map[string]int) // 不指定容量
map2 := make(map[string]int, 10) // 预分配容量
map1["key"] = 100 // 可以直接使用
fmt.Printf("map1: %v\n", map1)
// 输出: map1: map[key:100]
// 3. channel
// make(chan T, bufferSize)
ch1 := make(chan int) // 无缓冲channel
ch2 := make(chan int, 5) // 缓冲大小为5的channel
go func() {
ch2 <- 42 // 可以直接使用
}()
fmt.Printf("从channel接收: %d\n", <-ch2)
// 输出: 从channel接收: 42
close(ch1)
close(ch2)
}3. 实际对比示例
go
package main
func wrongUsage() {
// ❌ 错误:使用new创建map
mapPtr := new(map[string]int)
// *mapPtr 是 nil,不能直接使用
// (*mapPtr)["key"] = 1 // panic: assignment to entry in nil map
// ❌ 错误:使用new创建slice
slicePtr := new([]int)
// *slicePtr 是 nil,不能直接使用
// (*slicePtr)[0] = 1 // panic: index out of range
// ❌ 错误:使用new创建channel
chPtr := new(chan int)
// *chPtr 是 nil,不能直接使用
// *chPtr <- 1 // panic: send on nil channel
}go
package main
func correctUsage() {
// ✅ 正确:使用make创建map
m := make(map[string]int)
m["key"] = 1 // 可以直接使用
// ✅ 正确:使用make创建slice
s := make([]int, 5)
s[0] = 1 // 可以直接使用
// ✅ 正确:使用make创建channel
ch := make(chan int, 1)
ch <- 1 // 可以直接使用
<-ch
close(ch)
// ✅ 正确:使用new创建基本类型或结构体
intPtr := new(int)
*intPtr = 42
type Person struct {
Name string
}
personPtr := new(Person)
personPtr.Name = "Alice"
}5. 使用建议
go
// ✅ 何时使用new:
// 1. 创建基本类型的指针
ptr := new(int)
// 2. 创建结构体指针(但通常更推荐使用 &T{} 语法)
type Config struct {
Host string
Port int
}
config1 := new(Config) // 使用new
config2 := &Config{} // 更推荐:使用取地址符
config3 := &Config{ // 最推荐:初始化时赋值
Host: "localhost",
Port: 8080,
}
// ✅ 何时使用make:
// 1. 创建slice(必须使用make或字面量)
s := make([]int, 0, 10)
// 2. 创建map(必须使用make或字面量)
m := make(map[string]int)
// 3. 创建channel(必须使用make)
ch := make(chan int, 5)
// ❌ 永远不要:
// 1. 对slice/map/channel使用new
badMap := new(map[string]int) // 错误:得到nil map
// 2. 对基本类型或结构体使用make
// badInt := make(int) // 编译错误:make只能用于slice/map/channel常见面试追问
Q1: 为什么make不返回指针?
go
// 因为slice、map、channel本身就是引用类型
// 它们内部已经包含了指向底层数据结构的指针
// 再返回指针就是"指针的指针",没有必要
s := make([]int, 5) // s 本身就包含指向底层数组的指针
m := make(map[string]int) // m 本身就是指向hash表的指针
ch := make(chan int) // ch 本身就是指向channel结构的指针Q2: 能否用var声明代替make?
go
// 不能完全代替!
var s []int // s 是 nil slice,但可以append
var m map[string]int // m 是 nil map,不能赋值!会panic
var ch chan int // ch 是 nil channel,不能使用!
// 对比make:
s2 := make([]int, 0) // 非nil slice,已分配内存
m2 := make(map[string]int) // 非nil map,可以直接使用
ch2 := make(chan int) // 非nil channel,可以直接使用Q3: new和直接声明有什么区别?
go
// 功能上几乎没有区别
var i1 int // 零值初始化
i2 := new(int) // 零值初始化,但返回指针
// 主要区别:
// 1. new返回指针,var返回值
// 2. 需要指针时用new更简洁
// 3. 但&T{}语法更常用面试要点记忆
| 函数 | 用途 | 返回值 | 使用场景 |
|---|---|---|---|
| new(T) | 分配零值内存 | *T | 需要类型指针时 |
| make(T, args) | 初始化引用类型 | T | 创建slice/map/channel |
| &T{} | 创建并初始化 | *T | 创建结构体指针(推荐) |
| T{} | 创建值 | T | 创建普通值 |
3:Go的零值概念
Go语言为每种类型都定义了零值,变量在声明时如果没有明确初始化,会自动设置为零值:
点击查看完整代码实现
go
package main
import "fmt"
func demonstrateZeroValues() {
var i int // 0
var f float64 // 0
var b bool // false
var s string // ""
var p *int // nil
// 复杂类型的零值
var slice []int // nil
var m map[string]int // nil
var ch chan int // nil
var fn func() // nil
// 结构体的零值是所有字段都为零值
type Person struct {
Name string
Age int
}
var person Person // {Name: "", Age: 0}
fmt.Printf("int: %d\n", i) // 0
fmt.Printf("float64: %f\n", f) // 0.000000
fmt.Printf("bool: %t\n", b) // false
fmt.Printf("string: %q\n", s) // ""
fmt.Printf("pointer: %v\n", p) // <nil>
fmt.Printf("slice: %v\n", slice) // []
fmt.Printf("map: %v\n", m) // map[]
fmt.Printf("channel: %v\n", ch) // <nil>
fmt.Printf("func: %v\n", fn) // <nil>
fmt.Printf("struct: %+v\n", person) // {Name: Age:0}
}零值的实用性
- 安全的默认状态
go
func safeCounter() {
var count int // 零值为0,可以直接使用
count++ // 安全地递增
fmt.Println(count) // 输出1
}- 结构体的零值可用性
go
type Buffer struct {
data []byte
pos int
}
func (b *Buffer) Write(p []byte) {
// 即使Buffer是零值,也可以正常工作
b.data = append(b.data, p...)
}
func main() {
var buf Buffer // 零值状态
buf.Write([]byte("hello")) // 可以直接使用
}4:常量声明和iota的使用
iota 只能用于常量
1. 基本常量声明
go
package main
import "fmt"
// 单个常量
const Pi = 3.14159
// 多个常量
const (
StatusOK = 200
StatusError = 500
)
// 类型化常量
const MaxSize int = 100
// 无类型常量(更灵活)
const Timeout = 30 // 可以用于int、int64、time.Duration等go
// iota基础用法
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
Wednesday // 3
Thursday // 4
Friday // 5
Saturday // 6
)
// iota表达式
const (
_ = iota // 0 (忽略)
KB = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
TB // 1099511627776
)
// 多个iota在同一行
const (
a, b = iota + 1, iota + 2 // a=1, b=2
c, d // c=2, d=3
e, f // e=3, f=4
)3. 实际应用示例
点击查看完整代码实现
go
// HTTP状态码
type HTTPStatus int
const (
StatusContinue HTTPStatus = 100 + iota
StatusSwitchingProtocols // 101
StatusProcessing // 102
StatusEarlyHints // 103
)
const (
StatusOK HTTPStatus = 200 + iota
StatusCreated // 201
StatusAccepted // 202
StatusNonAuthoritativeInfo // 203
)
// 权限位操作
type Permission int
const (
Read Permission = 1 << iota // 1 (001)
Write // 2 (010)
Execute // 4 (100)
)
func (p Permission) String() string {
var perms []string
if p&Read != 0 {
perms = append(perms, "Read")
}
if p&Write != 0 {
perms = append(perms, "Write")
}
if p&Execute != 0 {
perms = append(perms, "Execute")
}
return strings.Join(perms, "|")
}
func demonstratePermissions() {
// 组合权限
userPerm := Read | Write // 3 (011)
adminPerm := Read | Write | Execute // 7 (111)
fmt.Printf("User permissions: %s\n", userPerm) // Read|Write
fmt.Printf("Admin permissions: %s\n", adminPerm) // Read|Write|Execute
// 检查权限
if userPerm&Write != 0 {
fmt.Println("User can write")
}
}5:变量赋值和类型转换
详细解答
1. Go不支持隐式类型转换
go
func typeConversionRules() {
var i32 int32 = 42
var i64 int64
// 错误:不同数值类型不能直接赋值
// i64 = i32 // 编译错误
// 正确:需要显式转换
i64 = int64(i32)
var f32 float32 = 3.14
var f64 float64
// 浮点数也需要显式转换
f64 = float64(f32)
fmt.Printf("int32: %d, int64: %d\n", i32, i64)
fmt.Printf("float32: %f, float64: %f\n", f32, f64)
}2. 数值类型转换
go
func numericConversions() {
// 整数类型转换
var i8 int8 = 127
var i16 int16 = int16(i8)
var i32 int32 = int32(i16)
var i64 int64 = int64(i32)
// 浮点数转整数(小数部分被截断)
f := 3.99
i := int(f) // i = 3
// 整数转浮点数
x := 42
fx := float64(x) // fx = 42.0
fmt.Printf("int8: %d, int16: %d, int32: %d, int64: %d\n", i8, i16, i32, i64)
fmt.Printf("float to int: %.2f -> %d\n", f, i)
fmt.Printf("int to float: %d -> %.2f\n", x, fx)
}3. 字符串和数值转换
点击查看完整代码实现
go
import (
"fmt"
"strconv"
)
func stringConversions() {
// 数值转字符串
i := 42
s1 := strconv.Itoa(i) // "42"
s2 := strconv.FormatInt(int64(i), 10) // "42"
s3 := fmt.Sprintf("%d", i) // "42"
f := 3.14159
s4 := strconv.FormatFloat(f, 'f', 2, 64) // "3.14"
// 字符串转数值
str := "123"
num, err := strconv.Atoi(str)
if err != nil {
fmt.Printf("转换失败: %v\n", err)
} else {
fmt.Printf("转换结果: %d\n", num) // 123
}
// 处理无效转换
invalid := "abc"
_, err = strconv.Atoi(invalid)
if err != nil {
fmt.Printf("无效转换: %v\n", err) // strconv.Atoi: parsing "abc": invalid syntax
}
}6:变量作用域和生命周期
1. 包级别作用域
go
package main
import "fmt"
// 包级别变量
var globalVar = "全局变量"
const GlobalConst = "全局常量"
func main() {
fmt.Println(globalVar) // 可以访问
fmt.Println(GlobalConst) // 可以访问
}2. 函数作用域
go
func functionScope() {
// 函数级别变量
localVar := "局部变量"
if true {
// 块级作用域
blockVar := "块级变量"
fmt.Println(localVar) // 可以访问外层变量
fmt.Println(blockVar) // 可以访问当前块变量
}
// fmt.Println(blockVar) // 编译错误:undeclared name: blockVar
fmt.Println(localVar) // 可以访问
}3. 变量遮蔽(Shadowing)
go
func shadowingExample() {
localVar := "外层变量"
fmt.Println(localVar) // 可以访问外层变量
if true {
// 块级作用域
blockVar := "块级变量"
fmt.Println(localVar) // 可以访问外层变量
fmt.Println(blockVar) // 可以访问当前块变量
}
fmt.Println(localVar) // 外层变量
if true {
localVar := "内层变量" // 变量遮蔽
fmt.Println(localVar) // 输出"内层变量"
}
fmt.Println(localVar) // 输出"外层变量"
}4. 闭包和变量捕获
点击查看完整代码实现
go
func closureExample() {
// 错误的方式:循环变量捕获
var funcs []func()
for i := 0; i < 3; i++ {
funcs = append(funcs, func() {
fmt.Printf("错误方式 - i: %d\n", i) // 都输出3
})
}
// 正确的方式:创建局部变量
var funcs2 []func()
for i := 0; i < 3; i++ {
i := i // 创建新的局部变量
funcs2 = append(funcs2, func() {
fmt.Printf("正确方式 - i: %d\n", i) // 输出0,1,2
})
}
fmt.Println("执行错误方式的闭包:")
for _, f := range funcs {
f() // 都输出 3
}
fmt.Println("执行正确方式的闭包:")
for _, f := range funcs2 {
f() // 输出 0, 1, 2
}
}核心知识点总结
类型转换要点
- 显式转换: Go不支持隐式类型转换
- 精度损失: 注意类型转换可能导致的精度损失或溢出
- 字符串转换: 使用strconv包进行字符串和数值的转换
- 错误处理: 转换失败时要正确处理错误
作用域管理要点
- 作用域层次: 包 > 文件 > 函数 > 块
- 变量遮蔽: 内层变量可以遮蔽外层同名变量
- 闭包陷阱: 注意循环中闭包的变量捕获问题
- 内存管理: 理解变量生命周期对内存管理的影响
