MidТеория9 min

Стандартные интерфейсы Go

io.Reader, io.Writer, fmt.Stringer, error, sort.Interface, http.Handler, context.Context и другие ключевые интерфейсы

Стандартная библиотека Go построена на небольших, хорошо определённых интерфейсах. Знание этих интерфейсов — ключ к написанию идиоматического Go-кода.

io.Reader и io.Writer

Это самые важные интерфейсы в Go. Огромная часть стандартной библиотеки строится на них.

io.Reader

// Defined in package io
type Reader interface {
    Read(p []byte) (n int, err error)
}

Read читает до len(p) байт в p. Возвращает количество прочитанных байт и ошибку. io.EOF сигнализирует об окончании данных.

package main

import (
    "fmt"
    "io"
    "strings"
)

func main() {
    // strings.Reader implements io.Reader
    r := strings.NewReader("Hello, Go!")

    buf := make([]byte, 4)
    for {
        n, err := r.Read(buf)
        if n > 0 {
            fmt.Printf("Read %d bytes: %q\n", n, buf[:n])
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println("Error:", err)
            break
        }
    }
    // Output:
    // Read 4 bytes: "Hell"
    // Read 4 bytes: "o, G"
    // Read 2 bytes: "o!"
}

Типы, реализующие io.Reader:

Тип Пакет Описание
*os.File os Чтение из файла
*strings.Reader strings Чтение из строки
*bytes.Reader bytes Чтение из []byte
*bytes.Buffer bytes Чтение из буфера
*bufio.Reader bufio Буферизованное чтение
http.Response.Body net/http HTTP response body
*gzip.Reader compress/gzip Декомпрессия gzip
*io.LimitedReader io Ограничение чтения N байт
io.TeeReader io Чтение с копированием в Writer

io.Writer

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

import (
    "bytes"
    "fmt"
    "io"
    "os"
)

func main() {
    // os.Stdout implements io.Writer
    fmt.Fprintln(os.Stdout, "Hello to stdout")

    // bytes.Buffer implements both Reader and Writer
    var buf bytes.Buffer
    buf.WriteString("Hello, ")
    buf.WriteString("World!")
    fmt.Println(buf.String()) // "Hello, World!"

    // Write to multiple writers at once
    var logBuf bytes.Buffer
    multi := io.MultiWriter(os.Stdout, &logBuf)
    fmt.Fprintln(multi, "This goes to both stdout and buffer")
    fmt.Println("Buffer contains:", logBuf.String())
}

Типы, реализующие io.Writer:

Тип Пакет Описание
*os.File os Запись в файл
*bytes.Buffer bytes Запись в буфер в памяти
*bufio.Writer bufio Буферизованная запись
http.ResponseWriter net/http HTTP response
*gzip.Writer compress/gzip Компрессия gzip
io.Discard io /dev/null (выбрасывает данные)
*strings.Builder strings Эффективная сборка строк

Композиция Reader/Writer

package main

import (
    "bufio"
    "compress/gzip"
    "io"
    "os"
)

// Copy from Reader to Writer — fundamental operation
func copyFile(dst, src string) error {
    in, err := os.Open(src)
    if err != nil {
        return err
    }
    defer in.Close()

    out, err := os.Create(dst)
    if err != nil {
        return err
    }
    defer out.Close()

    _, err = io.Copy(out, in)
    return err
}

// Chain readers for processing pipeline
func readCompressedFile(path string) ([]byte, error) {
    // File -> Gzip Reader -> Buffered Reader -> Read
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    gz, err := gzip.NewReader(f)
    if err != nil {
        return nil, err
    }
    defer gz.Close()

    br := bufio.NewReader(gz)
    return io.ReadAll(br)
}

// Write with compression
func writeCompressed(path string, data []byte) error {
    f, err := os.Create(path)
    if err != nil {
        return err
    }
    defer f.Close()

    gz := gzip.NewWriter(f)
    defer gz.Close()

    bw := bufio.NewWriter(gz)
    defer bw.Flush()

    _, err = bw.Write(data)
    return err
}

io.Closer, io.ReadWriter и другие комбинации

// Single-method interfaces
type Closer interface {
    Close() error
}

type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

// Composed interfaces
type ReadWriter interface {
    Reader
    Writer
}

type ReadCloser interface {
    Reader
    Closer
}

type WriteCloser interface {
    Writer
    Closer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

type ReadWriteSeeker interface {
    Reader
    Writer
    Seeker
}

Полезные утилиты пакета io

package main

import (
    "bytes"
    "fmt"
    "io"
    "strings"
)

func main() {
    r := strings.NewReader("Hello, World!")

    // io.ReadAll — read everything
    data, _ := io.ReadAll(r)
    fmt.Println(string(data)) // "Hello, World!"

    // io.Copy — copy from Reader to Writer
    r.Reset("Copy me!")
    var buf bytes.Buffer
    io.Copy(&buf, r)
    fmt.Println(buf.String()) // "Copy me!"

    // io.LimitReader — limit bytes read
    r.Reset("Long string that we only want part of")
    limited := io.LimitReader(r, 11)
    data, _ = io.ReadAll(limited)
    fmt.Println(string(data)) // "Long string"

    // io.TeeReader — read and copy simultaneously
    r.Reset("Tee example")
    var logBuf bytes.Buffer
    tee := io.TeeReader(r, &logBuf)
    data, _ = io.ReadAll(tee)
    fmt.Println(string(data))      // "Tee example"
    fmt.Println(logBuf.String())   // "Tee example" (copy)

    // io.Pipe — synchronous in-memory pipe
    pr, pw := io.Pipe()
    go func() {
        defer pw.Close()
        pw.Write([]byte("Piped data"))
    }()
    data, _ = io.ReadAll(pr)
    fmt.Println(string(data)) // "Piped data"

    // io.MultiReader — concatenate readers
    r1 := strings.NewReader("Hello, ")
    r2 := strings.NewReader("World!")
    multi := io.MultiReader(r1, r2)
    data, _ = io.ReadAll(multi)
    fmt.Println(string(data)) // "Hello, World!"
}

fmt.Stringer

Stringer определяет строковое представление типа:

type Stringer interface {
    String() string
}
package main

import (
    "fmt"
    "strings"
)

type IPAddr [4]byte

func (ip IPAddr) String() string {
    return fmt.Sprintf("%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3])
}

type Color struct {
    R, G, B uint8
}

func (c Color) String() string {
    return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B)
}

type User struct {
    Name  string
    Email string
    Roles []string
}

func (u User) String() string {
    return fmt.Sprintf("%s <%s> [%s]", u.Name, u.Email, strings.Join(u.Roles, ", "))
}

func main() {
    ip := IPAddr{192, 168, 1, 1}
    fmt.Println(ip) // 192.168.1.1 (String() called automatically)

    red := Color{255, 0, 0}
    fmt.Println(red) // #FF0000

    user := User{
        Name:  "Alice",
        Email: "[email protected]",
        Roles: []string{"admin", "editor"},
    }
    fmt.Println(user) // Alice <[email protected]> [admin, editor]
}

Важно: fmt.Println, fmt.Printf("%v"), fmt.Sprint и другие функции пакета fmt автоматически вызывают String(), если тип реализует fmt.Stringer.

Интерфейс error

Самый используемый интерфейс в Go:

type error interface {
    Error() string
}
package main

import (
    "fmt"
    "time"
)

// Custom error implementing error interface
type ValidationError struct {
    Field     string
    Value     interface{}
    Message   string
    Timestamp time.Time
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for field %q: %s (value: %v)",
        e.Field, e.Message, e.Value)
}

// Also implement Stringer for nicer display
func (e *ValidationError) String() string {
    return e.Error()
}

// Implement Unwrap for error chaining
type DatabaseError struct {
    Query string
    Err   error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("database error in query %q: %v", e.Query, e.Err)
}

func (e *DatabaseError) Unwrap() error {
    return e.Err
}

sort.Interface

type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}
package main

import (
    "fmt"
    "sort"
)

type Employee struct {
    Name   string
    Age    int
    Salary float64
}

// Implement sort.Interface for []Employee by salary
type BySalary []Employee

func (a BySalary) Len() int           { return len(a) }
func (a BySalary) Less(i, j int) bool { return a[i].Salary < a[j].Salary }
func (a BySalary) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

// By age
type ByAge []Employee

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

func main() {
    employees := []Employee{
        {"Alice", 30, 75000},
        {"Bob", 25, 65000},
        {"Charlie", 35, 90000},
        {"Diana", 28, 80000},
    }

    // Sort by salary using sort.Interface
    sort.Sort(BySalary(employees))
    fmt.Println("By salary:", employees)

    // Modern approach: sort.Slice (no interface needed)
    sort.Slice(employees, func(i, j int) bool {
        return employees[i].Age < employees[j].Age
    })
    fmt.Println("By age:", employees)

    // Even better: slices.SortFunc (Go 1.21+, uses generics)
    // slices.SortFunc(employees, func(a, b Employee) int {
    //     return cmp.Compare(a.Name, b.Name)
    // })
}

Совет: Начиная с Go 1.21, slices.SortFunc — более удобная альтернатива sort.Interface и sort.Slice.

encoding.TextMarshaler / json.Marshaler

Эти интерфейсы позволяют кастомизировать сериализацию:

package main

import (
    "encoding/json"
    "fmt"
    "time"
)

type Duration struct {
    time.Duration
}

// Custom JSON marshaling
func (d Duration) MarshalJSON() ([]byte, error) {
    return json.Marshal(d.String()) // "1h30m0s" instead of nanoseconds
}

func (d *Duration) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    dur, err := time.ParseDuration(s)
    if err != nil {
        return err
    }
    d.Duration = dur
    return nil
}

// Custom text marshaling (used by encoding/csv, flag, etc.)
type Color struct {
    R, G, B uint8
}

func (c Color) MarshalText() ([]byte, error) {
    return []byte(fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)), nil
}

func (c *Color) UnmarshalText(text []byte) error {
    _, err := fmt.Sscanf(string(text), "#%02x%02x%02x", &c.R, &c.G, &c.B)
    return err
}

type Config struct {
    Timeout Duration `json:"timeout"`
    Color   Color    `json:"color"`
}

func main() {
    cfg := Config{
        Timeout: Duration{90 * time.Minute},
        Color:   Color{255, 128, 0},
    }

    data, _ := json.MarshalIndent(cfg, "", "  ")
    fmt.Println(string(data))
    // {
    //   "timeout": "1h30m0s",
    //   "color": "#ff8000"
    // }

    // Unmarshal
    var cfg2 Config
    json.Unmarshal(data, &cfg2)
    fmt.Printf("Timeout: %v, Color: %+v\n", cfg2.Timeout, cfg2.Color)
}

http.Handler и http.HandlerFunc

Фундамент HTTP-серверов в Go:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

// HandlerFunc is a function adapter for Handler
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "time"
)

// Struct implementing http.Handler
type APIHandler struct {
    db     *Database
    logger *log.Logger
}

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

// Function as handler (using HandlerFunc adapter)
func healthCheck(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    fmt.Fprintln(w, "OK")
}

// Middleware pattern — decorator for http.Handler
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", healthCheck)

    // Chain middleware
    handler := loggingMiddleware(authMiddleware(mux))

    log.Println("Server starting on :8080")
    http.ListenAndServe(":8080", handler)
}

type Database struct{}

context.Context

Интерфейс для передачи отмены, дедлайнов и значений через цепочку вызовов:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}
package main

import (
    "context"
    "fmt"
    "time"
)

// ALWAYS pass context as the FIRST parameter
func fetchData(ctx context.Context, url string) ([]byte, error) {
    // Check if context is already cancelled
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
    }

    // Simulate work
    fmt.Printf("Fetching %s...\n", url)
    select {
    case <-time.After(2 * time.Second):
        return []byte("data"), nil
    case <-ctx.Done():
        return nil, fmt.Errorf("fetch %s: %w", url, ctx.Err())
    }
}

func main() {
    // Context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel() // Always call cancel to release resources

    data, err := fetchData(ctx, "https://api.example.com")
    if err != nil {
        fmt.Println("Error:", err) // "fetch ...: context deadline exceeded"
        return
    }
    fmt.Println("Data:", string(data))

    // Context with cancel
    ctx2, cancel2 := context.WithCancel(context.Background())
    go func() {
        time.Sleep(500 * time.Millisecond)
        cancel2() // Cancel after 500ms
    }()
    _, err = fetchData(ctx2, "https://api.example.com")
    fmt.Println("Error:", err)

    // Context with value (use sparingly!)
    type contextKey string
    const userIDKey contextKey = "userID"
    ctx3 := context.WithValue(context.Background(), userIDKey, 42)
    if id, ok := ctx3.Value(userIDKey).(int); ok {
        fmt.Println("User ID:", id) // 42
    }
}

Правила context.Context

Правило Описание
Первый параметр ctx context.Context всегда первый
Не хранить в структурах Передавать как параметр, не как поле struct
Не передавать nil Используйте context.TODO() если не уверены
WithValue осторожно Только для request-scoped данных (request ID, user)
Всегда вызывать cancel Предотвращает утечку горутин
Проверять ctx.Done() В длительных операциях и циклах

Сводная таблица стандартных интерфейсов

Интерфейс Методы Пакет Применение
io.Reader Read([]byte) (int, error) io Источник данных
io.Writer Write([]byte) (int, error) io Приёмник данных
io.Closer Close() error io Освобождение ресурсов
io.Seeker Seek(int64, int) (int64, error) io Навигация по данным
fmt.Stringer String() string fmt Текстовое представление
fmt.GoStringer GoString() string fmt Представление для %#v
error Error() string builtin Ошибки
sort.Interface Len, Less, Swap sort Сортировка
http.Handler ServeHTTP(RW, *Req) net/http HTTP обработчик
context.Context Deadline, Done, Err, Value context Отмена, дедлайны
json.Marshaler MarshalJSON() ([]byte, error) encoding/json Кастом JSON
json.Unmarshaler UnmarshalJSON([]byte) error encoding/json Кастом JSON parse
encoding.TextMarshaler MarshalText() ([]byte, error) encoding Текстовая сериализация
encoding.BinaryMarshaler MarshalBinary() ([]byte, error) encoding Бинарная сериализация
io.WriterTo WriteTo(Writer) (int64, error) io Оптимизация Copy
io.ReaderFrom ReadFrom(Reader) (int64, error) io Оптимизация Copy

Проверь себя

Чем `http.HandlerFunc` отличается от `http.Handler`?

Что происходит, когда тип реализует интерфейс `fmt.Stringer`?

Какой сигнал означает `io.EOF` при чтении?

Где правильно размещать `context.Context` в сигнатуре функции?

Какие два интерфейса являются фундаментом I/O в стандартной библиотеке Go?

Code Challenges

Реализация Stringer

GO

Реализуйте интерфейс fmt.Stringer для структуры Person. Формат: "Name (Age years old)".

Test Cases

1. Input: Person{Name: "Alice", Age: 30}→ Expected: Alice (30 years old)
2. Input: Person{Name: "Bob", Age: 0}→ Expected: Bob (0 years old)