Система ввода-вывода Go построена на двух фундаментальных интерфейсах: io.Reader и io.Writer. Вся экосистема -- от файлов до сети, от сжатия до шифрования -- использует эти интерфейсы. Понимание io -- ключ к написанию эффективного и компонуемого кода.
io пакет: фундаментальные интерфейсы
io.Reader и io.Writer
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Это самые важные интерфейсы в Go. Их реализуют:
*os.File-- файлы*bytes.Buffer-- буфер в памяти*strings.Reader-- чтение из строкиnet.Conn-- сетевые соединения*http.Request.Body-- тело HTTP-запроса*gzip.Writer-- сжатие*cipher.StreamWriter-- шифрование
io.Closer и io.Seeker
type Closer interface {
Close() error
}
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
// Composed interfaces
type ReadCloser interface {
Reader
Closer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
type ReadWriteSeeker interface {
Reader
Writer
Seeker
}
io.Copy -- универсальное копирование
// Copy from reader to writer -- the most used io function
func copyFile(dst, src string) error {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("opening source: %w", err)
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return fmt.Errorf("creating destination: %w", err)
}
defer out.Close()
// io.Copy uses a 32KB buffer internally
written, err := io.Copy(out, in)
if err != nil {
return fmt.Errorf("copying: %w", err)
}
fmt.Printf("Copied %d bytes\n", written)
return out.Close() // Important: check close error for writes!
}
io.ReadAll -- прочитать всё
func readResponse(resp *http.Response) ([]byte, error) {
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response: %w", err)
}
return data, nil
}
Осторожно:
io.ReadAllзагружает весь контент в память. Для больших файлов используйте потоковую обработку черезio.Copyилиbufio.Scanner.
io.TeeReader -- читать и копировать одновременно
// TeeReader: everything read from r is also written to w
func logAndProcess(r io.Reader) error {
var buf bytes.Buffer
tee := io.TeeReader(r, &buf)
// Process the data (reads go to both processor and buf)
if err := process(tee); err != nil {
return err
}
// buf now contains everything that was read
log.Printf("Processed data: %s", buf.String())
return nil
}
io.Pipe -- синхронный поток
func streamJSON(users []User) io.Reader {
pr, pw := io.Pipe()
go func() {
enc := json.NewEncoder(pw)
for _, u := range users {
if err := enc.Encode(u); err != nil {
pw.CloseWithError(err)
return
}
}
pw.Close()
}()
return pr // Reader that yields JSON as it's encoded
}
io.MultiReader и io.MultiWriter
// MultiReader: concatenate multiple readers
func concatenateFiles(files ...string) io.Reader {
readers := make([]io.Reader, 0, len(files)*2)
for i, f := range files {
file, _ := os.Open(f)
readers = append(readers, file)
if i < len(files)-1 {
readers = append(readers, strings.NewReader("\n---\n"))
}
}
return io.MultiReader(readers...)
}
// MultiWriter: write to multiple destinations at once
func loggedWriter(w io.Writer) io.Writer {
logFile, _ := os.Create("output.log")
return io.MultiWriter(w, logFile) // Writes go to both
}
io.LimitReader -- ограничение чтения
// Prevent reading more than 1MB
func safeRead(r io.Reader) ([]byte, error) {
const maxSize = 1 << 20 // 1 MB
limited := io.LimitReader(r, maxSize)
return io.ReadAll(limited)
}
os пакет: файлы и окружение
Открытие и создание файлов
// Read-only open
f, err := os.Open("data.txt") // Same as os.OpenFile("data.txt", os.O_RDONLY, 0)
if err != nil {
return fmt.Errorf("opening file: %w", err)
}
defer f.Close()
// Create (truncate if exists, create if not)
f, err := os.Create("output.txt") // Same as os.OpenFile("output.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
return fmt.Errorf("creating file: %w", err)
}
defer f.Close()
// OpenFile with specific flags
f, err := os.OpenFile("app.log",
os.O_APPEND|os.O_CREATE|os.O_WRONLY, // Append, create if needed, write-only
0644, // rw-r--r--
)
if err != nil {
return fmt.Errorf("opening log file: %w", err)
}
defer f.Close()
os.ReadFile и os.WriteFile (Go 1.16+)
Удобные функции для простых операций:
// Read entire file into memory
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("reading config: %w", err)
}
// Write data to file (creates or truncates)
err := os.WriteFile("output.txt", []byte("Hello, World!"), 0644)
if err != nil {
return fmt.Errorf("writing file: %w", err)
}
Права доступа (file permissions)
// Common permission patterns:
// 0644 -- owner: rw, group: r, others: r (files)
// 0755 -- owner: rwx, group: rx, others: rx (directories, executables)
// 0600 -- owner: rw only (secrets, keys)
// 0700 -- owner: rwx only (private directories)
os.WriteFile("public.txt", data, 0644)
os.WriteFile("secret.key", key, 0600)
os.Mkdir("mydir", 0755)
os.MkdirAll("path/to/deep/dir", 0755)
os.Stat -- информация о файле
info, err := os.Stat("myfile.txt")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist")
return
}
if err != nil {
return fmt.Errorf("stat: %w", err)
}
fmt.Printf("Name: %s\n", info.Name())
fmt.Printf("Size: %d bytes\n", info.Size())
fmt.Printf("Mode: %s\n", info.Mode())
fmt.Printf("Modified: %s\n", info.ModTime())
fmt.Printf("IsDir: %v\n", info.IsDir())
Переменные окружения
// Get environment variable
dbHost := os.Getenv("DB_HOST")
if dbHost == "" {
dbHost = "localhost" // Default
}
// Get with existence check
port, ok := os.LookupEnv("PORT")
if !ok {
port = "8080"
}
// Set environment variable
os.Setenv("APP_ENV", "test")
// All environment variables
for _, env := range os.Environ() {
// env is "KEY=VALUE"
parts := strings.SplitN(env, "=", 2)
fmt.Printf("%s = %s\n", parts[0], parts[1])
}
os.Args -- аргументы командной строки
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: %s <filename>\n", os.Args[0])
os.Exit(1)
}
filename := os.Args[1]
// Process file...
}
filepath пакет: работа с путями
import "path/filepath"
// Join path components (handles separators correctly)
path := filepath.Join("home", "user", "documents", "file.txt")
// Linux: "home/user/documents/file.txt"
// Windows: "home\\user\\documents\\file.txt"
// Get directory and filename
dir := filepath.Dir("/home/user/file.txt") // "/home/user"
base := filepath.Base("/home/user/file.txt") // "file.txt"
ext := filepath.Ext("archive.tar.gz") // ".gz"
// Clean path (resolve .., . etc)
clean := filepath.Clean("/home/user/../admin/./file.txt")
// "/home/admin/file.txt"
// Absolute path
abs, err := filepath.Abs("relative/path")
// Match pattern
matched, err := filepath.Match("*.go", "main.go") // true
filepath.WalkDir -- обход дерева каталогов
// WalkDir is more efficient than Walk (Go 1.16+)
func findGoFiles(root string) ([]string, error) {
var files []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err // Propagate errors
}
// Skip hidden directories
if d.IsDir() && strings.HasPrefix(d.Name(), ".") {
return filepath.SkipDir
}
// Collect .go files
if !d.IsDir() && filepath.Ext(path) == ".go" {
files = append(files, path)
}
return nil
})
return files, err
}
embed пакет (Go 1.16+)
Директива //go:embed встраивает файлы прямо в бинарник при компиляции:
import "embed"
// Embed single file as string
//go:embed version.txt
var version string
// Embed single file as bytes
//go:embed config/default.json
var defaultConfig []byte
// Embed directory as filesystem
//go:embed templates/*
var templatesFS embed.FS
// Embed multiple patterns
//go:embed static/*.css static/*.js static/*.html
var staticFiles embed.FS
func main() {
fmt.Println("Version:", version)
// Read from embedded FS
data, err := templatesFS.ReadFile("templates/index.html")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
// List embedded files
entries, _ := templatesFS.ReadDir("templates")
for _, e := range entries {
fmt.Println(e.Name())
}
}
Embed с HTTP-сервером
//go:embed static
var staticFS embed.FS
func main() {
// Serve embedded static files
staticHandler := http.FileServer(http.FS(staticFS))
http.Handle("/static/", staticHandler)
// Or strip prefix
sub, _ := fs.Sub(staticFS, "static")
http.Handle("/", http.FileServer(http.FS(sub)))
http.ListenAndServe(":8080", nil)
}
io/fs пакет: абстракция файловой системы
Go 1.16 добавил интерфейс fs.FS -- абстракцию над файловой системой:
type FS interface {
Open(name string) (File, error)
}
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
fs.WalkDir и fs.Sub
// Works with any fs.FS: os filesystem, embed.FS, zip files, etc.
func countFiles(fsys fs.FS) (int, error) {
count := 0
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
count++
}
return nil
})
return count, err
}
// fs.Sub creates a sub-filesystem
func getTemplates(fsys fs.FS) (fs.FS, error) {
return fs.Sub(fsys, "templates")
}
Тестирование с fs.FS
import "testing/fstest"
func TestCountFiles(t *testing.T) {
// Create in-memory filesystem for testing
testFS := fstest.MapFS{
"file1.txt": {Data: []byte("hello")},
"file2.txt": {Data: []byte("world")},
"subdir/file3.txt": {Data: []byte("nested")},
}
count, err := countFiles(testFS)
if err != nil {
t.Fatal(err)
}
if count != 3 {
t.Errorf("count = %d, want 3", count)
}
}
bufio: буферизированный I/O
bufio.Scanner -- чтение по строкам
func countLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
// Read file line by line
func processLines(filename string) error {
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("opening file: %w", err)
}
defer f.Close()
scanner := bufio.NewScanner(f)
// Increase buffer for long lines (default 64KB max)
buf := make([]byte, 0, 1024*1024)
scanner.Buffer(buf, 10*1024*1024) // Max 10MB per line
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text() // or scanner.Bytes() for []byte
fmt.Printf("%d: %s\n", lineNum, line)
}
return scanner.Err()
}
Пользовательский split-функция
// Split by words (built-in)
scanner.Split(bufio.ScanWords)
// Split by runes
scanner.Split(bufio.ScanRunes)
// Custom: split by paragraphs (double newline)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.Index(data, []byte("\n\n")); i >= 0 {
return i + 2, data[:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil // Need more data
})
bufio.Reader и bufio.Writer
// Buffered reader -- reduces system calls
br := bufio.NewReader(file)
br := bufio.NewReaderSize(file, 64*1024) // Custom buffer size
line, err := br.ReadString('\n') // Read until delimiter
b, err := br.ReadByte() // Read single byte
peek, err := br.Peek(10) // Peek without consuming
// Buffered writer -- batches writes
bw := bufio.NewWriter(file)
bw.WriteString("Hello, ")
bw.WriteString("World!\n")
bw.Flush() // Don't forget to flush!
// Or use defer
bw := bufio.NewWriter(file)
defer bw.Flush()
Практический пример: обработка CSV с потоками
func processLargeCSV(inputPath, outputPath string) error {
// Open input
in, err := os.Open(inputPath)
if err != nil {
return fmt.Errorf("opening input: %w", err)
}
defer in.Close()
// Create output
out, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("creating output: %w", err)
}
defer out.Close()
// Buffered I/O for performance
reader := csv.NewReader(bufio.NewReader(in))
writer := csv.NewWriter(bufio.NewWriter(out))
defer writer.Flush()
// Process line by line -- constant memory usage
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("reading CSV: %w", err)
}
// Transform record
transformed := transformRecord(record)
if err := writer.Write(transformed); err != nil {
return fmt.Errorf("writing CSV: %w", err)
}
}
return nil
}