Menu
CoddyTech

io.Reader và io.Writer

Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 73 trên 107.

Thư viện chuẩn của Go thể hiện thiết kế dựa trên giao diện thông qua hai giao diện cơ bản: io.Reader và io.Writer. Những giao diện đơn giản này hỗ trợ mọi thứ, từ thao tác với tệp đến giao tiếp mạng.

Interface io.Reader chỉ yêu cầu một phương thức:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Phương thức này đọc tối đa len(p) byte vào lát cắt và trả về số byte đã đọc. Tương tự, io.Writer định nghĩa:

type Writer interface {
    Write(p []byte) (n int, err error)
}

Bất kỳ kiểu nào triển khai các phương thức này đều hoạt động với toàn bộ hệ sinh thái I/O.

Đây là một kiểu tùy chỉnh triển khai io.Reader:

type RepeatReader struct {
    Char  byte
    Count int
    pos   int
}

func (r *RepeatReader) Read(p []byte) (int, error) {
    if r.pos >= r.Count {
        return 0, io.EOF
    }
    n := 0
    for n < len(p) && r.pos < r.Count {
        p[n] = r.Char
        n++
        r.pos++
    }
    return n, nil
}

Giờ đây, reader tùy chỉnh này hoạt động với mọi hàm cần một io.Reader:

reader := &RepeatReader{Char: 'A', Count: 5}
data, _ := io.ReadAll(reader)
fmt.Println(string(data))  // AAAAA

Thiết kế này cho phép bạn viết các hàm (function) nhận io.Reader hoặc io.Writer, nhờ đó chúng có thể hoạt động với tệp, kết nối mạng, bộ đệm hoặc bất kỳ cách triển khai tùy chỉnh nào.

challenge icon

Thử thách

Dễ

Hãy xây dựng một bộ xử lý luồng văn bản tùy chỉnh triển khai các interface io.Reader và io.Writer! Bạn sẽ tạo các kiểu có thể biến đổi văn bản khi dữ liệu đi qua chúng, qua đó cho thấy những interface nền tảng này giúp kết hợp các thao tác I/O mạnh mẽ như thế nào.

Bạn sẽ tổ chức code trong hai tệp:

  • streams.go: Định nghĩa các kiểu reader và writer tùy chỉnh.

    Tạo một struct CountingReader bao bọc một chuỗi và theo dõi số byte đã đọc. Struct này cần có các trường lưu chuỗi nguồn và vị trí hiện tại. Triển khai phương thức Read(p []byte) (int, error) để đọc byte từ nguồn vào slice đã cung cấp, cập nhật vị trí và trả về io.EOF khi đã đọc hết nguồn.

    Tạo một struct UppercaseWriter lưu dữ liệu được ghi và chuyển dữ liệu đó thành chữ hoa. Struct này cần lưu kết quả tích lũy bên trong. Triển khai phương thức Write(p []byte) (int, error) để chuyển các byte đầu vào thành chữ hoa rồi lưu lại. Thêm phương thức Result() string để lấy văn bản chữ hoa đã tích lũy.

    Tạo các hàm khởi tạo NewCountingReader(source string) *CountingReader và NewUppercaseWriter() *UppercaseWriter để khởi tạo các kiểu của bạn đúng cách.

  • main.go: Minh họa cách các kiểu I/O tùy chỉnh hoạt động với thư viện chuẩn.

    Đọc một chuỗi đầu vào, sau đó dùng CountingReader với io.ReadAll để đọc toàn bộ dữ liệu. Truyền dữ liệu đó qua UppercaseWriter bằng phương thức Write.

    In kết quả theo định dạng sau:

    Original: [input]
    Uppercase: [result from writer]
    Bytes read: [total bytes]

Đầu vào sẽ có dạng sau:

  • Một dòng văn bản

Ví dụ, với đầu vào:

Hello World

Kết quả đầu ra sẽ là:

Original: Hello World
Uppercase: HELLO WORLD
Bytes read: 11

Và với đầu vào:

Go interfaces are powerful

Kết quả đầu ra sẽ là:

Original: Go interfaces are powerful
Uppercase: GO INTERFACES ARE POWERFUL
Bytes read: 26

Điểm mấu chốt là CountingReader hoạt động trơn tru với io.ReadAll vì nó triển khai io.Reader. Mọi hàm trong hệ sinh thái Go nhận một io.Reader đều có thể hoạt động với kiểu tùy chỉnh của bạn. Đó chính là sức mạnh của thiết kế dựa trên interface.

Tự làm thử

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	// Đọc đầu vào
	reader := bufio.NewReader(os.Stdin)
	input, _ := reader.ReadString('\n')
	// Xóa ký tự xuống dòng ở cuối nếu có
	if len(input) > 0 && input[len(input)-1] == '\n' {
		input = input[:len(input)-1]
	}

	// TODO: Tạo CountingReader từ chuỗi đầu vào
	// countingReader := NewCountingReader(input)

	// TODO: Dùng io.ReadAll để đọc toàn bộ dữ liệu từ CountingReader
	// data, err := io.ReadAll(countingReader)

	// TODO: Tạo UppercaseWriter
	// upperWriter := NewUppercaseWriter()

	// TODO: Ghi dữ liệu vào UppercaseWriter
	// upperWriter.Write(data)

	// TODO: In kết quả theo định dạng yêu cầu:
	// Ban đầu: [input]
	// Chữ in hoa: [kết quả từ bộ ghi]
	// Số byte đã đọc: [tổng số byte]

	// Mã tạm để tránh lỗi import không dùng đến, xóa khi triển khai
	_ = io.EOF
	fmt.Println("Original:", input)
	// fmt.Println("Uppercase:", upperWriter.Result())
	// fmt.Println("Bytes read:", countingReader.Position)
}
quiz iconTự kiểm tra

Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.

Tất cả bài học trong Lập trình hướng đối tượng

Tự luyện tập: Trình biên dịch Go online