Skip to content

变量和常量 - 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, 3

2. new函数和make函数

核心区别对比表

特性newmake
核心目的分配内存,返回地址初始化复杂结构,立即可用
适用类型所有类型仅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}
}

零值的实用性

  1. 安全的默认状态
go
func safeCounter() {
    var count int  // 零值为0,可以直接使用
    count++        // 安全地递增
    fmt.Println(count)  // 输出1
}
  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
    }
}

核心知识点总结

类型转换要点

  1. 显式转换: Go不支持隐式类型转换
  2. 精度损失: 注意类型转换可能导致的精度损失或溢出
  3. 字符串转换: 使用strconv包进行字符串和数值的转换
  4. 错误处理: 转换失败时要正确处理错误

作用域管理要点

  1. 作用域层次: 包 > 文件 > 函数 > 块
  2. 变量遮蔽: 内层变量可以遮蔽外层同名变量
  3. 闭包陷阱: 注意循环中闭包的变量捕获问题
  4. 内存管理: 理解变量生命周期对内存管理的影响

正在精进