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)) // AAAAAThiế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.
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
CountingReaderbao 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ứcRead(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.EOFkhi đã đọc hết nguồn.Tạo một struct
UppercaseWriterlư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ứcWrite(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ứcResult() 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) *CountingReadervà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
CountingReadervớiio.ReadAllđể đọc toàn bộ dữ liệu. Truyền dữ liệu đó quaUppercaseWriterbằng phương thứcWrite.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 WorldKết quả đầu ra sẽ là:
Original: Hello World
Uppercase: HELLO WORLD
Bytes read: 11Và với đầu vào:
Go interfaces are powerfulKế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)
}
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
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp11Thư viện chuẩn và OOP
io.Reader và io.Writersort.Interfaceinterface fmt.Stringerencoding/json với structinterface http.HandlerÔn tập: mô hình REST API3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online