Лекция 5. Методы и интерфейсы¶
Методы¶
Метод в Go — это функция с особым параметром-«получателем» (receiver) перед именем:
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
r := Rectangle{Width: 3, Height: 4}
fmt.Println(r.Area()) // 12
Запись (r Rectangle) — это receiver. Внутри метода r — это копия структуры (по значению).
Value receiver vs pointer receiver¶
// получатель по значению — метод работает с КОПИЕЙ
func (r Rectangle) Scale(k float64) {
r.Width *= k
r.Height *= k
}
// получатель по указателю — метод видит/меняет ОРИГИНАЛ
func (r *Rectangle) ScaleInPlace(k float64) {
r.Width *= k
r.Height *= k
}
r := Rectangle{3, 4}
r.Scale(2) // ничего не изменилось
fmt.Println(r) // {3 4}
r.ScaleInPlace(2) // Go автоматически возьмёт &r
fmt.Println(r) // {6 8}
Когда что использовать¶
Pointer receiver:
- Метод должен изменить состояние.
- Структура «тяжёлая» — копировать дорого.
- Структура содержит мьютекс, файл-дескриптор и т. п. — копировать нельзя.
Value receiver:
- Тип маленький и иммутабельный (примитивы, точки, цвета).
- Структура должна семантически быть значением (например,
time.Time).
Правило согласованности. Если у типа есть хоть один метод с pointer receiver — обычно делают все методы pointer receiver. Иначе путаница: одни методы изменяют состояние, другие нет.
Методы на любых типах¶
Можно объявить методы не только на структурах, а на любом типе, объявленном в этом же пакете:
type Status int
const (
StatusPending Status = iota
StatusActive
StatusBanned
)
func (s Status) String() string {
return [...]string{"pending", "active", "banned"}[s]
}
Этим часто пользуются: своим именем оборачивают int, string, []byte — и навешивают на них поведение.
Что нельзя — добавить метод к чужому типу. Например, написать func (s string) Reverse() string запрещено, потому что string объявлен в пакете builtin. Это намеренно: предотвращает «monkey patching».
Интерфейсы¶
Интерфейс — это набор сигнатур методов. Любой тип, который реализует все эти методы, автоматически удовлетворяет интерфейсу. Не нужно писать implements или extends.
type Shape interface {
Area() float64
Perimeter() float64
}
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 }
func describe(s Shape) {
fmt.Printf("площадь=%.2f, периметр=%.2f\n", s.Area(), s.Perimeter())
}
describe(Circle{Radius: 5})
Circle нигде не указывает, что «реализует» Shape. Это structural typing — компилятор просто проверяет, что у переданного значения есть нужные методы.
Этот подход напоминает Python typing.Protocol (PEP 544) и противоположен Java/C#, где надо явно писать implements.
Несколько реализаций¶
type Rectangle struct{ W, H float64 }
func (r Rectangle) Area() float64 { return r.W * r.H }
func (r Rectangle) Perimeter() float64 { return 2 * (r.W + r.H) }
shapes := []Shape{
Circle{Radius: 5},
Rectangle{W: 3, H: 4},
}
for _, s := range shapes {
describe(s)
}
Пустой интерфейс / any¶
Интерфейс без методов — interface{}. С Go 1.18 для него есть синоним any. Удовлетворить ему может любой тип.
Это аналог Python Any или object. Использование any подавляет типобезопасность — берегите его на крайний случай (универсальные контейнеры, параметры логгера, передача через границы пакетов).
С появлением дженериков многие случаи any теперь решаются типобезопасно (см. ниже).
Type assertion и type switch¶
Из значения интерфейса можно «достать» конкретный тип:
var i any = "hello"
s := i.(string) // panic, если внутри не string
s, ok := i.(string) // safe-форма с comma-ok
if v, ok := i.(int); ok {
fmt.Println("int:", v)
}
Когда нужно различить несколько типов — type switch:
func describe(i any) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("int=%d", v)
case string:
return fmt.Sprintf("string=%q", v)
case []int:
return fmt.Sprintf("[]int len=%d", len(v))
case nil:
return "nil"
default:
return fmt.Sprintf("unknown %T", v)
}
}
Конструкция v := i.(type) работает только внутри switch.
Композиция интерфейсов¶
Один интерфейс может встраивать другие:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Reader
Writer
}
Это базовая идиома стандартной библиотеки. Всё, что умеет читать и писать, автоматически удовлетворяет io.ReadWriter.
Каноничные интерфейсы стандартной библиотеки¶
Запомните их — они везде:
error¶
Уже обсуждали в лекции 3.
fmt.Stringer¶
Если тип реализует String(), его форматирование %v, %s и fmt.Println использует этот метод.
type Distance float64
func (d Distance) String() string {
return fmt.Sprintf("%.2f км", float64(d))
}
fmt.Println(Distance(3.14159)) // 3.14 км
io.Reader и io.Writer¶
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Файлы, сетевые соединения, буферы (bytes.Buffer), архивы — всё это Reader/Writer. Благодаря этому одни и те же функции (io.Copy, bufio.NewScanner, json.NewDecoder) работают и с файлами, и с сетью, и с памятью.
sort.Interface (устарел, но полезен для понимания)¶
Раньше — единственный способ сортировки кастомных коллекций. Сейчас — slices.SortFunc (Go 1.21+), но интерфейс всё ещё используется в legacy-коде.
nil интерфейс — известная ловушка¶
Интерфейс внутри — это пара (тип, значение). Интерфейс равен nil, только если и тип, и значение nil.
var p *MyError = nil // p — nil-указатель
var e error = p // e — НЕ nil (тип внутри = *MyError, значение = nil)
if e == nil {
fmt.Println("чисто")
} else {
fmt.Println("грязно") // напечатает это
}
Это самый частый WTF-момент для новичков. Решение: возвращайте nil напрямую, а не «nil-указатель типизированной ошибки»:
Дженерики (с Go 1.18)¶
До 1.18 дженериков не было — приходилось писать одно и то же для каждого типа или использовать any и type assertions. В 1.18 их наконец добавили.
func Max[T cmp.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Max(3, 5) // int
Max(3.14, 2.71) // float64
Max("a", "b") // string
[T cmp.Ordered] — параметр типа с ограничением (constraint). cmp.Ordered — встроенное ограничение (Go 1.21+): любой упорядочиваемый тип (числа, строки).
Пользовательские ограничения¶
type Number interface {
int | int64 | float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
Здесь int | int64 | float64 — type union, новая форма интерфейса для дженериков.
Дженерики и интерфейсы¶
В большинстве случаев интерфейсы остаются удобнее (поведенческая абстракция), а дженерики уместны для контейнеров и алгоритмов на разных типах данных (одна реализация для int/float64/string). Хорошее правило: «если в коде есть interface{} + type assertion — может, нужен дженерик».
Параллель с Python¶
| Python | Go |
|---|---|
class C: def m(self): ... |
func (c *C) M() { ... } |
@abstractmethod / Protocol |
интерфейс (структурно — как Protocol) |
isinstance(x, T) |
type assertion x.(T) |
match x: case int(): ... |
switch v := x.(type) { case int: ... } |
| наследование классов | embedding структур + интерфейсов |
| дак-тайпинг | structural typing (статический эквивалент) |
T = TypeVar("T") + generic |
[T any] (Go 1.18+) |
Any |
any (синоним interface{}) |
__str__ |
String() string (интерфейс fmt.Stringer) |
Итог¶
Метод в Go — функция с receiver. Value receiver работает с копией, pointer receiver — с оригиналом; внутри типа методы согласуют. Интерфейс — набор методов; реализация автоматическая (structural typing). Канонические error, fmt.Stringer, io.Reader/io.Writer. Дженерики с 1.18 закрывают то, что раньше делали через any. В последней лекции темы — пакеты и тестирование.