Go 练习题(18 题)

学了那么多语法,得动手写才算真的会。这一页给 18 道精心挑选的 Go 练习题,按难度分三级——基础 8 题(语法 / 数据类型 / 条件 / 循环 / 函数 / 切片)、进阶 6 题(map / struct / 闭包 / goroutine / 接口 / defer)、实战 4 题(HTTP / 数据结构 / Reader)。每题都含题目描述、提示、可折叠的参考答案。

使用建议:先别急着展开答案。读题后自己打开本地 Go 环境(或 Go Playground)试着写,卡住了再看提示,实在写不出再对照参考答案。答案不止一种,你的写法可能比参考更好——能跑通、可读性强、符合 Go 风格就是好代码。

基础(8 题):语法 / 数据类型 / 条件 / 循环 / 函数 / 切片

这一组覆盖 Go 的基础语法元素,目标是用最短的代码解决一个小问题,训练"把思路翻译成 Go 代码"的能力。注意 Go 强制左大括号不换行、变量声明要用 := 还是 var。

第 1 题:翻转字符串

题目:写一个函数 reverseString(s string) string,把传入的字符串翻转。比如 "hello" 翻转成 "olleh"。要求支持中文字符。

提示:Go 的 string 是字节序列,含中文时直接按下标交换会乱码。先 []rune(s) 转成码点切片,再用双指针(i, j)从头尾向中间走,边走边交换。

参考答案
package main

import "fmt"

// Go 的字符串是字节序列,含中文时要先转 []rune 才能按"字符"翻转
func reverseString(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]   // 多重赋值交换
    }
    return string(runes)
}

func main() {
    fmt.Println(reverseString("hello"))   // olleh
    fmt.Println(reverseString("Go语言"))   // 言语oG
}

第 2 题:判断素数

题目:写一个函数 isPrime(n int) bool,判断正整数 n 是否为素数。

提示:小于 2 的都不是素数;只需检查从 2 到 sqrt(n) 的整除性。避免浮点可以用 i*i <= n 替代 i <= int(math.Sqrt(n))。

参考答案
package main

import "fmt"

func isPrime(n int) bool {
    if n < 2 {
        return false
    }
    // 只需检查到 sqrt(n),用 i*i <= n 避免浮点运算
    for i := 2; i*i <= n; i++ {
        if n%i == 0 {
            return false
        }
    }
    return true
}

func main() {
    for _, n := range []int{1, 2, 7, 10, 13} {
        fmt.Printf("%d -> %v\n", n, isPrime(n))
    }
}
// 输出:
// 1 -> false
// 2 -> true
// 7 -> true
// 10 -> false
// 13 -> true

第 3 题:求和(1 到 n)

题目:写一个函数 sum(n int) int,返回 1 + 2 + ... + n 的和。比如 sum(100) == 5050。

提示:循环累加最直观;等差数列公式 n*(n+1)/2 是 O(1) 写法。Go 没有内置 range 求和,要么 for 循环,要么手写。

参考答案
package main

import "fmt"

// 方法 1:循环累加
func sum(n int) int {
    total := 0
    for i := 1; i <= n; i++ {
        total += i
    }
    return total
}

// 方法 2:等差数列求和公式 n*(n+1)/2,O(1) 时间
func sumFormula(n int) int {
    return n * (n + 1) / 2
}

func main() {
    fmt.Println(sum(100))        // 5050
    fmt.Println(sumFormula(100)) // 5050
}

第 4 题:FizzBuzz

题目:打印 1 到 15。3 的倍数打印 Fizz,5 的倍数打印 Buzz,同时是 3 和 5 的倍数打印 FizzBuzz,其余打印数字本身。

提示:if / else if / else 的判断顺序很关键——必须先判断 15 的倍数,否则会被前面的分支截胡。Go 也支持 switch 且 case 不穿透,可改写得更清爽。

参考答案
package main

import "fmt"

func fizzBuzz() {
    for i := 1; i <= 15; i++ {
        if i%15 == 0 {           // 必须先判断 15(否则被 3 或 5 截胡)
            fmt.Println("FizzBuzz")
        } else if i%3 == 0 {
            fmt.Println("Fizz")
        } else if i%5 == 0 {
            fmt.Println("Buzz")
        } else {
            fmt.Println(i)
        }
    }
}

func main() {
    fizzBuzz()
}
// 输出:1 / 2 / Fizz / 4 / Buzz / Fizz / 7 / 8 / Fizz / Buzz
//      / 11 / Fizz / 13 / 14 / FizzBuzz

第 5 题:切片去重

题目:写一个函数 dedupe(nums []int) []int,去掉切片中的重复元素,且保持元素首次出现的顺序。比如 [1,3,2,3,1,4,2] → [1,3,2,4]。

提示:用一个 map[int]bool 记录见过的元素,遍历时没见过就加入结果切片。Go 没有 set 类型,map[T]bool 就是惯用的 set。

参考答案
package main

import "fmt"

// 利用 map 的 key 唯一性去重,保持首次出现顺序
func dedupe(nums []int) []int {
    seen := make(map[int]bool)
    result := []int{}
    for _, n := range nums {
        if !seen[n] {
            seen[n] = true
            result = append(result, n)
        }
    }
    return result
}

func main() {
    fmt.Println(dedupe([]int{1, 3, 2, 3, 1, 4, 2}))
    // [1 3 2 4]
}

第 6 题:冒泡排序

题目:写一个函数 bubbleSort(arr []int),用冒泡排序对切片原地升序排序。

提示:外层 n-1 轮,每轮把最大值"冒泡"到末尾,内层范围逐轮缩小(n-1-i);arr[j], arr[j+1] = arr[j+1], arr[j] 是 Go 的多重赋值交换。实际项目直接用 sort.Ints。

参考答案
package main

import "fmt"

// 原地排序,修改传入的切片
func bubbleSort(arr []int) {
    n := len(arr)
    for i := 0; i < n-1; i++ {
        // 每轮把最大值"冒泡"到末尾,所以内层范围逐轮缩小
        for j := 0; j < n-1-i; j++ {
            if arr[j] > arr[j+1] {
                arr[j], arr[j+1] = arr[j+1], arr[j]
            }
        }
    }
}

func main() {
    arr := []int{5, 2, 8, 1, 9, 3}
    bubbleSort(arr)
    fmt.Println(arr) // [1 2 3 5 8 9]
}
// 提示:实际项目用 sort.Ints(arr) 一行搞定

第 7 题:计算阶乘

题目:写一个函数 factorial(n int) int,返回 n!。要求 factorial(0) == 1,且对负数 panic。

提示:循环写法用一个累乘变量;递归写法更贴近数学定义。Go 没有 try/catch,错误输入用 panic 中断(或返回 error)。

参考答案
package main

import "fmt"

// 循环版
func factorial(n int) int {
    if n < 0 {
        panic("阶乘不接受负数")
    }
    result := 1
    for i := 2; i <= n; i++ {
        result *= i
    }
    return result
}

// 递归版
func factorialRec(n int) int {
    if n < 2 {
        return 1
    }
    return n * factorialRec(n-1)
}

func main() {
    fmt.Println(factorial(5))     // 120
    fmt.Println(factorialRec(6))  // 720
}

第 8 题:猜数字游戏

题目:程序随机生成一个 1~100 的整数,让用户从控制台输入猜测,提示"太大了 / 太小了",直到猜中。要求处理非数字输入不崩溃。

提示:rand.Intn(100)+1 生成目标(Go 1.20+ 自动随机种子);fmt.Scan(&guess) 读输入,返回 error 时清空缓冲区并提示重试。可用 switch 分派三种情况。

参考答案
package main

import (
    "fmt"
    "math/rand"
)

func main() {
    // Go 1.20+ 自动用随机种子;老版本需手动 rand.Seed(time.Now().UnixNano())
    target := rand.Intn(100) + 1   // 1~100
    fmt.Println("我已经想好一个 1~100 的数字,来猜猜看:")

    for {
        var guess int
        fmt.Print(">>> ")
        _, err := fmt.Scan(&guess)
        if err != nil {
            fmt.Println("请输入一个有效的整数")
            // 清空输入缓冲区里残留的非法字符
            var discard string
            fmt.Scanln(&discard)
            continue
        }
        switch {
        case guess < target:
            fmt.Println("太小了,再大一点")
        case guess > target:
            fmt.Println("太大了,再小一点")
        default:
            fmt.Println("恭喜你,猜对了!")
            return
        }
    }
}

进阶(6 题):map / struct / 闭包 / goroutine / 接口 / defer

这一组开始组合多个知识点——用 map 统计、用 struct 组织数据、用闭包封装状态、用 goroutine 并发、用接口实现多态。做完了你会对 Go 的工程化设计有直观感受。

第 9 题:map 词频统计

题目:给定一段英文文本,统计每个单词的出现次数,按频次降序输出。

提示:strings.Fields 按空白切分(比 Split(" ") 更健壮);counts[word]++ 自动计数(map 取不到时返回零值 0)。map 无序,要排序先把 key 抽成切片,再用 sort.Slice 配合闭包。

参考答案
package main

import (
    "fmt"
    "sort"
    "strings"
)

func wordCount(text string) map[string]int {
    counts := make(map[string]int)
    // strings.Fields 按空白切分(比 Split 更健壮,处理多空格)
    for _, word := range strings.Fields(text) {
        counts[word]++
    }
    return counts
}

func main() {
    text := "the quick brown fox the lazy dog the fox"
    counts := wordCount(text)

    // map 是无序的,要按频次排序输出需先抽成切片
    type kv struct {
        Word  string
        Count int
    }
    var list []kv
    for w, c := range counts {
        list = append(list, kv{w, c})
    }
    sort.Slice(list, func(i, j int) bool {
        return list[i].Count > list[j].Count
    })

    for _, item := range list {
        fmt.Printf("  %s: %d\n", item.Word, item.Count)
    }
}
// 输出(按频次降序):
//   the: 3
//   fox: 2
//   quick: 1
//   brown: 1
//   lazy: 1
//   dog: 1

第 10 题:struct 切片排序

题目:定义 Student 结构体(姓名 + 分数),给定一个学生切片,按分数降序排序后输出。

提示:Go 没有类的"自然排序",sort.Slice(sl, func(i, j int) bool { ... }) 配合闭包最灵活;闭包里写 sl[i].Score > sl[j].Score 实现降序。

参考答案
package main

import (
    "fmt"
    "sort"
)

type Student struct {
    Name  string
    Score int
}

func main() {
    students := []Student{
        {"小明", 90},
        {"小红", 95},
        {"小刚", 85},
    }

    // 按分数降序
    sort.Slice(students, func(i, j int) bool {
        return students[i].Score > students[j].Score
    })

    for _, s := range students {
        fmt.Printf("  %s: %d\n", s.Name, s.Score)
    }
}
// 输出:
//   小红: 95
//   小明: 90
//   小刚: 85
// 提示:Go 没有类的"自然排序",sort.Slice 配合闭包最灵活

第 11 题:闭包计数器

题目:写一个函数 makeCounter() func() int,每次调用返回的函数,计数器加 1 并返回当前值。要求两个独立计数器互不影响。

提示:外层函数定义 count 变量,内层匿名函数捕获并修改它——这就是闭包。Go 的闭包天然隔离,每次 makeCounter() 都创建独立的 count。

参考答案
package main

import "fmt"

// 返回一个函数,内部"封存"了 count 变量
func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

func main() {
    counter := makeCounter()
    fmt.Println(counter()) // 1
    fmt.Println(counter()) // 2
    fmt.Println(counter()) // 3

    // 再建一个独立计数器,互不影响
    counter2 := makeCounter()
    fmt.Println(counter2()) // 1
}

第 12 题:goroutine 并发求和

题目:写一个函数 concurrentSum(nums []int) int,把切片分两半,用两个 goroutine 各算一半,通过 channel 汇总结果。

提示:无缓冲 chan int 做汇合点;go func(){ ... }() 启动 goroutine;<-ch 接收是阻塞的,两次接收自然等到两个 goroutine 完成。这是 CSP 模型的最小例子。

参考答案
package main

import "fmt"

// 把切片分两半,两个 goroutine 各算一半,通过 channel 汇总
func concurrentSum(nums []int) int {
    ch := make(chan int)
    half := len(nums) / 2

    go func() {
        sum := 0
        for _, v := range nums[:half] {
            sum += v
        }
        ch <- sum
    }()
    go func() {
        sum := 0
        for _, v := range nums[half:] {
            sum += v
        }
        ch <- sum
    }()

    return <-ch + <-ch   // 两次接收,汇合两个 goroutine 的结果
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Println(concurrentSum(nums)) // 55
}
// 提示:channel 接收是阻塞的,<-ch 会等到对应 goroutine 发送完成

第 13 题:接口多态

题目:定义 Shape 接口(有 Area() float64 方法),实现 Rectangle 和 Circle 两种类型,写一个 printArea 函数能接收任意 Shape。

提示:Go 接口是隐式实现——只要类型拥有接口声明的全部方法,就自动实现,不需要 implements 关键字。参数声明为 s Shape,传 Rectangle 或 Circle 都行。

参考答案
package main

import "fmt"

// 定义接口:任何有 Area() 方法的类型都自动实现 Shape
type Shape interface {
    Area() float64
}

type Rectangle struct {
    W, H float64
}

func (r Rectangle) Area() float64 {
    return r.W * r.H
}

type Circle struct {
    R float64
}

func (c Circle) Area() float64 {
    return 3.14159 * c.R * c.R
}

// 参数是接口类型,能接收任何 Shape——这就是多态
func printArea(name string, s Shape) {
    fmt.Printf("%s 面积: %.2f\n", name, s.Area())
}

func main() {
    printArea("矩形", Rectangle{W: 3, H: 4}) // 12.00
    printArea("圆形", Circle{R: 2})          // 12.57
}
// Go 的接口是"隐式实现",不需要 implements 关键字

第 14 题:defer 执行顺序

题目:写一个函数,在开头 Println("start")、结尾 Println("end"),中间用 defer 注册三条打印。观察输出顺序,解释原因。

提示:defer 按 LIFO(后进先出)执行,且在函数 return 之前才触发。所以 "start" 和 "end" 先打印,三条 defer 倒序打印。这是 Go 资源管理的基石。

参考答案
package main

import "fmt"

func deferOrder() {
    fmt.Println("start")
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    defer fmt.Println("defer 3")
    fmt.Println("end")
}

func main() {
    deferOrder()
}
// 输出:
// start
// end
// defer 3
// defer 2
// defer 1
//
// 原理:defer 按 LIFO(后进先出)执行,函数 return 之前才依次触发。
// 常用于关文件、释放锁、打日志,保证资源一定被清理。

实战(4 题):综合应用

这一组每题都会综合用到多个知识点——标准库 net/http、指针与数据结构、递归、接口——做出一个接近真实场景的小程序。建议每题都完整跑一遍,再想想怎么扩展。

第 15 题:简易 HTTP 服务器

题目:用标准库 net/http 起一个服务,监听 8080 端口:(a) 访问 / 返回 Hello, <路径>!;(b) 访问 /api 返回一段 JSON。

提示:http.HandleFunc 注册路由,handler 函数签名 func(w http.ResponseWriter, r *http.Request);用 r.URL.Path 取路径;encoding/json 的 json.NewEncoder(w).Encode(data) 一行输出 JSON。

参考答案
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    // 路由:/  返回问候语
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        name := r.URL.Path[1:]       // 去掉前导 /
        if name == "" {
            name = "World"
        }
        fmt.Fprintf(w, "Hello, %s!", name)
    })

    // 路由:/api  返回 JSON
    http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        data := map[string]string{"msg": "ok", "lang": "go"}
        json.NewEncoder(w).Encode(data)
    })

    fmt.Println("服务已启动: http://localhost:8080")
    fmt.Println("  试试访问: http://localhost:8080/小明")
    fmt.Println("  试试访问: http://localhost:8080/api")
    http.ListenAndServe(":8080", nil)
}
// 标准库 net/http 自带生产级路由和服务器,Go 写后端就是这么短

第 16 题:链表反转

题目:定义单链表节点 Node,写一个函数 reverseList(head *Node) *Node,反转整个链表并返回新头节点。要求 O(n) 时间、O(1) 额外空间。

提示:三指针迭代法——维护 prev / curr / next,每轮把 curr.Next 掉头指向 prev,然后三个指针整体前进一步。注意 Go 指针赋值的写法。

参考答案
package main

import "fmt"

type Node struct {
    Val  int
    Next *Node
}

// 三指针迭代法:prev / curr / next 逐步翻转指向
func reverseList(head *Node) *Node {
    var prev *Node
    curr := head
    for curr != nil {
        next := curr.Next   // 暂存下一个
        curr.Next = prev    // 掉头指向前面
        prev = curr         // prev 前进
        curr = next         // curr 前进
    }
    return prev             // prev 就是新的头
}

// 辅助:把切片构造成链表
func buildList(vals []int) *Node {
    dummy := &Node{}
    tail := dummy
    for _, v := range vals {
        tail.Next = &Node{Val: v}
        tail = tail.Next
    }
    return dummy.Next
}

// 辅助:打印链表
func printList(head *Node) {
    for p := head; p != nil; p = p.Next {
        fmt.Printf("%d -> ", p.Val)
    }
    fmt.Println("nil")
}

func main() {
    head := buildList([]int{1, 2, 3, 4, 5})
    printList(head)            // 1 -> 2 -> 3 -> 4 -> 5 -> nil
    printList(reverseList(head)) // 5 -> 4 -> 3 -> 2 -> 1 -> nil
}

第 17 题:二叉树遍历

题目:定义二叉树节点 TreeNode,实现前序、中序、后序三种递归遍历,各打印节点值。

提示:递归天然适配树结构。nil 节点是递归出口;前序"根左右"、中序"左根右"、后序"左右根",区别只在于处理当前节点的位置。中序遍历二叉搜索树会得到有序序列。

参考答案
package main

import "fmt"

type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

// 前序遍历:根 → 左 → 右
func preorder(root *TreeNode) {
    if root == nil {
        return
    }
    fmt.Print(root.Val, " ")
    preorder(root.Left)
    preorder(root.Right)
}

// 中序遍历:左 → 根 → 右(对二叉搜索树会输出有序序列)
func inorder(root *TreeNode) {
    if root == nil {
        return
    }
    inorder(root.Left)
    fmt.Print(root.Val, " ")
    inorder(root.Right)
}

// 后序遍历:左 → 右 → 根
func postorder(root *TreeNode) {
    if root == nil {
        return
    }
    postorder(root.Left)
    postorder(root.Right)
    fmt.Print(root.Val, " ")
}

func main() {
    //       1
    //      / \
    //     2   3
    //    / \
    //   4   5
    root := &TreeNode{1,
        &TreeNode{2, &TreeNode{Val: 4}, &TreeNode{Val: 5}},
        &TreeNode{Val: 3},
    }
    fmt.Print("前序: "); preorder(root);   fmt.Println() // 1 2 4 5 3
    fmt.Print("中序: "); inorder(root);    fmt.Println() // 4 2 5 1 3
    fmt.Print("后序: "); postorder(root);  fmt.Println() // 4 5 2 3 1
}

第 18 题:实现 io.Reader

题目:定义一个类型 MyReader,实现 io.Reader 接口(即 Read([]byte) (int, error) 方法),让它每次读都把缓冲区填满字符 'A'。然后用 io.CopyN 读 5 个字节验证。

提示:只要给类型加上 Read(b []byte) (int, error) 方法,就自动满足 io.Reader;之后任何接收 io.Reader 的标准库函数(io.Copy、http、compress/gzip)都能用你的类型——这就是 Go 接口的威力。

参考答案
package main

import (
    "fmt"
    "io"
    "os"
)

// MyReader 实现了 Read([]byte) (int, error),即满足 io.Reader 接口。
// 这里让它无限产生字符 'A'。
type MyReader struct{}

func (MyReader) Read(b []byte) (int, error) {
    for i := range b {
        b[i] = 'A'
    }
    return len(b), nil
}

func main() {
    // 任何接收 io.Reader 的函数都能用我们自定义的类型
    // CopyN 从 reader 读 N 字节写到 writer,这里写到标准输出
    io.CopyN(os.Stdout, MyReader{}, 5)
    fmt.Println()
    // 输出:AAAAA
}
// 理解接口后,你就能写出自定义的"数据源",无缝接入 io / http / compress 等标准库。

练习建议

不要只看答案。把每道题在本地或 Go Playground 亲手敲一遍,改一改输入试试边界情况(空切片、负数、nil 指针)。Go 编译器很严格,类型错误、未使用变量都会直接报错——把报错读明白本身就是在学。

试着扩展:做完第 12 题并发求和,能不能改成切 4 份用 4 个 goroutine?做完第 15 题 HTTP 服务器,能不能加上读取 JSON 请求体?做完第 17 题二叉树,能不能实现层序遍历(BFS)?每个扩展都是一次小型的真实开发。

更多练习资源:刷算法去 LeetCode(支持 Go);做小项目去 GitHub 找灵感;遇到不懂的先查 Effective Go 和 官方 Tour。

← 上一篇 Go 实战项目

返回 Go 教程目录

✈️💬