Standard library
fmt
Format Verb
| 动词 | 用途 | 典型示例 | 输出示例 |
|---|---|---|---|
%v |
按默认格式输出任意值 | fmt.Printf("%v", []int{1, 2, 3}) |
[1 2 3] |
%+v |
输出结构体字段名和值 | fmt.Printf("%+v", User{ID: 1, Name: "Jasper"}) |
{ID:1 Name:Jasper} |
%s |
输出字符串 | fmt.Printf("用户名:%s", "Jasper") |
用户名:Jasper |
%q |
带引号输出字符串 | fmt.Printf("%q", " Jasper\n") |
" Jasper\n" |
%d |
输出十进制整数 | fmt.Printf("状态码:%d", 404) |
状态码:404 |
%f |
输出浮点数 | fmt.Printf("%f", 19.9) |
19.900000 |
%.2f |
保留两位小数 | fmt.Printf("金额:%.2f 元", 19.9) |
金额:19.90 元 |
%t |
输出布尔值 | fmt.Printf("登录状态:%t", true) |
登录状态:true |
%T |
输出值的类型 | fmt.Printf("%T", 3.14) |
float64 |
%w |
包装原始错误 | fmt.Errorf("读取配置失败: %w", os.ErrNotExist) |
读取配置失败: file does not exist |
%x |
输出十六进制 | fmt.Printf("%x", []byte{0x47, 0x6f}) |
476f |
Println
适合临时输出和简单调试
name := "Jasper"
age := 20
fmt.Println("用户信息:", name, age) // 输出: 用户信息: Jasper 20
Println 会自动:
- 在参数之间加空格;
- 在末尾换行。
Printf
name := "Jasper"
age := 20
fmt.Printf("姓名:%s,年龄:%d\n", name, age)
Printf不会自动换行
Sprintf
name := "Jasper"
age := 20
message := fmt.Sprintf("用户 %s 的年龄是 %d 岁", name, age)
// 返回一个字符串
Errorf
用于给错误增加上下文
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
string
Common Functions
| 函数 | 用途 | 典型示例 | 返回结果 |
|---|---|---|---|
Contains |
是否包含子串 | strings.Contains("user@example.com", "@") |
true |
HasPrefix |
是否以指定内容开头 | strings.HasPrefix("/api/users", "/api") |
true |
HasSuffix |
是否以指定内容结尾 | strings.HasSuffix("main.go", ".go") |
true |
TrimSpace |
删除首尾的空格、换行符和制表符等空白字符。 | strings.TrimSpace(" Go\n") |
"Go" |
Split |
按分隔符切分 | strings.Split("Go,Gin,GORM", ",") |
["Go" "Gin" "GORM"] |
Fields |
按连续空白切分 | strings.Fields("go run main.go") |
["go" "run" "main.go"] |
Join |
拼接字符串切片 | strings.Join([]string{"Go", "Gin"}, ", ") |
"Go, Gin" |
ReplaceAll |
替换所有匹配内容 | strings.ReplaceAll("a-b-c", "-", "_") |
"a_b_c" |
NewReplacer |
创建可重复使用的替换器 | strings.NewReplacer(".", "_") |
*strings.Replacer |
Cut |
在第一个分隔符处切开 | strings.Cut("port=8080", "=") |
"port", "8080", true |
EqualFold |
忽略大小写比较 | strings.EqualFold("go", "GO") |
true |
ToLower |
转为小写 | strings.ToLower("GoLang") |
"golang" |
ToUpper |
转为大写 | strings.ToUpper("GoLang") |
"GOLANG" |
strconv
Common Functions
| 函数 | 用途 | 典型示例 | 返回结果 |
|---|---|---|---|
Atoi |
字符串转 int |
strconv.Atoi("8080") |
8080, nil |
Itoa |
int 转字符串 |
strconv.Itoa(404) |
"404" |
ParseBool |
字符串转 bool |
strconv.ParseBool("true") |
true, nil |
FormatBool |
bool 转字符串 |
strconv.FormatBool(true) |
"true" |
ParseFloat |
字符串转 float64 |
strconv.ParseFloat("19.9", 64) |
19.9, nil |
FormatFloat |
浮点数转字符串 | strconv.FormatFloat(19.9, 'f', 2, 64) |
"19.90" |
ParseInt |
按进制解析整数 | strconv.ParseInt("ff", 16, 64) |
255, nil |
FormatInt |
按进制格式化整数 | strconv.FormatInt(255, 16) |
"ff" |
errors
Common Functions
| 函数 | 用途 | 典型示例 | 返回结果 |
|---|---|---|---|
errors.New |
创建固定错误 | errors.New("用户名不能为空") |
error |
fmt.Errorf |
创建带变量的错误 | fmt.Errorf("用户 %d 不存在", id) |
error |
%w |
包装原始错误 | fmt.Errorf("读取配置失败: %w", err) |
保留原错误 |
errors.Is |
判断错误链中是否存在某个错误 | errors.Is(err, os.ErrNotExist) |
bool |
errors.AsType |
提取指定类型的错误 | errors.AsType[*fs.PathError](err) |
错误值和 bool |
errors.New
创建一条内容固定的错误:
func validateName(name string) error {
if name == "" {
return errors.New("用户名不能为空")
}
return nil
}
fmt.Errorf
错误信息需要包含变量时,使用 fmt.Errorf:
func findUser(id int) error {
return fmt.Errorf("用户 %d 不存在", id)
}
errors.Is
判断错误是不是某个特定错误:
var ErrUserNotFound = errors.New("用户不存在")
func getUser(id int) error {
if id != 1 {
return fmt.Errorf("查询用户 %d 失败: %w", id, ErrUserNotFound)
}
return nil
}
调用:
err := getUser(1001)
if errors.Is(err, ErrUserNotFound) {
fmt.Println("返回 HTTP 404")
}
为什么不直接写:
err == os.ErrNotExist?因为
err往往已经被包装过:
err := fmt.Errorf("读取配置失败: %w", os.ErrNotExist)此时:
fmt.Println(err == os.ErrNotExist) // false fmt.Println(errors.Is(err, os.ErrNotExist)) // trueerrors.Is 会沿着错误链继续查找,因此官方建议判断特定错误时优先使用它,而不是简单的 ==。
errors.AsType
errors.Is 判断错误是不是某个值,errors.AsType 判断错误是不是某种类型。
AsType 会遍历错误链,找到类型匹配的错误后返回该错误和 true。
package main
import (
"errors"
"fmt"
"io/fs"
"os"
)
func main() {
_, err := os.Open("config.json")
pathErr, ok := errors.AsType[*fs.PathError](err)
if ok {
fmt.Println("操作:", pathErr.Op)
fmt.Println("路径:", pathErr.Path)
fmt.Println("原因:", pathErr.Err)
}
}
输出类似:
操作: open
路径: config.json
原因: no such file or directory
为什么不能直接进行类型断言
pathErr, ok := err.(*fs.PathError)当
err本身就是*fs.PathError时,这样可以。但如果错误被包装了:
err = fmt.Errorf("加载配置失败: %w", err)直接类型断言就会失败。
而
AsType会沿着%w形成的错误链向内寻找:
pathErr, ok := errors.AsType[*fs.PathError](err) // true
time
Common Functions
| 函数 | 用途 | 典型示例 | 返回结果 |
|---|---|---|---|
Now |
获取当前时间 | time.Now() |
time.Time |
Date |
创建指定时间 | time.Date(2026, 7, 24, 20, 30, 0, 0, time.Local) |
time.Time |
Format |
时间转字符串 | t.Format("2006-01-02 15:04:05") |
"2026-07-24 20:30:00" |
Parse |
字符串转时间 | time.Parse("2006-01-02", "2026-07-24") |
time.Time, error |
Add |
增加时间间隔 | t.Add(2 * time.Hour) |
两小时后的时间 |
AddDate |
增加年月日 | t.AddDate(0, 0, 7) |
七天后的时间 |
Sub |
计算时间差 | end.Sub(start) |
time.Duration |
Before |
是否早于另一时间 | start.Before(end) |
true |
After |
是否晚于另一时间 | end.After(start) |
true |
Unix |
获取 Unix 时间戳 | t.Unix() |
int64 |
Sleep |
暂停当前 goroutine | time.Sleep(time.Second) |
无返回值 |
Now
now := time.Now()
类型为:
time.Time
获取常用字段:
now := time.Now()
fmt.Println(now.Year())
fmt.Println(now.Month())
fmt.Println(now.Day())
fmt.Println(now.Hour())
Date
创建一个确定的时间:
t := time.Date(
2026, time.July, 24,
20, 30, 0, 0,
time.Local,
)
参数顺序:
time.Date(年, 月, 日, 时, 分, 秒, 纳秒, 时区)
Format
将 time.Time 转换成字符串:
t := time.Date(
2026, time.July, 24,
20, 30, 45, 0,
time.Local,
)
text := t.Format("2006-01-02 15:04:05")
fmt.Println(text) // 输出:2026-07-24 20:30:45
Go 的时间格式不是
YYYY-MM-DD,而是使用固定参考时间:2006-01-02 15:04:05
Parse
将字符串解析成 time.Time:
t, err := time.Parse("2006-01-02", "2026-07-24")
if err != nil {
fmt.Println("日期格式错误")
return
}
fmt.Println(t.Year()) // 2026
fmt.Println(t.Month()) // July
fmt.Println(t.Day()) // 24
格式必须和输入对应:
time.Parse("2006-01-02", "2026-07-24") // 正确
time.Parse(“2006/01/02”, “2026-07-24”) // 失败
Add
在时间上增加或减少一段时间:
t := time.Date(2026, time.July, 24, 20, 0, 0, 0, time.UTC)
later := t.Add(2 * time.Hour)
earlier := t.Add(-30 * time.Minute)
fmt.Println(later.Format("15:04")) // 22:00
fmt.Println(earlier.Format("15:04")) // 19:30
AddDate
按照年、月和日计算:
t := time.Date(2026, time.July, 24, 0, 0, 0, 0, time.UTC)
nextWeek := t.AddDate(0, 0, 7)
nextMonth := t.AddDate(0, 1, 0)
nextYear := t.AddDate(1, 0, 0)
fmt.Println(nextWeek.Format("2006-01-02")) // 2026-07-31
fmt.Println(nextMonth.Format("2006-01-02")) // 2026-08-24
fmt.Println(nextYear.Format("2006-01-02")) // 2027-07-24
Sub、Since
计算两个时间之间的间隔:
start := time.Date(2026, 7, 24, 10, 0, 0, 0, time.UTC)
end := time.Date(2026, 7, 24, 12, 30, 0, 0, time.UTC)
duration := end.Sub(start)
fmt.Println(duration) // 2h30m0s
fmt.Println(duration.Hours()) // 2.5
fmt.Println(duration.Minutes()) // 150
统计程序耗时:
start := time.Now()
fmt.Println("耗时:", time.Since(start))
Before、After、Equal
比较两个时间:
start := time.Date(2026, 7, 24, 10, 0, 0, 0, time.UTC)
end := time.Date(2026, 7, 24, 12, 0, 0, 0, time.UTC)
fmt.Println(start.Before(end)) // true
fmt.Println(end.After(start)) // true
fmt.Println(start.Equal(end)) // false
Sleep
暂停当前 goroutine:
time.Sleep(2 * time.Second)
Ticker
按固定间隔重复执行任务:
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for i := 0; i < 3; i++ {
<-ticker.C
fmt.Println("执行任务")
}
ticker.C是一个通道,每到指定间隔便可以从中接收时间值。
Unix
获取和解析 Unix 时间戳:
t := time.Now()
sec := t.Unix() // 秒级时间戳,int64
milli := t.UnixMilli() // 毫秒级时间戳
nano := t.UnixNano() // 纳秒级时间戳
fmt.Println(sec) // 输出:1785010245
反过来,从时间戳还原成 time.Time:
go
t := time.Unix(1785010245, 0) // 参数:秒, 纳秒
fmt.Println(t) // 输出:2026-07-24 20:30:45 +0800 CST
Unix 时间戳统一以 UTC 为基准计算(自 1970-01-01 00:00:00 UTC 起的秒数),但
time.Unix()还原出的time.Time默认使用本地时区展示。
slices
Common Functions
| 函数 | 用途 | 典型示例 | 结果 |
|---|---|---|---|
Contains |
判断是否包含元素 | slices.Contains([]int{1, 2, 3}, 2) |
true |
Index |
查找元素下标 | slices.Index([]string{"Go", "Gin"}, "Gin") |
1 |
Clone |
复制切片 | copy := slices.Clone(nums) |
独立切片 |
Equal |
判断两个切片是否相同 | slices.Equal(a, b) |
bool |
Delete |
删除指定范围 | slices.Delete(nums, 1, 2) |
删除下标 1 |
DeleteFunc |
按条件删除 | slices.DeleteFunc(nums, isEven) |
删除所有偶数 |
Insert |
在指定位置插入 | slices.Insert(nums, 1, 9) |
在下标 1 插入 |
Sort |
升序排序 | slices.Sort(nums) |
修改原切片 |
SortFunc |
自定义排序 | slices.SortFunc(users, cmp) |
修改原切片 |
Reverse |
反转切片 | slices.Reverse(nums) |
修改原切片 |
Min / Max |
获取最小值或最大值 | slices.Max(nums) |
最大元素 |
Compact |
删除连续重复元素 | slices.Compact(nums) |
连续元素去重 |
Contains
判断切片中是否存在指定元素:
roles := []string{"admin", "editor"}
if slices.Contains(roles, "admin") {
fmt.Println("拥有管理员权限")
}
若存在返回true,反之返回false
Index
查找元素第一次出现的下标:
languages := []string{"Go", "Gin", "GORM"}
index := slices.Index(languages, "GORM")
fmt.Println(index) // 2
找到返回元素第一次出现的下标。找不到返回-1。
Clone
浅拷贝一个切片:
original := []int{1, 2, 3}
copied := slices.Clone(original) // 创建一个新的底层数组
copied[0] = 100
fmt.Println(original) // [1 2 3]
fmt.Println(copied) // [100 2 3]
此时如果元素内部仍然包含切片、Map 或指针,这些内部数据仍可能被共享。
比如:
type User struct {
Name string
Tags []string
}
original := []User{
{
Name: "Jasper",
Tags: []string{"Go", "Gin"},
},
}
copied := slices.Clone(original)
Tags 自己也是一个切片。复制 User 时,复制的是 Tags 的切片描述符。
original[0].Tags ──┐
├──> ["Go" "Gin"]
copied[0].Tags ──┘
所以修改内部元素仍会互相影响:
copied[0].Tags[0] = "Rust"
fmt.Println(original[0].Tags) // [Rust Gin]
fmt.Println(copied[0].Tags) // [Rust Gin]
解决办法:手动深拷贝
copied := slices.Clone(original)
for i := range copied {
copied[i].Tags = slices.Clone(original[i].Tags)
}
Equal
判断两个切片的长度和对应元素是否全部相同:
a := []int{1, 2, 3}
b := []int{1, 2, 3}
c := []int{3, 2, 1}
fmt.Println(slices.Equal(a, b)) // true
fmt.Println(slices.Equal(a, c)) // false
Delete
删除切片中指定下标范围的元素:
languages := []string{"Go", "Java", "Python"}
languages = slices.Delete(languages, 1, 2) // 左闭右开
fmt.Println(languages) // [Go Python]
DeleteFunc
按条件删除所有符合条件的元素:
nums := []int{1, 2, 3, 4, 5, 6}
nums = slices.DeleteFunc(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println(nums) // [1 3 5]
判断函数返回 true,代表删除该元素。
Insert
在指定下标前插入元素:
nums := []int{10, 30}
nums = slices.Insert(nums, 1, 15, 18)
fmt.Println(nums) // [10 15 18 30]
Sort
对整数、字符串等有序类型进行升序排序:
nums := []int{5, 2, 8, 1}
slices.Sort(nums)
fmt.Println(nums) // [1 2 5 8]
Sort会直接修改原切片,不返回新切片。
降序可以先升序,再反转:
slices.Sort(nums)
slices.Reverse(nums)
SortFunc
不断拿出俩个元素进行比较,决定谁应该放在前面:
import (
"cmp"
"slices"
)
type User struct {
Name string
Age int
}
users := []User{
{Name: "Jasper", Age: 20},
{Name: "Alice", Age: 18},
{Name: "Bob", Age: 22},
}
slices.SortFunc(users, func(a, b User) int {
return cmp.Compare(a.Age, b.Age)
})
fmt.Println(users) // [{Alice 18} {Jasper 20} {Bob 22}]
每次只需要回答一个问题:在最终排好序的结果里,
a应该排在b的前面吗?
-
返回负数:意思是“a 应该排在 b 前面“
-
返回正数:意思是“a 应该排在 b 后面“
-
返回
0:意思是“a 和 b 谁前谁后无所谓“(相等)
Reverse
原地反转切片:
nums := []int{1, 2, 3, 4}
slices.Reverse(nums)
fmt.Println(nums) // [4 3 2 1]
它会修改原切片,不返回结果。
Min 与 Max
scores := []int{85, 92, 76, 88}
fmt.Println(slices.Min(scores)) // 76
fmt.Println(slices.Max(scores)) // 92
不能传入空切片,否则会发生 panic
Compact
删除连续出现的重复元素:
nums := []int{1, 1, 2, 2, 2, 3}
nums = slices.Compact(nums)
fmt.Println(nums) // [1 2 3]
只删除连续重复
需要对整数切片整体去重时,可以先排序:
nums := []int{3, 1, 2, 1, 3}
slices.Sort(nums)
nums = slices.Compact(nums)
fmt.Println(nums) // [1 2 3]
maps
Common Functions
| 函数 | 用途 | 典型示例 | 结果 |
|---|---|---|---|
Clone |
复制 Map | copied := maps.Clone(original) |
得到独立 Map |
Copy |
将一个 Map 合并到另一个 | maps.Copy(dst, src) |
同名键被覆盖 |
Equal |
比较键值对是否相同 | maps.Equal(a, b) |
bool |
EqualFunc |
自定义值的比较方式 | maps.EqualFunc(a, b, equal) |
bool |
DeleteFunc |
按条件删除键值对 | maps.DeleteFunc(m, del) |
修改原 Map |
Keys |
遍历所有键 | for key := range maps.Keys(m) |
顺序不固定 |
Values |
遍历所有值 | for value := range maps.Values(m) |
顺序不固定 |
Clone
浅拷贝:新建一个Map,使用普通赋值复制键和值。
original := map[string]int{
"Go": 90,
"Gin": 80,
}
copied := maps.Clone(original)
copied["Go"] = 100
fmt.Println(original["Go"]) // 90
fmt.Println(copied["Go"]) // 100
如果值中包含切片、Map 或指针,内部数据仍可能共享。
Copy
把 src 中的键值对复制到 dst:
dst := map[string]int{
"Go": 80,
}
src := map[string]int{
"Go": 100,
"Gin": 90,
}
maps.Copy(dst, src)
fmt.Println(dst) // map[Go:100 Gin:90]
如果两个 Map 存在相同的键,以 src 的值为准。它等价于:
for key, value := range src {
dst[key] = value
}
典型场景是“默认配置 + 用户配置”
Equal
判断两个 Map 是否包含完全相同的键值对:
a := map[string]int{
"Go": 90,
"Gin": 80,
}
b := map[string]int{
"Gin": 80,
"Go": 90,
}
fmt.Println(maps.Equal(a, b)) // true
EqualFunc
自定义相等规则时使用 EqualFunc。
例如忽略字符串大小写:
a := map[int]string{
1: "Go",
}
b := map[int]string{
1: "GO",
}
equal := maps.EqualFunc(a, b, func(x, y string) bool {
return strings.EqualFold(x, y)
})
fmt.Println(equal) // true
值是切片时也需要 EqualFunc:
a := map[string][]int{
"scores": {80, 90},
}
b := map[string][]int{
"scores": {80, 90},
}
equal := maps.EqualFunc(a, b, func(x, y []int) bool {
return slices.Equal(x, y)
})
fmt.Println(equal) // true
DeleteFunc
按条件删除键值对:
scores := map[string]int{
"Alice": 95,
"Bob": 58,
"Jasper": 88,
}
maps.DeleteFunc(scores, func(name string, score int) bool {
return score < 60
})
fmt.Println(scores) // map[Alice:95 Jasper:88]
判断函数的含义:
func(key, value) bool {
return true // 删除
return false // 保留
}
DeleteFunc直接修改原 Map,不需要接收返回值。
os
Common Functions
| 函数 | 用途 | 典型示例 | 返回结果/输出 |
|---|---|---|---|
os.ReadFile |
一次读入整个文件 | os.ReadFile("config.yaml") |
([]byte, error) |
os.WriteFile |
覆盖写入,不存在则创建 | os.WriteFile("a.txt", data, 0644) |
error |
os.OpenFile |
自定义模式打开(追加等) | os.OpenFile("app.log", flag, 0644) |
(*os.File, error) |
os.MkdirAll |
递归创建多级目录 | os.MkdirAll("data/logs", 0755) |
error(已存在也返回 nil) |
os.Stat |
查文件信息 / 判断存在 | os.Stat("a.txt") |
(os.FileInfo, error) |
os.ReadDir |
列出目录内容 | os.ReadDir("uploads") |
([]os.DirEntry, error) |
os.Rename |
改名 / 移动 | os.Rename("a.tmp", "a.txt") |
error |
os.Remove / os.RemoveAll |
删单个 / 递归删除 | os.RemoveAll("tmp") |
error |
os.Getenv / os.LookupEnv |
读环境变量 | os.LookupEnv("PORT") |
string / (string, bool) |
os.Args |
命令行参数 | os.Args[1] |
[]string |
ReadFile
一次性把整个文件读进内存,最常用来读配置。
data, err := os.ReadFile("config.yaml")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
输出(假设 config.yaml 内容就是这两行):
text
port: 8080
db: dev
返回的是
[]byte,处理文本要先string(data)。整个文件都会进内存,只适合中小文件。
WriteFile
一次性写入:文件不存在则创建,已存在则清空重写。
report := []byte("总用户数: 1024\n活跃用户: 87\n")
if err := os.WriteFile("report.txt", report, 0644); err != nil {
log.Fatal(err)
}
执行后 report.txt 的内容变为:
总用户数: 1024
活跃用户: 87
0644是八进制权限:所有者可读写,其他人只读;目录惯用0755(多了“可进入”权限)。
OpenFile
WriteFile 只会覆盖,想在文件末尾追加(典型是写日志)必须用 OpenFile 组合 flag。
f, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
defer f.Close()
if _, err := f.WriteString("user=1001 action=login ok\n"); err != nil {
log.Fatal(err)
}
| Flag | 组别 | 作用 |
|---|---|---|
os.O_RDONLY |
访问模式 | 只读 |
os.O_WRONLY |
访问模式 | 只写 |
os.O_RDWR |
访问模式 | 可读可写 |
os.O_CREATE |
行为开关 | 不存在就创建 |
os.O_TRUNC |
行为开关 | 打开时把文件清空为 0 字节 |
os.O_APPEND |
行为开关 | 每次写自动落到文件末尾 |
os.O_EXCL |
行为开关 | 配合 O_CREATE:文件已存在则直接报错 |
os.O_SYNC |
行为开关 | 每次写同步刷进磁盘,极少用(性能代价大) |
MkdirAll
一次建好整条目录路径,已存在直接返回 nil。
if err := os.MkdirAll("data/uploads/2026-07", 0755); err != nil {
log.Fatal(err)
}
因为“已存在不报错”,它适合放在服务启动时无脑执行,确保日志目录、上传目录一定存在。对比:
os.Mkdir只能建一层,父目录不存在会直接报错,实战里基本只用MkdirAll。
Stat
判断文件是否存在
info, err := os.Stat("config.yaml")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("配置不存在,使用默认配置")
return
}
if err != nil {
log.Fatal(err)
}
ReadDir
列出目录里的条目。
假设磁盘结构:
data/uploads/
├── avatar.png
├── docs/
│ └── resume.pdf
└── report.pdf
entries, err := os.ReadDir("data/uploads")
if err != nil {
log.Fatal(err)
}
fmt.Println("条目数:", len(entries))
for _, e := range entries {
fmt.Println(e.Name(), e.IsDir())
}
输出:
条目数: 3
avatar.png false
docs true
report.pdf false
| 方法 | 返回值 | 说明 |
|---|---|---|
e.Name() |
string |
纯文件名,不带路径 |
e.IsDir() |
bool |
是否是子目录 |
e.Info() |
(os.FileInfo, error) |
详情:Size()、ModTime() 等 |
os.Rename / os.Remove / os.RemoveAll
// 实战模式:先写临时文件,成功后再改名成正式文件
if err := os.Rename("report.tmp", "report.txt"); err != nil {
log.Fatal(err)
}
if err := os.Remove("old.txt"); err != nil { // 单个文件或空目录
log.Fatal(err)
}
if err := os.RemoveAll("data/tmp"); err != nil { // 目录连同内容整个删掉
log.Fatal(err)
}
输出:程序无输出;效果是 report.tmp 变成 report.txt,old.txt 与 data/tmp 被删除。
Getenv / LookupEnv
终端设置:
export PORT=8080
Go 中读取:
port := os.Getenv("PORT")
Getenv 无法区分:
- 变量不存在
- 变量存在,但值为空
需要区分时使用 LookupEnv:
port, ok := os.LookupEnv("PORT")
if !ok {
port = "8080"
}
fmt.Println(port)
LookupEnv 的第二个返回值表示环境变量是否存在,即使变量的值是空字符串,也会返回 true。
Args
终端里的一行命令按空格切段,程序启动后在 os.Args([]string)里原样拿到:
app.exe config.yaml 8080
│ │ │
│ │ └─ os.Args[2] = "8080"
│ └─ os.Args[1] = "config.yaml"
└─ os.Args[0] = 程序本身
fmt.Printf("%q\n", os.Args)
// go run . hello world 的输出:
// ["/tmp/go-build.../main" "hello" "world"]
典型写法:先查 len 再取参数,出错用非零码退出:
if len(os.Args) < 2 {
fmt.Println("请提供用户名")
os.Exit(1) // 退出码非 0,外部脚本能感知失败
}
fmt.Println("Hello,", os.Args[1])
// go run . Jasper 的输出:Hello, Jasper
os.Args[0]是程序自身(go run时为临时构建路径,内容不固定),真参数从[1]开始;不查len直接取[1],无参数时会 panic。参数全是
string:"8080"要strconv.Atoi才能当数字。
os.Exit立即结束进程,所有 defer 不执行;复杂参数交给flag包,不手动解析。
path/filepath
path/filepath 用于处理本地文件系统路径,会自动适配当前操作系统的路径分隔符。WSL/Linux 使用 /,Windows 原生环境通常使用 \。
处理 URL 路径时不要用它,应使用
path或net/url。
以下输出按WSL/Linux 环境展示。
Common Functions
| 函数 | 用途 | 典型示例 | 结果 |
|---|---|---|---|
Join |
拼接路径 | Join("config", "app.yaml") |
config/app.yaml |
Base |
获取文件名 | Base("/app/config.yaml") |
config.yaml |
Dir |
获取所在目录 | Dir("/app/config.yaml") |
/app |
Ext |
获取扩展名 | Ext("app.prod.yaml") |
.yaml |
Clean |
清理多余的 .、..、分隔符 |
Clean("a//b/../c") |
a/c |
Abs |
转为绝对路径 | Abs("config/app.yaml") |
取决于当前目录 |
Rel |
计算相对路径 | Rel("/app", "/app/static/a.png") |
static/a.png |
WalkDir |
递归遍历目录 | 遍历配置文件 | 每个文件和目录 |
Join
因为不同系统的分隔符不同,不要手动写 "storage/" + filename。
Join 会使用当前系统的分隔符,并自动清理路径。
package main
import (
"fmt"
"path/filepath"
)
func main() {
configPath := filepath.Join("config", "app.yaml")
logPath := filepath.Join("storage", "logs", "app.log")
fmt.Println(configPath) // config/app.yaml
fmt.Println(logPath) // storage/logs/app.log
}
Base、Dir、Ext
-
Base:获取路径最后一部分,通常是文件名。
-
Dir:获取所在目录。
-
Ext:获取扩展名,包含
.。
Ext 只识别最后一个扩展名,
app.tar.gz返回.gz没有扩展名时返回空字符串
package main
import (
"fmt"
"path/filepath"
"strings"
)
func main() {
path := "/srv/app/config/app.prod.yaml"
filename := filepath.Base(path)
dir := filepath.Dir(path)
ext := filepath.Ext(path)
name := strings.TrimSuffix(filename, ext)
// 从 app.prod.yaml 末尾删除 .yaml,得到 app.prod
fmt.Println(filename) // app.prod.yaml
fmt.Println(dir) // /srv/app/config
fmt.Println(ext) // .yaml
fmt.Println(name) // app.prod
}
Clean、Abs、Rel
Clean:整理路径,删除多余的分隔符、.,并处理可以抵消的目录/..。Abs:将路径转换为绝对路径。相对路径会以程序的当前工作目录为基础。Rel:计算从基础路径前往目标路径所需的相对路径。
Clean只处理路径字符串,不检查文件或目录是否真实存在。
Abs会自动对结果执行Clean,返回值需要处理error。
Rel(basePath, targetPath)的参数顺序是“基础路径、目标路径”,结果中可能出现..。
package main
import (
"fmt"
"path/filepath"
)
func main() {
cleanPath := filepath.Clean(
"storage//logs/../data/./users.json",
)
absPath, err := filepath.Abs("config/app.yaml")
if err != nil {
fmt.Println("获取绝对路径失败:", err)
return
}
relPath, err := filepath.Rel(
"/srv/app/config",
"/srv/app/static/avatar.png",
)
if err != nil {
fmt.Println("计算相对路径失败:", err)
return
}
fmt.Println(cleanPath) // storage/data/users.json
fmt.Println(absPath) // 当前工作目录/config/app.yaml
fmt.Println(relPath) // ../static/avatar.png
}
WalkDir
WalkDir:从指定根目录开始,递归访问其中的每个目录和文件。- 每访问一个路径,就执行一次你传入的回调函数。
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
// 处理当前文件或目录
return nil
})
回调函数的三个参数:
path:当前文件或目录的完整路径。d:当前路径的信息,可以判断它是文件还是目录、获取名称等。err:访问当前路径时发生的错误。
WalkDir也会访问传入的根目录本身。
d.IsDir()用于判断当前路径是否为目录。回调返回
nil表示继续遍历;返回普通错误会立即终止遍历。
WalkDir默认不会进入符号链接指向的目录。
假设目录结构如下:
configs/
├── app.yaml
├── dev.yaml
├── readme.txt
└── production/
└── app.yaml
查找所有 .yaml 文件:
package main
import (
"fmt"
"io/fs"
"path/filepath"
)
func main() {
err := filepath.WalkDir(
"configs",
func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// 目录不处理,但仍然继续进入目录内部
if d.IsDir() {
return nil
}
if filepath.Ext(path) == ".yaml" {
fmt.Println(path)
}
return nil
},
)
if err != nil {
fmt.Println("遍历失败:", err)
}
}
输出:
configs/app.yaml
configs/dev.yaml
configs/production/app.yaml
io
Reader 与 Writer
io.Reader:表示“可以从中读取数据”的对象。io.Writer:表示“可以向其中写入数据”的对象。
它们本质上是两个接口:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
常见实现:
*os.File 既可以是 Reader,也可以是 Writer
*http.Response.Body 是 Reader
*bytes.Buffer 既可以是 Reader,也可以是 Writer
strings.Reader 是 Reader
正因为函数接收的是接口,而不是具体类型,所以同一个 io.Copy 既能复制文件,也能复制 HTTP 数据或内存中的数据。
log/slog
还能向上返回的错误,使用 fmt.Errorf 增加上下文;已经在当前层最终处理的错误,再使用 slog 记录。
一条日志通常由:时间 + 日志级别 + 消息 + 属性组成。
例如:
slog.Info("server started", "port", 8080)
输出类似:
time=2026-08-24T06:30:00.000+08:00 level=INFO msg="server started" port=8080
其中:
"server started" → 日志消息
"port" → 属性名
8080 → 属性值
Common Functions
| 函数 / 类型 | 用途 | 典型示例 |
|---|---|---|
Debug |
调试日志 | slog.Debug("config loaded") |
Info |
普通运行日志 | slog.Info("server started") |
Warn |
警告日志 | slog.Warn("redis reconnecting") |
Error |
错误日志 | slog.Error("query failed", "error", err) |
New |
创建 Logger | slog.New(handler) |
NewTextHandler |
创建文本 Handler | slog.NewTextHandler(os.Stdout, nil) |
NewJSONHandler |
创建 JSON Handler | slog.NewJSONHandler(os.Stdout, nil) |
Logger.With |
给后续日志附加固定属性 | logger.With("service", "api") |
SetDefault |
设置全局默认 Logger | slog.SetDefault(logger) |
HandlerOptions |
配置日志级别、源码位置等 | &slog.HandlerOptions{Level: slog.LevelDebug} |
Logger 与 Handler
slog 主要由两个部分组成:
Logger
│
│ 产生日志
▼
Handler
│
│ 决定怎样输出
▼
终端 / 文件 / 日志系统
handler := slog.NewTextHandler(os.Stdout, nil) // 创建文本Handler
logger := slog.New(handler) // 创建Logger
logger = logger.With(
"service", "go-web-template",
"env", "development",
)
// With 返回一个新的 Logger,并给之后所有日志自动添加固定属性键值对
logger.Info("server started", "port", 8080)
// level=INFO msg="server started" service=go-web-template env=development port=8080
slog.SetDefault(logger) // 设置为全局默认 Logger,以后无需传logger
自己创建它们的目的,就是把
slog从“能打印日志”,变成“符合 项目要求的日志系统”
HandlerOptions
HandlerOptions 的意义主要是让你统一控制整个项目到底输出哪些日志
创建 Handler 时可以传入配置:
handler := slog.NewTextHandler(
os.Stdout,
&slog.HandlerOptions{
Level: slog.LevelDebug,
AddSource: true,
},
)
Level 指定最低允许输出什么级别的日志。
例如:
Level: slog.LevelWarn
此时:
Debug ×
Info ×
Warn √
Error √
如果不指定则默认最低级别:
slog.NewTextHandler(os.Stdout, nil) // slog.LevelInfo
AddSource 用于记录日志代码所在位置:
AddSource: true
输出类似:
source=/app/main.go:25
UnmarshalText
把字符串形式的日志级别解析成 slog.Level
func (l *Level) UnmarshalText(data []byte) error
var level slog.Level
if err := level.UnmarshalText([]byte(cfg.Level)); err != nil {
return nil, fmt.Errorf("parse log level %q: %w", cfg.Level, err)
}
转换过程:
"debug"
↓
UnmarshalText
↓
slog.LevelDebug
并且忽略大小写。
UnmarshalText会直接修改level,因此不需要接收转换后的返回值。
uuid
Common Functions
| 函数 / 方法 | 用途 | 典型示例 | 返回结果 |
|---|---|---|---|
New |
生成通用 UUID,目前等价于 UUIDv4 | uuid.New() |
uuid.UUID |
NewV4 |
生成随机 UUIDv4 | uuid.NewV4() |
uuid.UUID |
NewV7 |
生成按时间排序的 UUIDv7 | uuid.NewV7() |
uuid.UUID |
Parse |
字符串解析成 UUID | uuid.Parse(text) |
(uuid.UUID, error) |
MustParse |
解析 UUID,失败直接 panic | uuid.MustParse(text) |
uuid.UUID |
String |
UUID 转字符串 | id.String() |
string |
Compare |
比较两个 UUID 的顺序 | a.Compare(b) |
int |
Nil |
获取全零 UUID | uuid.Nil() |
uuid.UUID |
Parse
将字符串解析成 uuid.UUID:
text := "019c1234-5678-7abc-8def-0123456789ab"
id, err := uuid.Parse(text)
if err != nil {
fmt.Println("UUID 格式错误")
return
}
fmt.Println(id)
处理用户输入、URL 参数、请求参数时应该使用 Parse:
id, err := uuid.Parse(c.Param("id"))
if err != nil {
// 返回参数格式错误
}
因为外部传入的字符串不能保证一定合法。
MustParse
MustParse 同样把字符串转换成 UUID:
id := uuid.MustParse(
"019c1234-5678-7abc-8def-0123456789ab",
)
区别是解析失败时不会返回 error,而是直接 panic。
因此适合已经确定合法的固定值:
var systemID = uuid.MustParse(
"019c1234-5678-7abc-8def-0123456789ab",
)
不要这样处理用户输入:
id := uuid.MustParse(c.Param("id"))
因为用户只要传入:
abc
程序就可能 panic。
简单记:
外部输入 → Parse
程序内固定 UUID → MustParse
Nil
UUID 也存在“零值 UUID”:
nilID := uuid.Nil()
fmt.Println(nilID)
输出:
00000000-0000-0000-0000-000000000000
uuid.UUID 本质是数组,所以可以直接使用 == 比较:
id := uuid.Nil()
if id == uuid.Nil() {
fmt.Println("UUID 为空")
}
Compare
比较两个 UUID 的排序:
a := uuid.NewV7()
b := uuid.NewV7()
result := a.Compare(b)
返回规则:
-1 a 在 b 前面
0 a 和 b 相同
+1 a 在 b 后面
例如:
if a.Compare(b) < 0 {
fmt.Println("a 排在 b 前面")
}
对于 UUIDv7,这个排序通常具有实际意义,因为 UUID 中包含时间信息。
工程常见用法
例如创建用户:
type User struct {
ID uuid.UUID
Name string
}
func NewUser(name string) User {
return User{
ID: uuid.NewV7(),
Name: name,
}
}
调用:
user := NewUser("Jasper")
fmt.Println(user.ID)
整个过程:
创建用户
↓
uuid.NewV7()
↓
生成 128 位业务 ID
↓
保存到数据库
↓
API / 日志中使用字符串形式展示
如果只是需要随机唯一标识:
uuid.New()
如果明确作为数据库中的业务 ID:
uuid.NewV7()
如果解析外部传进来的 UUID:
uuid.Parse(text)
这三个基本覆盖 Go Web 项目里绝大多数 UUID 使用场景。