Skip to content

Latest commit

 

History

History
162 lines (115 loc) · 5.54 KB

01-stdlib-core.md

File metadata and controls

162 lines (115 loc) · 5.54 KB

标准库核心

Java 工程师学 Go,最先被低估的往往是标准库。没有 Spring 全家桶,也没有 Apache Commons 补洞——大量日常工作靠 os、io、bufio、encoding/json 就能做完。这一节把写 CLI 和服务时最常用的包过一遍,并对照 Java 的习惯。

io.Reader / io.Writer:组合优于继承的现场

n, err := io.Copy(dst, src)
data, err := io.ReadAll(r)

io.Reader 只有一个方法 Read([]byte) (int, error)。几乎所有「能读字节」的东西都实现它:文件、HTTP body、压缩流、bytes.Buffer、strings.Reader。

对比 Java:InputStream / Reader 分裂、装饰器层层嵌套(BufferedInputStream → GZIPInputStream → …)。Go 用小接口 + 函数包装:

gz, _ := gzip.NewReader(file)
defer gz.Close()
scanner := bufio.NewScanner(gz) // 任意 Reader 都能套

原则:函数参数收 io.Reader/io.Writer,不要收 *os.File。测试时塞 strings.NewReader 即可,不必碰磁盘。

bufio:带缓冲的扫描

f, _ := os.Open("app.log")
defer f.Close()
sc := bufio.NewScanner(f)
for sc.Scan() {
    line := sc.Text()
    _ = line
}
if err := sc.Err(); err != nil {
    return err
}

默认每行上限约 64KB,超长行要 sc.Buffer(buf, max)。对比 Java 的 BufferedReader.readLine()——语义接近,但 Go 把错误留到循环外用 Err() 检查。

io/fs 与 embed

io/fs.FS 抽象了「只读文件系统」:

func Count(fsys fs.FS, root string) error {
    return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
        // ...
        return nil
    })
}

测试用 fstest.MapFS,生产用 os.DirFS("."),行为一致。项目 01、10 都依赖这个抽象。

embed 把文件编译进二进制(对比 classpath resource):

//go:embed templates/*
var templatesFS embed.FS

单文件部署时,模板、迁移 SQL、静态资源都不用再带目录。项目 11、14 会用到。

path/filepath:跨平台路径

永远用 filepath.Join、filepath.Clean、filepath.WalkDir,不要手拼 "/"。Windows 上 \ 与 / 的差异由这个包消化。

注意:path 包是给 URL 路径用的;文件系统路径用 path/filepath。

os/exec:调外部命令

cmd := exec.CommandContext(ctx, "git", "log", "--oneline")
cmd.Dir = repoDir
out, err := cmd.Output()

对比 ProcessBuilder:Go 把 stdout/stderr/退出码模型收得更紧。务必用 CommandContext,这样取消能杀掉子进程。项目 12 用它读 git log。

安全提醒:不要把未校验的用户输入拼进 shell;优先 Command(name, args...) 而不是 bash -c。

encoding/json

type User struct {
    ID    int64  `json:"id"`
    Email string `json:"email"`
    Secret string `json:"-"` // 不序列化
}

data, err := json.Marshal(u)
err = json.NewDecoder(r).Decode(&u)

对照 Jackson/Gson:

点 Java Go
字段可见性 getter / @JsonProperty 导出字段(大写)+ struct tag
未知字段 默认忽略或 FAIL_ON_UNKNOWN Decoder.DisallowUnknownFields()
原始片段 JsonNode json.RawMessage
流式 Jackson Streaming API json.Encoder/Decoder

json.RawMessage 适合「先看 type 字段再决定怎么解」的多态 JSON。

time.Duration 是有类型的

timeout := 5 * time.Second
time.Sleep(timeout)

time.Duration 底层是 int64 纳秒,不是浮点秒。写 Sleep(5) 能编译但语义是 5 纳秒——这是从 Java TimeUnit 过来最容易踩的坑之一。API 里超时参数请直接收 time.Duration,别收 int 秒。

小抄:什么时候用哪个包

需求 包
读文件/目录 os, io/fs
按行扫描 bufio
打包进二进制 embed
路径 path/filepath
子进程 os/exec
JSON encoding/json
时间与超时 time

资源所有权与错误边界

Go 标准库经常返回“需要调用方关闭”的对象,例如 *os.File、http.Response.Body、*sql.Rows。一般规则是:成功获得资源的一方负责释放。

f, err := fsys.Open(name)
if err != nil {
    return err
}
defer f.Close()

不要在 Open 失败前就 defer f.Close(),因为此时 f 可能是 nil。也不要在循环中无限堆积 defer;如果要处理大量文件,把单文件逻辑抽成函数,让 defer 在每次调用结束时执行。

常见设计反模式

  • 参数收 *os.File,实际只需要 io.Reader。
  • 手工拼文件路径或 URL 路径,混用 path 与 filepath。
  • 用 io.ReadAll 读取无上限的网络输入。
  • 忽略 Scanner.Err()、Rows.Err() 这类延迟报告的错误。
  • 为了调一个外部命令而启动 shell,平白引入转义和注入问题。

练习

配套材料:题目骨架 · 参考答案

  1. 用 fstest.MapFS 构造三个文件,统计总字节数,整个测试不访问磁盘。
  2. 把一个只接收 *os.File 的函数改为接收 io.Reader,然后用 strings.NewReader 测试。
  3. 为 bufio.Scanner 构造一行超过 64KB 的输入,观察默认失败并调整 buffer。

下一节:CLI 与 slog

配套项目:10 代码行数统计 · 11 脚手架生成器 · 12 Git changelog