关键词 (后端实战常用):Go 入门、fmt、Println、变量与函数、结构体、多返回值、返回指针、error、err 命名、错误包装、errors.Is、errors.As、goroutine、channel、WaitGroup、select、ready、context、Go 并发
获取实战代码 :如需在本地跑通本文示例,请克隆仓库 WenSongWang/go-quickstart-7days,本文示例在 day1 目录,克隆后在项目根目录执行下文中的命令即可。
阅读说明 :本文包含 Day 1 四个示例的完整代码 与逐段解读,仅读博客即可学完当日内容,无需打开项目。若再克隆仓库动手跑一遍,理解会更牢。
一、本篇目标
学完本文并跑通本目录下的示例代码,你将掌握:
| 模块 | 内容 |
|---|---|
| 环境 | 安装 Go、go run / go build |
| 语法 | 变量、函数、结构体、多返回值、error |
| 错误处理 | errors.Is / errors.As、错误包装 fmt.Errorf("%w", err)(面试常问) |
| 并发 | goroutine、channel、sync.WaitGroup、select、context 取消与超时(面试高频) |
二、Go 下载与安装
若本机还未安装 Go,按下面步骤即可(建议 Go 1.21+)。
| 步骤 | 说明 |
|---|---|
| 1. 下载 | 打开官网 https://go.dev/dl/,按系统选安装包:Windows 选 .msi,macOS 选 .pkg 或 .tar.gz,Linux 选 .tar.gz。 |
| 2. 安装 | Windows :双击 .msi 按提示安装(默认会写入 PATH)。macOS :双击 .pkg 或解压 .tar.gz 到 /usr/local/go。Linux :解压到 /usr/local,例如 sudo tar -C /usr/local -xzf go1.21.x.linux-amd64.tar.gz。 |
| 3. 配置 PATH | 安装程序通常已配置。若终端找不到 go,把 Go 的 bin 目录加入环境变量:Windows 在「环境变量」里加,Mac/Linux 在 ~/.bashrc 或 ~/.zshrc 里加 export PATH=$PATH:/usr/local/go/bin。 |
| 4. 验证 | 新开终端执行:go version,能输出版本号即成功(如 go1.21.12 windows/amd64、go1.22.x 等均可)。再执行 go env GOPATH 可看工作目录(可选)。 |
本系列所有命令均在项目根目录 (即 go-quickstart-7days 下)执行。
三、前置要求
- 已完成上面「Go 下载与安装」或本机已有 Go 1.21+ (
go version可输出版本号)。 - 若只读博客 :无其他要求。若想跑代码:需克隆本仓库并进入项目根目录。
四、示例与知识点(先混个眼熟)
| 示例目录 | 主要知识点 |
|---|---|
hello/ |
最简程序:package main、main()、fmt.Println |
basics/ |
变量与短声明、结构体、*返回 User 指针 、多返回值 + error、if err != nil、err 常规命名 |
errors/ |
错误包装 %w、errors.Is / errors.As、自定义错误类型 |
concurrency/ |
goroutine、channel、sync.WaitGroup、打印顺序不固定 、ready channel(与 Done() 区分)、select 超时、context |
五、语法学习(实战常用,对应示例代码)
以下为写后端时最常碰到 的语法,与示例对应关系如下,可结合 day1/ 下四个示例对照阅读。
| 语法点 | 说明 | 对应示例 |
|---|---|---|
package main |
可执行程序必须用 main 包,入口函数为 main() |
hello、basics、errors、concurrency |
import |
导入标准库或第三方包,如 "fmt"、"errors" |
所有示例 |
| fmt / Println / Printf | 终端打印;Println 打一行,Printf 用 %d、%s 等占位符 |
hello、basics |
| 变量 | var 变量名 类型 = 值 或短声明 变量名 := 值(类型推断) |
basics |
| 函数 | func 函数名(参数) 返回值类型 { ... },多返回值用 (A, B) |
basics、errors、concurrency |
| 结构体 | type 名称 struct { 字段 类型 },用于请求/响应、实体等 |
basics(User) |
| *返回指针 T | 出错时只能返回 nil,只有指针能表示"没有";先判 err 再使用返回值(重点) | basics(FindUser 返回 *User) |
| 多返回值 + error | Go 惯例:(结果, error),调用处 if err != nil { ... };err 是常规变量名,非关键字 |
basics、errors |
| 错误包装 | fmt.Errorf("描述: %w", err) 保留错误链,便于 errors.Is/errors.As |
errors |
errors.Is(err, 目标) |
判断是否为目标 sentinel 错误(包装后仍可匹配) | errors |
errors.As(err, &目标) |
将错误断言为具体类型(如 *ValidationError) |
errors |
| goroutine | go 函数() 启动轻量级并发;多个 goroutine 打印顺序不固定 |
concurrency |
| channel | make(chan int, 缓冲)、close(ch)、for v := range ch;信号 channel 用 chan struct{} |
concurrency |
sync.WaitGroup |
Add(1)、Done()、Wait() 等待多个 goroutine 结束(Done 是方法名,与变量名区分) |
concurrency |
select |
多路复用 channel,常与 time.After 做超时;示例里用 ready 避免与 wg.Done() 混淆 |
concurrency |
context |
context.WithTimeout、ctx.Done()、取消/超时传递(面试高频);可改 doWithContext 里时间观察"超时"分支 |
concurrency |
建议 :先看本表;若跑代码,再打开对应目录的 main.go 逐行对照。
六、核心概念与最小示例(不看代码也能懂)
下面用极短代码 + 一句话把几个最容易懵的点说清,只看博客即可建立印象。
为什么函数经常返回指针(*User)而不是值(User)?
因为出错时没有"结果"可返回,只能返回 nil;只有指针类型才有 nil,值类型没有。所以"查用户"这类函数会写成:
go
func FindUser(id int) (*User, error) {
if id <= 0 {
return nil, errors.New("invalid id") // 出错:第一个返回值用 nil
}
return &User{ID: id, Name: "张三"}, nil // 正常:返回指针和 nil 错误
}
调用方必须先判断 err,再使用第一个返回值(否则可能是 nil,直接访问会 panic):
go
u, err := FindUser(1)
if err != nil {
return // 或打日志
}
fmt.Println(u.Name) // 这里 u 一定非 nil,安全
多返回值与 err 的常规写法
Go 里没有 try-catch,错误通过最后一个返回值 传出来;约定俗成把这个变量命名为 err(不是关键字)。写法固定成:
go
u, err := FindUser(1)
if err != nil {
// 处理错误
return
}
// 用 u
错误包装:%w 与 errors.Is
在已有错误外包一层说明时,用 %w 可以把"里面的错误"保留住,后面还能用 errors.Is 认出是哪种错:
go
return fmt.Errorf("查询用户: %w", ErrNotFound) // 包装
// 调用方
if errors.Is(err, ErrNotFound) {
// 知道是"未找到"
}
goroutine 与 channel 一句话
- goroutine :
go 函数()让这个函数在"后台"跑,不卡住当前代码;多个 goroutine 同时跑,所以打印顺序不固定是正常的。 - channel :在多个 goroutine 之间传数据,
ch <- 1发,<-ch收;close(ch)后接收方for v := range ch会收完已有数据后结束。 - select :在多个 channel 上"等谁先到",常配合
time.After(100*time.Millisecond)做超时:要么业务先完成,要么超时走另一分支。 - context :给"整条请求链路"设一个死线 (如 3 秒),下游查 DB、调接口都能收到"时间到了别干了"的信号,通过
ctx.Done()判断。
七、Day 1 示例代码全文与逐段解读(只看博客即可对照学习)
下面把四个示例的完整代码贴出,并分段做简短解读。读者无需打开项目,按顺序看下去即可把 Day 1 学完。
示例 1:hello------最简程序
完整代码(即 day1/hello/main.go,含注释):
go
package main // 可执行程序必须是 main 包
import "fmt" // 导入标准库 fmt,用于在终端打印
func main() { // 程序入口,从 main 开始执行
fmt.Println("Hello, Go! 7天入门后端开发") // 打印一行到终端并换行
}
解读:
package main:可执行程序必须是 main 包。import "fmt":导入标准库 fmt,用于在终端打印。func main():程序入口,从 main 开始执行。fmt.Println(...):打印一行字符串到终端并换行。运行后终端会输出:Hello, Go! 7天入门后端开发。
示例 2:basics------变量、结构体、返回指针、多返回值与 err
完整代码(即 day1/basics/main.go,含注释):
go
package main
import (
"errors"
"fmt"
)
// User 结构体:把多个字段绑在一起,表示"用户"(后端常用来表示请求/响应或数据库一行)
type User struct {
ID int
Name string
}
// FindUser 返回 *User(指针)而不是 User:出错时只能返回 nil,只有指针才有 nil;调用方要先判 err 再用 u
func FindUser(id int) (*User, error) {
if id <= 0 {
return nil, errors.New("invalid user id") // 出错:第一个返回值用 nil
}
return &User{ID: id, Name: "张三"}, nil // 正常:& 取地址得到 *User
}
func main() {
var a int = 1 // 显式类型
b := 2 // 短声明,类型自动推断
fmt.Println("a+b =", a+b)
u, err := FindUser(1) // err 是常规命名(非关键字);u 是 *User
if err != nil { // 必须先判断 err,再使用 u
fmt.Println("错误:", err)
return
}
fmt.Printf("用户: ID=%d Name=%s\n", u.ID, u.Name) // %d 数字 %s 字符串 \n 换行
_, err = FindUser(-1) // _ 忽略第一个返回值,只关心 err
if err != nil {
fmt.Println("预期错误:", err)
}
}
解读:
- User 结构体:把 ID、Name 绑成一种类型,表示"用户";后端里常用来表示请求/响应或数据库一行。
- *FindUser 返回 User :出错时没有用户可返回,只能返回
nil,只有指针类型才有 nil;正常时返回&User{...}, nil。调用方必须先if err != nil再使用u,否则 u 可能为 nil 会 panic。 - var a / b := :
var a int = 1显式类型,b := 2短声明、类型推断。 - u, err := FindUser(1) :err 是 Go 里错误返回值的常规命名(非关键字)。先判 err,再用 u;
fmt.Printf里%d数字、%s字符串、\n换行。 - _, err = FindUser(-1) :用
_忽略第一个返回值,只关心 err;故意传 -1 触发错误分支,打印"预期错误"。
示例 3:errors------错误包装、errors.Is、errors.As
完整代码(即 day1/errors/main.go,含注释):
go
package main
import (
"errors"
"fmt"
)
// 定义"已知错误",方便后面用 errors.Is 判断
var ErrNotFound = errors.New("not found")
var ErrInvalidID = errors.New("invalid id")
// ValidationError 自定义错误类型,带字段,便于 errors.As 取出具体信息
type ValidationError struct {
Field string
}
// 实现 Error() 后 *ValidationError 就是一种 error
func (e *ValidationError) Error() string {
return "validation error: " + e.Field
}
// 用 %w 包装底层错误,保留错误链,这样 errors.Is 即使包了一层也能匹配
func FindUser(id int) error {
if id <= 0 {
return fmt.Errorf("find user: %w", ErrInvalidID)
}
if id > 100 {
return fmt.Errorf("find user: %w", ErrNotFound)
}
if id == 50 {
return fmt.Errorf("find user: %w", &ValidationError{Field: "id"})
}
return nil
}
func main() {
// errors.Is:判断是不是某个已知错误(包装后仍可认)
err := FindUser(-1)
if errors.Is(err, ErrInvalidID) {
fmt.Println("检测到 ErrInvalidID:", err)
}
err = FindUser(101)
if errors.Is(err, ErrNotFound) {
fmt.Println("检测到 ErrNotFound:", err)
}
// errors.As:把错误转成具体类型,取出里面的字段
err = FindUser(50)
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Println("校验错误字段:", valErr.Field)
}
}
解读:
- ErrNotFound / ErrInvalidID :用
errors.New定义"已知错误",方便在调用方用errors.Is统一判断。 - ValidationError :自定义错误类型,带 Field 字段;实现
Error() string后就是一种 error,便于errors.As取出具体信息。 - fmt.Errorf("...: %w", err) :用
%w包装底层错误,保留错误链,这样errors.Is(err, ErrNotFound)即使包了一层也能匹配。 - errors.Is(err, ErrInvalidID):判断 err 是不是某个已知错误(包装后仍可认)。
- errors.As(err, &valErr):把 err 转成 *ValidationError,成功时 valErr.Field 即"哪个字段校验失败"。面试常问 Is/As 区别。
示例 4:concurrency------goroutine、channel、WaitGroup、select、context
完整代码(即 day1/concurrency/main.go,含注释):
go
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
// ---------- 1. goroutine + channel:后台发数据,主流程收 ----------
ch := make(chan int, 2) // 带缓冲的 channel,能存 2 个 int
go func() {
ch <- 1
ch <- 2
close(ch) // 发完后关闭,for range 会收完已有的就结束
}()
for v := range ch { // 一直从 ch 收,直到 ch 被 close
fmt.Println("channel:", v)
}
// ---------- 2. sync.WaitGroup:等 3 个 goroutine 都干完再继续;打印顺序不固定是正常的 ----------
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1) // 要等的任务 +1
go func(id int) { // 把 i 传进去,避免闭包捕获同一个 i
defer wg.Done() // 函数退出前把任务数 -1(Done 是方法名,不是变量)
time.Sleep(10 * time.Millisecond)
fmt.Println("worker", id)
}(i)
}
wg.Wait() // 阻塞直到 Add 和 Done 次数相等
fmt.Println("all workers done")
// ---------- 3. select:多路等待,谁先到执行谁;变量名用 ready 避免和 wg.Done() 混淆 ----------
ready := make(chan struct{}) // 空结构体 channel 只当"信号"用
go func() {
time.Sleep(50 * time.Millisecond)
close(ready) // 关闭后 <-ready 会收到信号
}()
select {
case <-ready:
fmt.Println("done") // 50ms 内收到 ready,走这里
case <-time.After(100 * time.Millisecond):
fmt.Println("timeout") // 100ms 还没收到 ready,走这里(超时)
}
// ---------- 4. context:20ms 后自动取消;defer cancel() 释放内部计时器 ----------
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer cancel()
result := doWithContext(ctx)
fmt.Println("context result:", result)
}
// select 等两路:ctx 超时 或 10ms"完成";10 < 20 所以通常返回 "ok";若改成 30ms 会先超时返回 "cancelled"
func doWithContext(ctx context.Context) string {
select {
case <-ctx.Done():
return "cancelled: " + ctx.Err().Error()
case <-time.After(10 * time.Millisecond):
return "ok"
}
}
解读:
- 1. goroutine + channel :
make(chan int, 2)带缓冲的 channel;go func(){ ch<-1; ch<-2; close(ch) }()在后台发数据,主流程for v := range ch收完 1、2 后因 ch 已 close 而结束。 - 2. WaitGroup :
wg.Add(1)、defer wg.Done()、wg.Wait()等待 3 个 goroutine 都结束。go func(id int)(i)把 i 传进去,避免闭包捕获同一个 i。多个 goroutine 并发执行,"worker 0/1/2" 打印顺序不固定是正常的。 - 3. select :变量名用 ready 而不是 done,避免和上面的
wg.Done()方法混淆。ready在 50ms 后被 close,主流程 select 等<-ready或 100ms 超时;通常 50ms 先到会打印 "done"。 - 4. context :
context.WithTimeout(context.Background(), 20*time.Millisecond)创建 20ms 后取消的 context;defer cancel()释放内部计时器。doWithContext里 select 等两路:ctx.Done()(超时/取消)或 10ms 后"完成";因 10ms < 20ms,通常返回 "ok"。若把 10 改成 30,会先超时走ctx.Done()返回 "cancelled"。
八、运行当天代码
本日示例用 go run 即可(边编译边运行,不生成可执行文件)。若想生成可执行文件 再用命令行运行,可用 go build。
方式一:go run(推荐,适合学习)
在项目根目录执行:
bash
go run ./day1/hello
go run ./day1/basics
go run ./day1/errors
go run ./day1/concurrency
方式二:go build(生成可执行文件)
在项目根目录执行后,当前目录会多出 hello、basics、errors、concurrency(Windows 下为 hello.exe 等),可直接双击或命令行运行:
bash
go build -o hello ./day1/hello
go build -o basics ./day1/basics
go build -o errors ./day1/errors
go build -o concurrency ./day1/concurrency
./hello # Linux/macOS;Windows 下为 hello.exe
./basics
# ...
建议按顺序运行:先 hello 再 basics、errors、concurrency,对应「入门 → 语法 → 错误处理 → 并发」。
九、学习建议
- 先跑再改 :每个示例都只有少量代码,跑通后改一改(变量名、返回值、错误信息),再
go run看效果。 - 重点看 :
basics里返回 *User 指针 的原因;errors里的错误包装与errors.Is/errors.As;concurrency里的 goroutine、channel、context,面试经常问到。 - 易混淆点 :
concurrency里 "worker 0/1/2" 打印顺序不固定是正常的;select 示例用的变量名是 ready (与wg.Done()方法区分);context 示例可把 doWithContext 里的 10ms 改成 30ms,观察"超时"分支。 - 遇到报错先看终端提示,再对照本目录下的
.go源码理解。
十、小结
Day 1 打好「环境 + 语法 + 错误 + 并发」基础,后面 Day 2 的 HTTP、Day 5 的 context 中间件都会用到。只看本文也能建立正确概念;若再克隆仓库把四个示例跑一遍,理解会更牢,再进入 Day 2 会更顺。