Структуры - создаем свои типы

Структуры в Go - это способ группировки данных. Если вы знакомы с классами из ООП, структуры - это данные без наследования, но с методами.

Основы структур

Объявление

// Простая структура
type Person struct {
    Name string
    Age  int
}

// С вложенными типами
type Employee struct {
    Person    Person
    ID        int
    Salary    float64
    IsRemote  bool
}

Создание экземпляров

// Нулевое значение
var p1 Person // Name: "", Age: 0

// Литерал структуры
p2 := Person{
    Name: "Alice",
    Age:  30,
}

// Позиционная инициализация (не рекомендуется)
p3 := Person{"Bob", 25}

// Частичная инициализация
p4 := Person{Name: "Carol"} // Age будет 0
Неинициализированные поля получают нулевые значения своих типов: 0 для чисел, "" для строк, nil для указателей.

Работа со структурами

Доступ к полям

person := Person{Name: "Alice", Age: 30}

// Чтение
fmt.Println(person.Name) // Alice

// Запись
person.Age = 31

// Через указатель (автоматическое разыменование)
p := &person
p.Age = 32 // эквивалентно (*p).Age = 32

Анонимные структуры

// Для одноразового использования
response := struct {
    Status  int
    Message string
}{
    Status:  200,
    Message: "OK",
}

// В слайсах
points := []struct{ X, Y int }{
    {10, 20},
    {30, 40},
}

Методы

Value receiver

type Rectangle struct {
    Width, Height float64
}

// Метод с value receiver
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Использование
rect := Rectangle{10, 5}
fmt.Println(rect.Area()) // 50

Pointer receiver

// Метод с pointer receiver (может изменять структуру)
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

// Использование
rect := Rectangle{10, 5}
rect.Scale(2) // автоматически &rect
fmt.Println(rect) // {20 10}

<ComparisonTable data={{ headers: ["Тип receiver", "Когда использовать", "Особенности"], rows: [ ["Value", "Метод не меняет структуру, структура маленькая", "Копия при вызове"], ["Pointer", "Метод меняет структуру, структура большая, нужна единая сущность", "Работа с оригиналом"] ] }} />

Встраивание (Embedding)

Go использует композицию вместо наследования:

type Contact struct {
    Email string
    Phone string
}

type Person struct {
    Name string
    Age  int
    Contact // встраивание
}

// Использование
p := Person{
    Name: "Alice",
    Age:  30,
    Contact: Contact{
        Email: "alice@example.com",
        Phone: "+1234567890",
    },
}

// Прямой доступ к полям Contact
fmt.Println(p.Email) // alice@example.com
fmt.Println(p.Contact.Email) // тоже работает

Встраивание с методами

type Logger struct {
    prefix string
}

func (l *Logger) Log(message string) {
    fmt.Printf("[%s] %s\n", l.prefix, message)
}

type Service struct {
    *Logger // встраивание указателя
    name string
}

// Использование
svc := Service{
    Logger: &Logger{prefix: "INFO"},
    name:   "UserService",
}

svc.Log("Service started") // метод Logger доступен напрямую

Теги структур

Теги - это метаданные для полей:

type User struct {
    ID        int    `json:"id" db:"user_id"`
    Name      string `json:"name" db:"user_name"`
    Email     string `json:"email" db:"email" validate:"required,email"`
    Password  string `json:"-" db:"password"` // "-" исключает из JSON
    CreatedAt time.Time `json:"created_at,omitempty"`
}

Использование с JSON

user := User{
    ID:    1,
    Name:  "Alice",
    Email: "alice@example.com",
}

// Маршалинг в JSON
data, _ := json.Marshal(user)
fmt.Println(string(data))
// {"id":1,"name":"Alice","email":"alice@example.com"}

// Анмаршалинг из JSON
jsonStr := `{"id":2,"name":"Bob","email":"bob@example.com"}`
var newUser User
json.Unmarshal([]byte(jsonStr), &newUser)

Сравнение структур

type Point struct {
    X, Y int
}

p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{2, 3}

fmt.Println(p1 == p2) // true
fmt.Println(p1 == p3) // false

// Структуры можно использовать как ключи в map
m := make(map[Point]string)
m[p1] = "A"
Структуры сравнимы, если все их поля сравнимы. Структуры со [слайсами](./06-slices.md), [maps](./07-maps.md) или функциями не сравнимы.

Паттерны и best practices

Конструкторы

type Server struct {
    host string
    port int
    tls  bool
}

// Конструктор
func NewServer(host string, port int) *Server {
    return &Server{
        host: host,
        port: port,
        tls:  port == 443,
    }
}

// С опциональными параметрами
type ServerOption func(*Server)

func WithTLS(enabled bool) ServerOption {
    return func(s *Server) {
        s.tls = enabled
    }
}

func NewServerWithOptions(host string, port int, opts ...ServerOption) *Server {
    s := &Server{host: host, port: port}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

Builder pattern

type QueryBuilder struct {
    table   string
    columns []string
    where   []string
    limit   int
}

func (q *QueryBuilder) Select(columns ...string) *QueryBuilder {
    q.columns = columns
    return q
}

func (q *QueryBuilder) From(table string) *QueryBuilder {
    q.table = table
    return q
}

func (q *QueryBuilder) Where(condition string) *QueryBuilder {
    q.where = append(q.where, condition)
    return q
}

func (q *QueryBuilder) Limit(n int) *QueryBuilder {
    q.limit = n
    return q
}

func (q *QueryBuilder) Build() string {
    // построение SQL запроса
    return fmt.Sprintf("SELECT %s FROM %s WHERE %s LIMIT %d",
        strings.Join(q.columns, ", "),
        q.table,
        strings.Join(q.where, " AND "),
        q.limit)
}

// Использование
query := (&QueryBuilder{}).
    Select("id", "name").
    From("users").
    Where("age > 18").
    Limit(10).
    Build()

Validation

type Email string

func (e Email) Validate() error {
    if !strings.Contains(string(e), "@") {
        return errors.New("invalid email")
    }
    return nil
}

type User struct {
    Name  string
    Email Email
}

func (u *User) Validate() error {
    if u.Name == "" {
        return errors.New("name is required")
    }
    return u.Email.Validate()
}

Производительность

Размер и выравнивание

type Inefficient struct {
    a bool   // 1 байт + 7 padding
    b int64  // 8 байт
    c bool   // 1 байт + 7 padding
} // Итого: 24 байта

type Efficient struct {
    b int64  // 8 байт
    a bool   // 1 байт
    c bool   // 1 байт + 6 padding
} // Итого: 16 байт

fmt.Println(unsafe.Sizeof(Inefficient{})) // 24
fmt.Println(unsafe.Sizeof(Efficient{}))   // 16

Копирование vs указатели

type Large struct {
    data [1000]int
}

// Плохо: копирует 8KB при каждом вызове
func processValue(l Large) {
    // ...
}

// Хорошо: передает только указатель (8 байт)
func processPointer(l *Large) {
    // ...
}

Практический пример: REST API модель

type APIResponse struct {
    Success bool        `json:"success"`
    Data    interface{} `json:"data,omitempty"`
    Error   *APIError   `json:"error,omitempty"`
    Meta    *Meta       `json:"meta,omitempty"`
}

type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    Details map[string]interface{} `json:"details,omitempty"`
}

type Meta struct {
    Page       int `json:"page"`
    PerPage    int `json:"per_page"`
    Total      int `json:"total"`
    TotalPages int `json:"total_pages"`
}

// Конструкторы для удобства
func SuccessResponse(data interface{}) APIResponse {
    return APIResponse{
        Success: true,
        Data:    data,
    }
}

func ErrorResponse(code, message string) APIResponse {
    return APIResponse{
        Success: false,
        Error: &APIError{
            Code:    code,
            Message: message,
        },
    }
}

Итоги

  • Структуры группируют связанные данные
  • Методы добавляют поведение
  • Композиция через встраивание вместо наследования
  • Теги для метаданных
  • Value vs Pointer receivers

В следующем уроке изучим интерфейсы - мощный инструмент абстракции!

Типичная ошибка

Хранить всё строками «чтобы проще». Потом всё равно придётся парсить - лучше сразу правильные типы.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.