Интерфейсы — одна из самых мощных и элегантных концепций Go. Они обеспечивают полиморфизм без наследования, через имплицитную (неявную) реализацию.
Определение интерфейса
Интерфейс определяет набор методов, которые тип должен реализовать:
package main
import (
"fmt"
"math"
)
// Interface definition — a contract
type Shape interface {
Area() float64
Perimeter() float64
}
// Circle implements Shape implicitly — no "implements" keyword!
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.Radius
}
// Rectangle also implements Shape
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Function accepting the interface — works with ANY Shape
func printShapeInfo(s Shape) {
fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}
func main() {
c := Circle{Radius: 5}
r := Rectangle{Width: 10, Height: 3}
// Both satisfy the Shape interface
printShapeInfo(c) // Area: 78.54, Perimeter: 31.42
printShapeInfo(r) // Area: 30.00, Perimeter: 26.00
// Slice of interfaces
shapes := []Shape{c, r, Circle{Radius: 1}}
totalArea := 0.0
for _, s := range shapes {
totalArea += s.Area()
}
fmt.Printf("Total area: %.2f\n", totalArea)
}
Имплицитная реализация (Duck Typing)
В Go нет ключевого слова implements. Тип реализует интерфейс автоматически, если имеет все методы интерфейса:
// No "implements" needed — just have the right methods
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow!"
}
// Interface defined AFTER the types — still works!
type Speaker interface {
Speak() string
}
// Both Dog and Cat implement Speaker without knowing about it
func makeNoise(s Speaker) {
fmt.Println(s.Speak())
}
Go Proverb: "The bigger the interface, the weaker the abstraction." Чем больше интерфейс, тем слабее абстракция.
Проверка реализации в compile-time
// Compile-time check that Circle implements Shape
var _ Shape = Circle{} // Value satisfies
var _ Shape = (*Circle)(nil) // Pointer satisfies
// This is a common Go idiom in library code
// If Circle doesn't implement Shape, compilation fails
Правила удовлетворения интерфейса
package main
import "fmt"
type Printer interface {
Print()
}
type ValueType struct {
Name string
}
// Value receiver — both T and *T satisfy the interface
func (v ValueType) Print() {
fmt.Println(v.Name)
}
type PointerType struct {
Name string
}
// Pointer receiver — only *T satisfies the interface
func (p *PointerType) Print() {
fmt.Println(p.Name)
}
func main() {
var p Printer
// Value receiver: both value and pointer work
p = ValueType{Name: "value"} // OK
p.Print()
p = &ValueType{Name: "ptr"} // OK
p.Print()
// Pointer receiver: ONLY pointer works
// p = PointerType{Name: "value"} // COMPILE ERROR!
p = &PointerType{Name: "ptr"} // OK
p.Print()
}
| Метод определён с | T удовлетворяет |
*T удовлетворяет |
|---|---|---|
Value receiver (t T) |
Да | Да |
Pointer receiver (t *T) |
Нет | Да |
Почему? Если метод имеет pointer receiver, значению типа
Tне всегда можно взять адрес (например, элемент карты). Поэтому Go требует*T.
Empty Interface: interface{} / any
Пустой интерфейс не содержит методов — его удовлетворяет любой тип:
package main
import "fmt"
func main() {
// interface{} — old syntax
var anything interface{}
anything = 42
anything = "hello"
anything = []int{1, 2, 3}
anything = struct{ Name string }{"Go"}
// any — alias for interface{} (Go 1.18+)
var value any
value = 42
value = "hello"
value = true
// Common use: variadic functions accepting any type
printAll(1, "hello", true, 3.14)
// To use the value, you need type assertion
if s, ok := value.(string); ok {
fmt.Println("String:", s)
}
fmt.Println(anything, value)
}
func printAll(args ...any) {
for _, arg := range args {
fmt.Printf("%v (%T)\n", arg, arg)
}
}
Когда (НЕ) использовать interface{} / any
| Использование | Пример | Рекомендация |
|---|---|---|
| JSON parsing | map[string]any |
OK |
| Generics заменитель | func Process(v any) |
Используйте дженерики |
| Printf-like functions | func Log(msg string, args ...any) |
OK |
| Коллекция разных типов | []any |
Осторожно, рассмотрите интерфейс |
| API parameter | func Handle(data any) |
Определите конкретный интерфейс |
Совет: С появлением дженериков (Go 1.18+) необходимость в
interface{}/anyзначительно снизилась. Предпочитайте типизированные решения.
Композиция интерфейсов (Embedding)
Go поощряет маленькие интерфейсы, которые можно комбинировать:
package main
import (
"fmt"
"io"
)
// Small, focused interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Composed interfaces through embedding
type ReadWriter interface {
Reader
Writer
}
type ReadCloser interface {
Reader
Closer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// Real-world example: custom interface composition
type Logger interface {
Log(level string, msg string)
}
type Metrics interface {
RecordLatency(operation string, duration float64)
IncrementCounter(name string)
}
type Observable interface {
Logger
Metrics
}
// A service that requires observability
type UserService struct {
obs Observable
}
func NewUserService(obs Observable) *UserService {
return &UserService{obs: obs}
}
func (s *UserService) GetUser(id int) {
s.obs.Log("INFO", fmt.Sprintf("Getting user %d", id))
s.obs.IncrementCounter("user.get")
// ... fetch user
}
Стандартная библиотека — отличный пример композиции:
io.Reader ──┐
├── io.ReadWriter ──┐
io.Writer ──┘ │
├── io.ReadWriteCloser
io.Closer ──────────────────────────┘
Принцип маленьких интерфейсов
"Accept interfaces, return structs." — Go Proverb
// GOOD: small, focused interfaces
type Saver interface {
Save(data []byte) error
}
type Loader interface {
Load(key string) ([]byte, error)
}
// Functions accept only what they need
func backup(s Saver, data []byte) error {
return s.Save(data)
}
func restore(l Loader, key string) ([]byte, error) {
return l.Load(key)
}
// BAD: large kitchen-sink interface
type Storage interface {
Save(data []byte) error
Load(key string) ([]byte, error)
Delete(key string) error
List() ([]string, error)
Exists(key string) bool
GetMetadata(key string) (map[string]string, error)
SetMetadata(key string, meta map[string]string) error
}
Интерфейсы из 1-2 методов в stdlib
| Интерфейс | Методы | Пакет |
|---|---|---|
io.Reader |
Read([]byte) (int, error) |
io |
io.Writer |
Write([]byte) (int, error) |
io |
io.Closer |
Close() error |
io |
fmt.Stringer |
String() string |
fmt |
error |
Error() string |
builtin |
sort.Interface |
Len(), Less(), Swap() |
sort |
http.Handler |
ServeHTTP(ResponseWriter, *Request) |
net/http |
json.Marshaler |
MarshalJSON() ([]byte, error) |
encoding/json |
context.Context |
4 метода | context |
nil интерфейсы vs интерфейсы, содержащие nil
Это одна из самых коварных ловушек в Go:
package main
import "fmt"
type MyError struct {
Message string
}
func (e *MyError) Error() string {
return e.Message
}
func getError(fail bool) error {
var err *MyError // Typed nil pointer
if fail {
err = &MyError{Message: "something failed"}
}
return err // TRAP: returns non-nil interface holding nil pointer!
}
func main() {
err := getError(false)
// This is NEVER nil, even when err is supposed to be nil!
if err != nil {
fmt.Println("Error:", err) // PANIC: calling Error() on nil *MyError
}
}
Объяснение
Интерфейс в Go состоит из двух указателей: (type, value).
nil interface: (nil, nil) → == nil ✓
interface holding nil: (*MyError, nil) → != nil ✗ (type is not nil!)
interface holding value: (*MyError, 0xc0000a2000) → != nil ✗
Правильный подход
func getError(fail bool) error {
if fail {
return &MyError{Message: "something failed"}
}
return nil // Return nil interface directly, NOT typed nil
}
// Or use explicit nil check before returning
func getErrorSafe(fail bool) error {
var err *MyError
if fail {
err = &MyError{Message: "something failed"}
}
if err == nil {
return nil // Explicitly return nil interface
}
return err
}
Правило: Никогда не возвращайте типизированный nil через интерфейс. Всегда возвращайте
return nilнапрямую для «нет ошибки».
Accept Interfaces, Return Structs
package main
import (
"context"
"fmt"
)
// GOOD: Accept interface — flexible, testable
type UserRepository interface {
FindByID(ctx context.Context, id int) (*User, error)
}
type UserService struct {
repo UserRepository // Accept interface
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo} // Return struct
}
// Concrete implementation
type PostgresUserRepo struct {
connString string
}
func NewPostgresUserRepo(connString string) *PostgresUserRepo {
return &PostgresUserRepo{connString: connString} // Return struct
}
func (r *PostgresUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
// ... database query
return &User{ID: id, Name: "Alice"}, nil
}
// For testing — mock implementation
type MockUserRepo struct {
Users map[int]*User
}
func (m *MockUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
user, ok := m.Users[id]
if !ok {
return nil, fmt.Errorf("user %d not found", id)
}
return user, nil
}
type User struct {
ID int
Name string
}
Этот паттерн обеспечивает:
- Тестируемость — легко подставить mock
- Гибкость — легко заменить реализацию
- Явные зависимости — видно, что сервису нужно
- Маленькие интерфейсы — каждый потребитель определяет свой интерфейс