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 教程目录