本章節涵蓋 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.Buffer | strings.Builder |
|---|---|---|
| 用途 | 讀寫皆可 | 只寫 |
| 輸出 | []byte 或 string | 只能 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.Body | HTTP 回應讀取 |
*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) // 讀取的同時寫入 dstio.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是最佳實務:
- 可以設定逾時和取消
- 可以傳遞請求範圍的值
- 可以優雅地終止整個請求鏈