本章節涵蓋 Go 語言的輸入輸出操作,包括字串處理、io 套件、緩衝 I/O 以及網路程式設計。

strings 套件#

strings.Builder#

strings.Builder 是用於高效建構字串的型別:

import "strings"

var builder strings.Builder

builder.WriteString("Hello, ")
builder.WriteString("World!")
builder.WriteByte('!')
builder.WriteRune('🎉')

result := builder.String()  // "Hello, World!!🎉"
fmt.Println(builder.Len())  // 目前長度

為什麼使用 strings.Builder

Go 的字串是不可變的,每次 + 連線都會建立新字串並複製內容。使用 strings.Builder 可以避免這種重複組態和複製。

// 低效做法(大量記憶體組態)
s := ""
for i := 0; i < 1000; i++ {
    s += "a"  // 每次都建立新字串
}

// 高效做法
var builder strings.Builder
for i := 0; i < 1000; i++ {
    builder.WriteString("a")
}
s := builder.String()

Builder 內部機制#

strings.Builder 內部使用 []byte 切片儲存內容:

type Builder struct {
    addr *Builder  // 用於檢測非法複製
    buf  []byte    // 內部緩衝區
}
預分配緩衝區

如果知道最終字串的大致長度,可以預先分配緩衝區:

var builder strings.Builder
builder.Grow(1000)  // 預分配 1000 位元組

for i := 0; i < 100; i++ {
    builder.WriteString("0123456789")
}
// 不會觸發擴容

strings.Builder 不能被複製後繼續使用。複製後呼叫方法會 panic:

var b1 strings.Builder
b1.WriteString("hello")

b2 := b1  // 複製
// b2.WriteString("world")  // panic!

strings.Reader#

strings.Reader 讓字串實作 io.Reader 等介面:

reader := strings.NewReader("Hello, World!")

// 實作的介面
// io.Reader, io.ReaderAt, io.WriterTo
// io.Seeker, io.ByteScanner, io.RuneScanner

buf := make([]byte, 5)
n, _ := reader.Read(buf)  // n=5, buf="Hello"

// 重置讀取位置
reader.Reset("New content")

常用字串函式#

s := "Hello, World!"

// 查找
strings.Contains(s, "World")     // true
strings.HasPrefix(s, "Hello")    // true
strings.HasSuffix(s, "!")        // true
strings.Index(s, "o")            // 4
strings.LastIndex(s, "o")        // 8

// 轉換
strings.ToUpper(s)               // "HELLO, WORLD!"
strings.ToLower(s)               // "hello, world!"
strings.TrimSpace("  hi  ")      // "hi"
strings.Trim(s, "!")             // "Hello, World"

// 分割與連線
parts := strings.Split("a,b,c", ",")   // ["a", "b", "c"]
joined := strings.Join(parts, "-")      // "a-b-c"

// 替換
strings.Replace(s, "World", "Go", 1)    // "Hello, Go!"
strings.ReplaceAll(s, "l", "L")         // "HeLLo, WorLd!"

bytes 套件#

bytes.Buffer#

bytes.Buffer 提供可讀寫的位元組緩衝區:

import "bytes"

var buf bytes.Buffer

// 寫入操作
buf.WriteString("Hello")
buf.WriteByte(' ')
buf.Write([]byte("World"))

// 讀取操作
data := buf.Bytes()    // 取得內容(不消耗)
str := buf.String()    // 轉為字串

line, _ := buf.ReadBytes('\n')  // 讀取直到分隔符

Buffer vs Builder 比較#

特性bytes.Bufferstrings.Builder
用途讀寫皆可只寫
輸出[]bytestring只能 string
讀取支援 Read、ReadByte 等不支援
適用I/O 操作、資料處理字串建構
  • 只需要建構字串 -> 使用 strings.Builder
  • 需要讀寫操作 -> 使用 bytes.Buffer
  • 處理二進位資料 -> 使用 bytes.Buffer

io 套件#

核心介面#

io 套件定義了 Go 語言 I/O 操作的核心介面:

// 讀取介面
type Reader interface {
    Read(p []byte) (n int, err error)
}

// 寫入介面
type Writer interface {
    Write(p []byte) (n int, err error)
}

// 關閉介面
type Closer interface {
    Close() error
}

// 組合介面
type ReadWriter interface {
    Reader
    Writer
}

type ReadCloser interface {
    Reader
    Closer
}

type WriteCloser interface {
    Writer
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

Go 的 I/O 設計遵循「小介面」原則。透過組合這些基本介面,可以靈活地描述各種 I/O 能力。

io.Reader 詳解#

// Read 方法契約:
// - 讀取最多 len(p) 位元組到 p
// - 回傳讀取的位元組數 n(0 <= n <= len(p))
// - 到達結尾時回傳 n, io.EOF
// - 可能同時回傳資料和錯誤

func ReadAll(r io.Reader) ([]byte, error) {
    var result []byte
    buf := make([]byte, 1024)

    for {
        n, err := r.Read(buf)
        if n > 0 {
            result = append(result, buf[:n]...)
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return nil, err
        }
    }
    return result, nil
}
常見 io.Reader 實作
型別說明
*os.File檔案讀取
*strings.Reader從字串讀取
*bytes.Reader從位元組切片讀取
*bytes.Buffer從緩衝區讀取
net.Conn網路連線讀取
*http.Response.BodyHTTP 回應讀取
*gzip.Reader解壓縮讀取

io.Writer 詳解#

// Write 方法契約:
// - 將 p 中的資料寫入底層資料流
// - 回傳寫入的位元組數 n(0 <= n <= len(p))
// - 若 n < len(p),必須回傳非 nil 的錯誤

type UpperWriter struct {
    w io.Writer
}

func (u *UpperWriter) Write(p []byte) (int, error) {
    upper := bytes.ToUpper(p)
    return u.w.Write(upper)
}

// 使用
var buf bytes.Buffer
uw := &UpperWriter{w: &buf}
uw.Write([]byte("hello"))
fmt.Println(buf.String())  // "HELLO"

實用 I/O 函式#

import "io"

// 複製資料
n, err := io.Copy(dst, src)  // 完整複製
n, err := io.CopyN(dst, src, 1024)  // 複製 1024 位元組

// 讀取全部
data, err := io.ReadAll(reader)

// 寫入字串
n, err := io.WriteString(writer, "hello")

// 讀取指定數量
buf := make([]byte, 10)
n, err := io.ReadFull(reader, buf)  // 必須讀滿 10 位元組

// 限制讀取
limitedReader := io.LimitReader(reader, 1024)  // 最多讀 1024 位元組

// 多重讀取
tee := io.TeeReader(src, dst)  // 讀取的同時寫入 dst

io.Pipe#

建立同步的記憶體管道:

pr, pw := io.Pipe()

go func() {
    defer pw.Close()
    pw.Write([]byte("Hello from pipe"))
}()

data, _ := io.ReadAll(pr)
fmt.Println(string(data))  // "Hello from pipe"

bufio 套件#

緩衝讀取#

import "bufio"

file, _ := os.Open("large_file.txt")
reader := bufio.NewReader(file)

// 按行讀取
for {
    line, err := reader.ReadString('\n')
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatal(err)
    }
    fmt.Print(line)
}

// 或使用 Scanner(更簡潔)
file.Seek(0, 0)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

緩衝寫入#

file, _ := os.Create("output.txt")
writer := bufio.NewWriter(file)

writer.WriteString("Hello\n")
writer.WriteString("World\n")

// 必須刷新緩衝區
writer.Flush()
file.Close()

使用 bufio.Writer 時,必須在結束前呼叫 Flush() 確保所有資料都被寫入。否則緩衝區中的資料可能會遺失。

Scanner 分詞器#

// 預設按行分割
scanner := bufio.NewScanner(strings.NewReader("word1 word2 word3"))

// 改為按單詞分割
scanner.Split(bufio.ScanWords)

for scanner.Scan() {
    fmt.Println(scanner.Text())  // word1, word2, word3
}

// 內建的分割函式
// bufio.ScanLines  - 按行(預設)
// bufio.ScanWords  - 按單詞
// bufio.ScanRunes  - 按 rune
// bufio.ScanBytes  - 按位元組
自訂分割函式
// 以逗號分割
splitComma := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
    if atEOF && len(data) == 0 {
        return 0, nil, nil
    }
    if i := bytes.IndexByte(data, ','); i >= 0 {
        return i + 1, data[0:i], nil
    }
    if atEOF {
        return len(data), data, nil
    }
    return 0, nil, nil
}

scanner := bufio.NewScanner(strings.NewReader("a,b,c,d"))
scanner.Split(splitComma)
for scanner.Scan() {
    fmt.Println(scanner.Text())  // a, b, c, d
}

檔案操作#

基本檔案 I/O#

import "os"

// 讀取整個檔案
data, err := os.ReadFile("file.txt")

// 寫入整個檔案
err := os.WriteFile("file.txt", []byte("content"), 0644)

// 開啟檔案
file, err := os.Open("file.txt")        // 唯讀
file, err := os.Create("new.txt")       // 建立(覆寫)
file, err := os.OpenFile("file.txt",
    os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)  // 自訂模式

defer file.Close()

檔案資訊#

info, err := os.Stat("file.txt")
if os.IsNotExist(err) {
    fmt.Println("檔案不存在")
    return
}

fmt.Println("名稱:", info.Name())
fmt.Println("大小:", info.Size())
fmt.Println("修改時間:", info.ModTime())
fmt.Println("是否為目錄:", info.IsDir())
fmt.Println("權限:", info.Mode())

目錄操作#

// 建立目錄
os.Mkdir("dir", 0755)
os.MkdirAll("dir/sub/deep", 0755)  // 遞迴建立

// 列出目錄
entries, _ := os.ReadDir(".")
for _, entry := range entries {
    info, _ := entry.Info()
    fmt.Printf("%s %8d %s\n",
        info.Mode(), info.Size(), entry.Name())
}

// 遍歷目錄樹
filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
    if err != nil {
        return err
    }
    fmt.Println(path)
    return nil
})

網路程式設計#

TCP 用戶端#

import "net"

func main() {
    // 建立連線
    conn, err := net.Dial("tcp", "example.com:80")
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // 傳送請求
    fmt.Fprintf(conn, "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")

    // 讀取回應
    response, _ := io.ReadAll(conn)
    fmt.Println(string(response))
}

TCP 伺服器#

func main() {
    // 監聽埠口
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }
    defer listener.Close()

    fmt.Println("伺服器啟動於 :8080")

    for {
        // 接受連線
        conn, err := listener.Accept()
        if err != nil {
            log.Println("Accept error:", err)
            continue
        }

        // 處理連線(併發)
        go handleConnection(conn)
    }
}

func handleConnection(conn net.Conn) {
    defer conn.Close()

    reader := bufio.NewReader(conn)
    for {
        message, err := reader.ReadString('\n')
        if err != nil {
            return
        }
        fmt.Printf("收到: %s", message)
        conn.Write([]byte("收到訊息\n"))
    }
}

設定逾時#

// 連線逾時
conn, err := net.DialTimeout("tcp", "example.com:80", 5*time.Second)

// 讀寫逾時
conn.SetDeadline(time.Now().Add(10 * time.Second))      // 同時設定
conn.SetReadDeadline(time.Now().Add(5 * time.Second))   // 讀取逾時
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))  // 寫入逾時

在生產環境中,務必設定適當的逾時時間。沒有逾時的網路操作可能導致 goroutine 永久阻塞。

UDP 通訊#

// UDP 用戶端
conn, _ := net.Dial("udp", "example.com:53")
defer conn.Close()
conn.Write([]byte("hello"))

// UDP 伺服器
addr := net.UDPAddr{Port: 8080}
conn, _ := net.ListenUDP("udp", &addr)
defer conn.Close()

buf := make([]byte, 1024)
n, remoteAddr, _ := conn.ReadFromUDP(buf)
fmt.Printf("收到來自 %s: %s\n", remoteAddr, buf[:n])
HTTP 伺服器範例
import "net/http"

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
    })

    http.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{
            "status": "ok",
        })
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Context 與網路操作#

import "context"

func fetchData(ctx context.Context, url string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}

func main() {
    // 建立帶逾時的 context
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    data, err := fetchData(ctx, "https://example.com/api")
    if err != nil {
        if ctx.Err() == context.DeadlineExceeded {
            log.Println("請求逾時")
        }
        return
    }

    fmt.Println(string(data))
}

在網路操作中使用 context.Context 是最佳實務:

  1. 可以設定逾時和取消
  2. 可以傳遞請求範圍的值
  3. 可以優雅地終止整個請求鏈