【Go 语言】核心特性、基础语法及面试题

文章目录

  • 目录
    • [一、Go 核心特性](#一、Go 核心特性)
    • [二、Go 基础语法](#二、Go 基础语法)
      • [1. 变量声明(3种方式)](#1. 变量声明(3种方式))
      • [2. 函数(多返回值+匿名函数)](#2. 函数(多返回值+匿名函数))
      • [3. 接口(隐式实现)](#3. 接口(隐式实现))
      • [4. 并发(Goroutine + Channel)](#4. 并发(Goroutine + Channel))
      • [5. defer 延迟执行](#5. defer 延迟执行)
    • [三、Go 高频面试题及答案(含代码+解析)](#三、Go 高频面试题及答案(含代码+解析))
    • 四、面试复习总结

目录

若对您有帮助的话,请点赞收藏加关注哦,您的关注是我持续创作的动力!有问题请私信或联系邮箱:funian.gm@gmail.com

Go(Golang)是 Google 设计的静态强类型语言,主打简洁、高效、并发友好,广泛用于微服务、云原生、中间件开发。

一、Go 核心特性

特性名称 核心说明 面试高频考点
静态强类型+类型推断 编译时校验类型,支持 var a = 10(自动推断为 int),无需显式声明类型 1. 静态类型 vs 动态类型区别;2. var:= 的差异
简洁语法 无分号(自动补全)、无类继承、少关键字(仅 25 个),避免冗余语法 1. 为什么 Go 不支持类继承?2. 语法简洁带来的开发优势
原生并发模型(Goroutine) 轻量级线程(栈初始 2KB,可动态扩缩),由 Go 运行时(GOMAXPROCS)调度,而非 OS 内核 1. Goroutine 与 OS 线程的区别;2. GOMAXPROCS 作用;3. Goroutine 调度原理
通信机制(Channel) 用于 Goroutine 间安全通信("不要通过共享内存通信,而通过通信共享内存") 1. Channel 无缓冲 vs 有缓冲的区别;2. Channel 关闭后读取/写入的行为;3. 如何用 Channel 实现同步
接口(隐式实现) 无需 implements 关键字,只要结构体实现接口所有方法即自动适配 1. 隐式接口的优势;2. nil 接口为何不等于 nil;3. 空接口(interface{})的用途
垃圾回收(GC) 并发三色标记+写屏障,低延迟(Go 1.19+ 支持并发标记和清理) 1. Go GC 的核心原理;2. 如何优化 GC 性能;3. 逃逸分析与 GC 的关系
值类型 vs 引用类型 值类型(int、struct、array)拷贝值,引用类型(slice、map、channel)拷贝指针 1. 常见值类型/引用类型分类;2. 函数传参时值拷贝的坑;3. slice 扩容机制
延迟执行(defer) 函数退出前执行,用于资源释放(文件、锁),支持多个 defer 按"后进先出"执行 1. defer 的执行顺序;2. defer 与 return 的执行逻辑;3. defer 踩坑场景
编译速度快 静态链接、无依赖解析、编译优化(仅编译被引用代码) 1. Go 编译快的原因;2. 静态链接的优势与缺点

二、Go 基础语法

1. 变量声明(3种方式)

Go 推荐"短变量声明"(:=),但仅用于函数内;全局变量需用 var

go 复制代码
package main

import "fmt"

// 1. 全局变量(函数外只能用 var)
var globalVar int = 100
var globalVar2 = 200 // 类型推断

func main() {
    // 2. 函数内:短变量声明(:=),自动推断类型
    localVar := "hello"
    fmt.Println(localVar) // 输出:hello

    // 3. 显式声明类型
    var explicitVar string = "world"
    fmt.Println(explicitVar) // 输出:world

    // 多变量声明
    a, b := 1, 2
    fmt.Println(a, b) // 输出:1 2
}

2. 函数(多返回值+匿名函数)

Go 函数支持多返回值(常用于返回结果+错误),且支持匿名函数和闭包。

go 复制代码
package main

import "errors"
import "fmt"

// 多返回值函数(结果+错误)
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("除数不能为0")
    }
    return a / b, nil
}

func main() {
    // 调用多返回值函数
    res, err := divide(10, 2)
    if err != nil {
        fmt.Println("错误:", err)
        return
    }
    fmt.Println("结果:", res) // 输出:结果:5

    // 匿名函数(立即执行)
    func(msg string) {
        fmt.Println("匿名函数:", msg) // 输出:匿名函数:hello go
    }("hello go")

    // 闭包(捕获外部变量)
    counter := func() func() int {
        i := 0
        return func() int {
            i++
            return i
        }
    }()
    fmt.Println(counter()) // 输出:1
    fmt.Println(counter()) // 输出:2
}

3. 接口(隐式实现)

Go 接口是"行为契约",无需显式绑定,结构体实现接口所有方法即自动适配,灵活性极高。

go 复制代码
package main

import "fmt"

// 定义接口(仅声明方法,无实现)
type Animal interface {
    Speak() string
}

// 结构体 Dog 实现 Animal 接口(无需 implements 关键字)
type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return fmt.Sprintf("汪!我是%s", d.Name)
}

// 结构体 Cat 实现 Animal 接口
type Cat struct {
    Age int
}

func (c Cat) Speak() string {
    return fmt.Sprintf("喵!我%d岁了", c.Age)
}

// 接收 Animal 接口的函数(多态)
func LetItSpeak(a Animal) {
    fmt.Println(a.Speak())
}

func main() {
    dog := Dog{Name: "旺财"}
    cat := Cat{Age: 3}

    LetItSpeak(dog) // 输出:汪!我是旺财
    LetItSpeak(cat) // 输出:喵!我3岁了
}

4. 并发(Goroutine + Channel)

Go 原生支持并发,通过 go 关键字启动 Goroutine,channel 实现 Goroutine 间通信。

go 复制代码
package main

import "fmt"
import "time"

// Goroutine 间通过 channel 通信
func producer(ch chan<- int) { // 只写 channel
    for i := 1; i <= 5; i++ {
        ch <- i // 向 channel 写入数据
        time.Sleep(100 * time.Millisecond)
    }
    close(ch) // 关闭 channel(告知接收方无数据)
}

func consumer(ch <-chan int) { // 只读 channel
    for num := range ch { // 循环读取 channel,直到关闭
        fmt.Println("收到数据:", num)
    }
}

func main() {
    // 创建无缓冲 channel(同步通信)
    ch := make(chan int)

    // 启动 2 个 Goroutine
    go producer(ch)
    go consumer(ch)

    // 主 Goroutine 等待 1 秒(避免提前退出)
    time.Sleep(1 * time.Second)
    // 输出:
    // 收到数据: 1
    // 收到数据: 2
    // 收到数据: 3
    // 收到数据: 4
    // 收到数据: 5
}

5. defer 延迟执行

defer 用于函数退出前执行(如关闭文件、释放锁),多个 defer 按"后进先出(LIFO)"顺序执行。

go 复制代码
package main

import "fmt"

func main() {
    defer fmt.Println("defer 1") // 最后执行
    defer fmt.Println("defer 2") // 中间执行
    fmt.Println("主逻辑")         // 先执行

    // 输出顺序:
    // 主逻辑
    // defer 2
    // defer 1

    // 进阶:defer 中修改返回值(仅当返回值有命名时生效)
    fmt.Println(add(1, 2)) // 输出:4
}

func add(a, b int) (res int) { // 命名返回值 res
    defer func() {
        res += 1 // 延迟修改返回值
    }()
    return a + b // 实际返回:3 + 1 = 4
}

三、Go 高频面试题及答案(含代码+解析)

面试题 1:Goroutine 和 OS 线程的区别?

题目分析:

考察对 Go 并发模型的核心理解,是面试必问考点。

答案对比表:
对比维度 Goroutine OS 线程(Thread)
调度者 Go 运行时(GOMAXPROCS) 操作系统内核
栈大小 初始 2KB,可动态扩缩(最大 1GB) 固定大小(通常 1MB),不可动态调整
上下文切换开销 极低(用户态切换,无需内核参与) 较高(内核态切换,需保存寄存器/内存映射)
并发数量 支持百万级并发 仅支持数千级并发(受内存限制)
依赖 基于 M:N 调度(M 个 Goroutine 映射到 N 个 OS 线程) 1:1 调度(一个线程对应一个内核线程)
代码验证:
go 复制代码
package main

import (
	"fmt"
	"runtime"
	"time"
)

func main() {
	// 设置最大 OS 线程数(默认等于 CPU 核心数)
	runtime.GOMAXPROCS(2)

	// 启动 10000 个 Goroutine(无压力)
	for i := 0; i < 10000; i++ {
		go func(n int) {
			time.Sleep(1 * time.Second)
			fmt.Println("Goroutine", n)
		}(i)
	}

	time.Sleep(2 * time.Second)
}

面试题 2:defer 的执行顺序是什么?return 和 defer 的执行逻辑?

题目分析:

考察 defer 底层机制,容易踩坑,高频考点。

核心结论:
  1. 多个 defer 按「后进先出(LIFO)」执行;
  2. return 执行逻辑:先计算返回值 → 执行 defer → 真正返回;
  3. 若返回值是命名变量,defer 可修改返回值;若为匿名返回值,修改无效。
代码示例:
go 复制代码
package main

import "fmt"

// 1. 多个 defer 执行顺序(LIFO)
func deferOrder() {
	defer fmt.Println("a")
	defer fmt.Println("b")
	defer fmt.Println("c")
	// 输出:c → b → a
}

// 2. 命名返回值:defer 可修改
func namedReturn() (res int) {
	defer func() {
		res = 100 // 修改返回值
	}()
	return 50 // 先赋值 res=50,再执行 defer 改为 100,最终返回 100
}

// 3. 匿名返回值:defer 无法修改
func anonymousReturn() int {
	var res int = 50
	defer func() {
		res = 100 // 仅修改局部变量 res,返回值已拷贝为 50
	}()
	return res
}

func main() {
	deferOrder()
	fmt.Println(namedReturn())      // 输出:100
	fmt.Println(anonymousReturn())  // 输出:50
}

面试题 3:slice 和 array 的区别?slice 扩容机制是什么?

题目分析:

slice 是 Go 中最常用的数据结构,扩容机制是高频考点。

核心结论:
  1. array :固定长度(var arr [5]int),值类型,拷贝时复制整个数组;
  2. slice :动态长度(var s []int),引用类型(底层是「数组指针+长度+容量」),拷贝时复制指针;
  3. 扩容机制 (Go 1.18+ 规则):
    • 当切片长度 < 256 时,扩容后容量 = 原容量 × 2;
    • 当切片长度 ≥ 256 时,扩容后容量 = 原容量 × 1.25(向上取整);
    • 若扩容后容量仍小于所需长度,则直接扩容到所需长度。
代码示例:
go 复制代码
package main

import "fmt"

func main() {
	// 1. array vs slice 声明
	var arr [3]int = [3]int{1, 2, 3} // 固定长度 3
	var s []int = arr[0:2]           // slice 引用 arr 的前 2 个元素(长度 2,容量 3)

	fmt.Println("arr:", arr) // 输出:arr: [1 2 3]
	fmt.Println("s:", s)     // 输出:s: [1 2]

	// 2. slice 修改会影响原 array(引用类型)
	s[0] = 100
	fmt.Println("arr:", arr) // 输出:arr: [100 2 3]
	fmt.Println("s:", s)     // 输出:s: [100 2]

	// 3. slice 扩容验证
	s = append(s, 4, 5) // 原容量 3,长度 2 → 追加 2 个元素,长度 4,触发扩容
	fmt.Println("s 扩容后:", s)      // 输出:[100 2 4 5]
	fmt.Println("s 容量:", cap(s)) // 输出:6(原容量 3 < 256 → 3×2=6)
}

面试题 4:nil 接口和 nil 结构体的区别?

题目分析:

考察接口的底层结构,是 Go 面试的"坑题"之一。

核心结论:
  1. 接口(interface{})的底层结构包含两部分:type(类型)和 value(值);
  2. 只有当 typevalue 都为 nil 时,接口才等于 nil;
  3. 当把 nil *int 赋值给接口时,接口的 type*intvalue 是 nil → 因此接口不等于 nil。
代码示例:
go 复制代码
package main

import "fmt"

func main() {
	// 1. 接口的 type 和 value 都为 nil → 等于 nil
	var i1 interface{} = nil
	fmt.Println(i1 == nil) // 输出:true

	// 2. 接口的 type 是 *int,value 是 nil → 不等于 nil
	var p *int = nil
	var i2 interface{} = p
	fmt.Println(i2 == nil) // 输出:false

	// 验证接口的底层结构(通过反射)
	import "reflect"
	fmt.Printf("i2 类型:%v,值:%v\n", reflect.TypeOf(i2), reflect.ValueOf(i2))
	// 输出:i2 类型:*int,值:<nil>
}

面试题 5:channel 无缓冲和有缓冲的区别?关闭 channel 后读写会发生什么?

题目分析:

考察 channel 的核心特性,是并发模块的高频考点。

核心结论:
channel 类型 核心特性(通信方式) 写入阻塞条件 读取阻塞条件
无缓冲 同步通信("手递手") 无接收方时阻塞 无发送方时阻塞
有缓冲 异步通信("队列") 缓冲区满时阻塞 缓冲区空时阻塞
关闭 channel 后的行为:
  1. 读取:已写入的数据可正常读取,读完后返回该类型的零值(如 int 返回 0),可通过 val, ok := <-ch 判断 channel 是否关闭(ok 为 false 表示关闭);
  2. 写入:会触发 panic(关闭后的 channel 不能写入);
  3. 多次关闭:会触发 panic(channel 只能关闭一次)。
代码示例:
go 复制代码
package main

import "fmt"

func main() {
	// 1. 无缓冲 channel(同步)
	ch1 := make(chan int)
	go func() {
		ch1 <- 100 // 无接收方则阻塞,直到主 Goroutine 读取
	}()
	fmt.Println(<-ch1) // 无发送方则阻塞,直到子 Goroutine 写入 → 输出:100

	// 2. 有缓冲 channel(异步)
	ch2 := make(chan int, 2) // 缓冲区大小 2
	ch2 <- 200
	ch2 <- 300
	// ch2 <- 400 // 缓冲区满,阻塞
	fmt.Println(<-ch2) // 输出:200
	fmt.Println(<-ch2) // 输出:300

	// 3. 关闭 channel 后读取
	close(ch2)
	val1, ok1 := <-ch2 // 缓冲区空,返回零值和 false → val1=0,ok1=false
	fmt.Println(val1, ok1) // 输出:0 false

	// 4. 关闭 channel 后写入(触发 panic)
	// ch2 <- 500 // 报错:panic: send on closed channel

	// 5. 遍历读取 channel(直到关闭)
	ch3 := make(chan int, 3)
	ch3 <- 1
	ch3 <- 2
	ch3 <- 3
	close(ch3)
	for num := range ch3 {
		fmt.Println(num) // 输出:1、2、3(读完后退出循环)
	}
}

四、面试复习总结

Go 面试的核心考点集中在:

  1. 并发模型:Goroutine 调度、Channel 通信、GOMAXPROCS;
  2. 语法特性:defer 执行逻辑、接口隐式实现、值类型 vs 引用类型;
  3. 数据结构:slice 扩容、map 底层实现(哈希表)、channel 缓冲机制;
  4. 工程实践 :错误处理(error 接口)、GC 优化、逃逸分析。
相关推荐
周杰伦_Jay3 小时前
【Python开发面试题及答案】核心考点+原理解析+实战场景
开发语言·python
前端不太难3 小时前
RN Hooks 设计规范与反模式清单
开发语言·php·设计规范
HyperAI超神经3 小时前
【vLLM 学习】vLLM TPU 分析
开发语言·人工智能·python·学习·大语言模型·vllm·gpu编程
czlczl200209254 小时前
Spring Boot 参数校验进阶:抛弃复杂的 Group 分组,用 @AssertTrue 实现“动态逻辑校验”
java·spring boot·后端
ForteScarlet4 小时前
如何解决 Kotlin/Native 在 Windows 下 main 函数的 args 乱码?
开发语言·windows·kotlin
月殇_木言4 小时前
应用层自定义协议与序列化
开发语言
a努力。4 小时前
网易Java面试被问:偏向锁在什么场景下反而降低性能?如何关闭?
java·开发语言·后端·面试·架构·c#
前端达人4 小时前
CSS终于不再是痛点:2026年这7个特性让你删掉一半JavaScript
开发语言·前端·javascript·css·ecmascript
wjs20244 小时前
SVG 多边形
开发语言