Стандартная библиотека 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 |