Слайс модели в Go - работа с model-slice

05 января 2026
Автор

Олег Марков

Введение

Слайс модели (часто его называют model-slice) — это типичная конструкция, с которой вы будете сталкиваться практически в каждом Go-проекте, где есть работа с данными: БД, внешние API, кэш или бизнес-логика.

Под слайсом модели обычно понимают срез структур одного типа. Например, список пользователей, список заказов или список товаров. В коде это выглядит как []User, []Order, []Product и так далее. По сути, это обычный слайс Go, но вокруг него возникает целый набор типичных задач:

  • как объявлять и инициализировать слайсы моделей;
  • как безопасно их изменять и передавать между функциями;
  • как использовать append и управлять емкостью, чтобы избежать лишних аллокаций;
  • как маппить данные из БД в слайс моделей;
  • как фильтровать, сортировать и преобразовывать слайс моделей;
  • как избежать типичных ловушек с указателями и копированием.

Здесь вы увидите, как это выглядит на практике, и получите набор шаблонов, которые можно переносить в свои проекты.

Что такое слайс модели и зачем он нужен

Базовое определение

Смотрите, давайте начнем с простого примера модели:

// User описывает пользователя в системе
type User struct {
    ID    int64  // Идентификатор пользователя
    Name  string // Имя пользователя
    Email string // Адрес электронной почты
}

// UserSlice - слайс модели User
type UserSlice []User

По сути, UserSlice — это alias для []User. Вы можете вообще не вводить отдельный тип и использовать просто []User, но отдельный тип часто помогает:

  • сгруппировать методы, связанные со слайсом модели;
  • сделать код более самодокументируемым;
  • избежать путаницы, если есть несколько разных слайсов с одинаковым базовым типом.

Структура слайса в Go

Чтобы увереннее работать с model-slice, полезно понимать, как устроен слайс в Go внутри. Упрощенно слайс состоит из:

  • указателя на массив (где реально лежат данные);
  • длины (len);
  • емкости (cap).

Это важно, потому что:

  • при append может произойти перераспределение массива;
  • разные слайсы могут ссылаться на один и тот же базовый массив;
  • операции среза (a[low:high]) не копируют данные, а создают "окно" поверх того же массива.

Давайте разберемся на примере:

users := []User{
    {ID: 1, Name: "Alice"},
    {ID: 2, Name: "Bob"},
}

// Здесь мы создаем срез от исходного слайса
activeUsers := users[:1]

// Меняем имя в activeUsers
activeUsers[0].Name = "Alice Updated"

// Изменение видно и в исходном слайсе, так как данные общие
fmt.Println(users[0].Name) // Выведет: Alice Updated

// Здесь обе переменные users и activeUsers указывают на один и тот же массив, // поэтому изменение через один слайс видно в другом.

Это ключевой момент, который сильно влияет на работу со слайсом модели: вы часто фактически делитесь одними и теми же данными между разными частями кода.

Объявление и инициализация model-slice

Простое объявление

Чаще всего слайс модели объявляют так:

var users []User // Объявлен, но нулевой (nil) слайс

или сразу инициализируют:

users := []User{
    {ID: 1, Name: "Alice"},
    {ID: 2, Name: "Bob"},
}

// В первом случае у вас nil-слайс без данных. // Во втором - слайс длиной 2 с инициализированными элементами.

Nil-слайс и пустой слайс — важное различие:

var a []User          // nil-слайс
b := []User{}         // пустой, но не nil
fmt.Println(a == nil) // true
fmt.Println(b == nil) // false

// Это может быть важно при маршалинге в JSON, проверках на отсутствие данных, // и при работе с ORM/драйверами.

Инициализация через make

Если вы заранее знаете ожидаемый размер, удобно использовать make:

// Создаем слайс на 0 элементов, но с емкостью 100
users := make([]User, 0, 100)

// Здесь мы резервируем место под 100 пользователей
// Это уменьшит количество аллокаций при дальнейшем append

Такой подход полезен, если:

  • вы читаете данные батчами;
  • заранее знаете приблизительный размер выборки из БД;
  • строите слайс в цикле.

Собственный тип для model-slice

Вводя собственный тип, вы получаете возможность "прикрутить" к нему методы:

type UserSlice []User

// FilterActive возвращает только активных пользователей
func (s UserSlice) FilterActive() UserSlice {
    // Здесь мы создаем новый слайс для результата
    out := make(UserSlice, 0, len(s))
    for _, u := range s {
        if u.IsActive() {
            // Добавляем пользователя, если он активен
            out = append(out, u)
        }
    }
    return out
}

// Теперь вы можете писать: // users := UserSlice{...} // active := users.FilterActive()

Такой подход делает работу со слайсом модели более выразительной и структурированной.

Использование append с model-slice

Базовый сценарий

append — главный инструмент работы со слайсом модели. Давайте посмотрим, как он используется:

var users []User

// Добавляем одного пользователя
users = append(users, User{ID: 1, Name: "Alice"})

// Добавляем сразу несколько
users = append(users,
    User{ID: 2, Name: "Bob"},
    User{ID: 3, Name: "Charlie"},
)

// Важно - результат append всегда нужно присваивать обратно слайсу, // потому что при расширении может выделиться новый массив.

Обратите внимание: если вы забыли сделать присваивание, изменения могут потеряться.

Добавление одного слайса моделей в другой

Частый сценарий — объединить два слайда моделей:

var allUsers []User
admins := []User{{ID: 1, Name: "Admin"}}
guests := []User{{ID: 2, Name: "Guest"}}

// Здесь мы добавляем элементы слайса admins в конец allUsers
allUsers = append(allUsers, admins...)

// А потом добавляем гостей
allUsers = append(allUsers, guests...)

// Оператор ... раскрывает слайс поэлементно для append.

Управление емкостью и аллокациями

При работе с большими слайсами моделей важно избегать лишних аллокаций. Давайте посмотрим, что происходит при росте слайса:

users := make([]User, 0, 2) // capacity = 2

users = append(users, User{ID: 1})
users = append(users, User{ID: 2})
// Здесь все еще используется один массив

users = append(users, User{ID: 3})
// Здесь, скорее всего, произойдет перераспределение памяти

// После третьего append Go создаст новый массив большей емкости и скопирует туда данные.

Если вы заранее знаете максимальный размер, лучше заложить емкость с запасом:

expectedCount := 1000
users := make([]User, 0, expectedCount)

Это особенно полезно при:

  • загрузке данных из БД;
  • парсинге больших JSON/XML;
  • обработке больших файлов.

Модификация элементов в процессе append

Иногда вы хотите модифицировать элемент перед добавлением. Покажу вам, как это реализовано на практике:

rawIDs := []int64{1, 2, 3}
users := make([]User, 0, len(rawIDs))

for _, id := range rawIDs {
    u := User{
        ID:   id,
        Name: fmt.Sprintf("User %d", id),
    }
    // Добавляем подготовленную модель в слайс
    users = append(users, u)
}

// Здесь мы создаем User на каждой итерации, заполняем его и затем добавляем в слайс.

Главное — не хранить указатель на переменную цикла u между итерациями, если вы меняете ее в следующем шаге. Иначе все элементы могут указывать на одну и ту же область памяти. Для значимых типов (struct без указателя) это безопасно, но для указателей стоит быть внимательнее.

Model-slice и работа с базой данных

Пример с database/sql

Один из типичных случаев использования слайса модели — чтение данных из БД:

// User модель для БД
type User struct {
    ID    int64
    Name  string
    Email string
}

// LoadUsers загружает пользователей из БД и возвращает слайс моделей
func LoadUsers(ctx context.Context, db *sql.DB) ([]User, error) {
    // Выполняем запрос
    rows, err := db.QueryContext(ctx, `
        SELECT id, name, email
        FROM users
        WHERE deleted_at IS NULL
    `)
    if err != nil {
        return nil, err
    }
    defer rows.Close() // Не забываем закрывать rows

    users := make([]User, 0, 100) // Предположим, ожидаем до 100 пользователей

    for rows.Next() {
        var u User
        // Сканируем данные строки в структуру
        if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
            return nil, err
        }
        // Добавляем модель в слайс
        users = append(users, u)
    }

    if err := rows.Err(); err != nil {
        return nil, err
    }

    return users, nil
}

// Здесь слайс моделей используется как контейнер для результатов SQL-запроса.

В этом коде вы видите типичный шаблон:

  1. Создаем слайс моделей с предварительной емкостью.
  2. В цикле создаем модель и сканируем значения из rows.
  3. Через append заполняем слайс.

Пример с ORM (GORM)

Если вы используете ORM (например, GORM), работа с model-slice еще более прямолинейна:

type User struct {
    ID    uint
    Name  string
    Email string
}

// LoadActiveUsers загружает активных пользователей через GORM
func LoadActiveUsers(db *gorm.DB) ([]User, error) {
    var users []User
    // Передаем указатель на слайс моделей в ORM
    if err := db.
        Where("deleted_at IS NULL").
        Find(&users).Error; err != nil {
        return nil, err
    }
    return users, nil
}

// ORM сама заполняет слайс моделей, создавая экземпляры User для каждой строки.

В таких случаях важно помнить:

  • ORM ожидает указатель на слайс (&users), чтобы его заполнять;
  • многие ORM корректно работают как с nil-слайсом, так и с пустым.

Обработка, фильтрация и преобразование model-slice

Фильтрация слайса модели

Допустим, у модели User есть поле Active. Давайте посмотрим, как можно отфильтровать только активных пользователей:

// User с признаком активности
type User struct {
    ID     int64
    Name   string
    Active bool
}

// FilterActive возвращает новый слайс только с активными пользователями
func FilterActive(users []User) []User {
    out := make([]User, 0, len(users))
    for _, u := range users {
        if u.Active {
            // Добавляем в результат только активных
            out = append(out, u)
        }
    }
    return out
}

// Мы создаем новый слайс, не модифицируя исходный. Это безопаснее и предсказуемее.

Такой подход (создание нового слайса) упрощает понимание кода: вы точно знаете, что исходный набор данных не изменяется.

Проекция model-slice в другой тип

Частая задача — из слайса моделей получить слайс DTO или только часть данных:

// UserDTO - облегченная модель для ответа API
type UserDTO struct {
    ID   int64
    Name string
}

// MapToDTO преобразует слайс User в слайс UserDTO
func MapToDTO(users []User) []UserDTO {
    dtos := make([]UserDTO, 0, len(users))
    for _, u := range users {
        dto := UserDTO{
            ID:   u.ID,
            Name: u.Name,
        }
        dtos = append(dtos, dto)
    }
    return dtos
}

// Здесь мы явно копируем нужные поля из модели в DTO и формируем новый слайс.

Это типичная операция на границе слоев: из слоя данных в слой API.

Поиск элементов в слайсе моделей

Типичные утилиты — найти элемент по ID или по какому-то полю:

// FindByID ищет пользователя по ID и возвращает указатель на него
func FindByID(users []User, id int64) *User {
    for i := range users {
        if users[i].ID == id {
            // Возвращаем адрес элемента внутри слайса
            return &users[i]
        }
    }
    return nil
}

// Обратите внимание - мы берем адрес по индексу i, а не по переменной цикла. // Это важно, чтобы не получить адрес временной копии.

Теперь вы увидите, как это выглядит в использовании:

users := []User{{ID: 1}, {ID: 2}}

u := FindByID(users, 2)
if u != nil {
    // Меняем поле прямо в слайсе
    u.Name = "Updated Name"
}

// Изменение отражается в исходном слайсе, так как мы работаем с указателем на его элемент.

Сортировка слайса модели

Для сортировки удобно использовать пакет sort:

// SortUsersByName сортирует пользователей по имени
func SortUsersByName(users []User) {
    sort.Slice(users, func(i, j int) bool {
        return users[i].Name < users[j].Name
    })
}

// Функция сортирует слайс "на месте" - без создания копии.

Если важно не менять исходный порядок, можно сначала сделать копию:

func SortedUsersByName(users []User) []User {
    out := make([]User, len(users))
    copy(out, users) // Копируем данные в новый слайс
    sort.Slice(out, func(i, j int) bool {
        return out[i].Name < out[j].Name
    })
    return out
}

// Теперь исходный слайс остается нетронутым.

Передача model-slice между функциями и слоями

Передача по значению и "по ссылке"

Слайсы в Go передаются по значению, но при этом содержат указатель на массив. Давайте посмотрим на эффект:

// AddUser добавляет пользователя в слайс (не сработает как ожидается)
func AddUser(users []User, u User) {
    users = append(users, u)
    // Здесь мы изменили только локальную копию переменной users
}

// AddUserPtr принимает указатель на слайс и изменяет его "снаружи"
func AddUserPtr(users *[]User, u User) {
    *users = append(*users, u)
}

Теперь давайте разберемся на примере:

users := []User{}

AddUser(users, User{ID: 1})
// Здесь users все еще пустой, так как append был на копии

AddUserPtr(&users, User{ID: 2})
// Теперь в users будет один элемент

// Если вам нужно, чтобы функция могла изменить сам слайс (например, его длину или емкость), // передавайте указатель на слайс.

Чаще всего достаточно возвращать новый слайс из функции:

func AddUserReturn(users []User, u User) []User {
    users = append(users, u)
    return users
}

// Такой стиль более идиоматичен - вызывающий код явно видит, что нужно использовать результат.

Иммутабельный стиль

Для упрощения рассуждений о коде иногда используют "иммутабельный" подход: не изменять существующий слайс, а создавать новый. Например:

// WithoutUser возвращает новый слайс без пользователя с заданным ID
func WithoutUser(users []User, id int64) []User {
    out := make([]User, 0, len(users))
    for _, u := range users {
        if u.ID == id {
            // Пропускаем этого пользователя
            continue
        }
        out = append(out, u)
    }
    return out
}

// Исходный слайс не меняется. Это упрощает отладку и снижает риск неожиданных побочных эффектов.

Типичные ошибки и подводные камни при работе с model-slice

Ошибка с указателем на переменную цикла

Очень частая проблема — сохранение адреса временной переменной:

type User struct {
    ID   int64
    Name string
}

// Ошибочный код - все указатели будут указывать на одну и ту же переменную u
func BuildUserPointers(ids []int64) []*User {
    result := make([]*User, 0, len(ids))
    for _, id := range ids {
        u := User{ID: id}
        result = append(result, &u) // Здесь мы берем адрес переменной цикла
    }
    return result
}

// В этом примере на каждой итерации используется одна и та же переменная u, // поэтому все элементы result будут указывать на одно и то же место в памяти.

Правильный вариант — брать адрес элемента слайса или выделять новый объект с помощью new:

func BuildUserPointers(ids []int64) []*User {
    result := make([]*User, 0, len(ids))
    for _, id := range ids {
        u := &User{ID: id} // Создаем новый объект в куче
        result = append(result, u)
    }
    return result
}

// Теперь на каждой итерации создается новый User, и указатели разные.

Сохранение слайса после append в другом месте

Еще одна ловушка — сохранение ссылки на слайс, который затем "неожиданно" меняется при append:

users := make([]User, 0, 1)
users = append(users, User{ID: 1})

// Сохраняем ссылку на слайс
backup := users

// Делаем append, который может перераспределить массив
users = append(users, User{ID: 2})

// В зависимости от перераспределения backup может указывать на старый массив
// или на тот же самый, что и users

// Здесь важно понимать - backup и users могут разделять один массив или нет, // в зависимости от того, произошла ли аллокация.

Безопасный подход:

  • если вам нужно "заморозить" состояние — делайте копию через copy;
  • не полагайтесь на то, что два слайса будут навсегда разделять один массив.
backup := make([]User, len(users))
copy(backup, users)
// Теперь backup точно независим от users

Модификация данных в под-слайсе

Допустим, вы взяли часть слайса:

allUsers := []User{
    {ID: 1, Name: "A"},
    {ID: 2, Name: "B"},
    {ID: 3, Name: "C"},
}

// Берем только первых двух
firstTwo := allUsers[:2]

// Меняем имя первого в под-слайсе
firstTwo[0].Name = "Updated"

// Изменение отразится и в allUsers, потому что данные общие.

Если вам нужен независимый фрагмент, скопируйте его:

safeCopy := append([]User(nil), allUsers[:2]...)
// safeCopy теперь отдельный слайс с собственным массивом

Практические паттерны работы с model-slice

Пагинация для API

Слайс моделей часто отдают наружу через API с поддержкой пагинации. Давайте посмотрим простой паттерн:

type Page struct {
    Items      []User // Слайс моделей
    TotalCount int    // Общее количество записей
    Limit      int    // Размер страницы
    Offset     int    // Смещение
}

// PaginateUsers возвращает структуру Page
func PaginateUsers(users []User, limit, offset int) Page {
    // Вычисляем границы слайса
    if offset > len(users) {
        offset = len(users)
    }
    end := offset + limit
    if end > len(users) {
        end = len(users)
    }

    // Берем под-слайс с нужной частью
    pageItems := users[offset:end]

    return Page{
        Items:      pageItems,
        TotalCount: len(users),
        Limit:      limit,
        Offset:     offset,
    }
}

// Здесь мы используем модели в слайсе для формирования страницы данных.

Группировка слайса моделей по ключу

Еще одна распространенная операция — сгруппировать модели по какому-то полю:

// GroupUsersByEmailDomain группирует пользователей по домену email
func GroupUsersByEmailDomain(users []User) map[string][]User {
    result := make(map[string][]User)
    for _, u := range users {
        parts := strings.SplitN(u.Email, "@", 2)
        if len(parts) != 2 {
            // Пропускаем некорректные адреса
            continue
        }
        domain := parts[1]
        // Добавляем пользователя в группу по домену
        result[domain] = append(result[domain], u)
    }
    return result
}

// Здесь слайс модели внутри map выступает как "корзина" для каждой группы.

Обновление моделей по карте

Еще один практический пример — сопоставление данных из карты и слайса моделей:

// UpdateUserNames обновляет имена пользователей по карте ID -> Name
func UpdateUserNames(users []User, names map[int64]string) {
    for i := range users {
        if newName, ok := names[users[i].ID]; ok {
            // Обновляем имя, если в карте есть новое значение
            users[i].Name = newName
        }
    }
}

// Мы проходим по слайсу моделей и для каждого элемента смотрим, есть ли обновление в карте.

Заключение

Слайс модели (model-slice) — один из центральных инструментов, когда вы работаете с данными в Go. Он совмещает:

  • простоту стандартного слайса;
  • выразительность, когда вы добавляете к нему методы;
  • гибкость при работе с БД, API и бизнес-логикой.

Ключевые моменты, которые стоит держать в голове:

  • append всегда возвращает новый слайс — результат нужно присваивать;
  • срезы (a[low:high]) разделяют общий массив с исходным слайсом;
  • при необходимости изоляции делайте копию через copy или append в новый слайс;
  • для изменения длины слайса внутри функции удобнее возвращать новый слайс или принимать указатель;
  • аккуратно обращайтесь с указателями на элементы слайса и переменные цикла.

Используя эти принципы, вы сможете уверенно строить слой данных, писать читаемый код и избегать неприятных багов, связанных с общими массивами и неожиданными изменениями.

Частозадаваемые технические вопросы по теме статьи и ответы на них

Как правильно маршалить и анмаршалить слайс моделей в JSON

Если у вас есть слайс моделей []User, вы можете напрямую передавать его в json.Marshal:

data, err := json.Marshal(users)
// data - это []byte с JSON-массивом

// Для чтения из JSON делайте так:

var users []User
if err := json.Unmarshal(data, &users); err != nil {
    // Обрабатываем ошибку
}

Главное — передавать указатель на слайс в Unmarshal, чтобы он мог заполнить его данными.

Как удалить элемент из слайса модели по индексу

Стандартный паттерн удаления по индексу:

func RemoveAt(users []User, i int) []User {
    copy(users[i:], users[i+1:]) // Сдвигаем хвост влево
    users = users[:len(users)-1] // Укорачиваем слайс на 1
    return users
}

// Такой подход сохраняет порядок и не создает новый массив.

Как эффективно очистить слайс моделей, чтобы освободить память

Если вы хотите "забыть" данные и освободить память, одной обрезки длины мало. Лучше обнулить ссылки (если структура содержит указатели) и сбросить слайс:

users = nil

// Если нужно повторно использовать, можно сделать:

users = users[:0]

Так вы сохраните массив, но "забудете" элементы. Память под массив останется до сборки мусора или повторного использования.

Как избежать гонок данных при одновременном доступе к слайсу моделей

Слайсы не потокобезопасны. Если вы читаете и пишете один и тот же слайс из разных горутин, защищайте доступ mutex-ом или каналами:

type UserStore struct {
    mu    sync.RWMutex
    users []User
}

func (s *UserStore) GetAll() []User {
    s.mu.RLock()
    defer s.mu.RUnlock()
    out := make([]User, len(s.users))
    copy(out, s.users)
    return out
}

func (s *UserStore) Add(u User) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.users = append(s.users, u)
}

// Здесь мы защищаем слайс моделей с помощью мьютекса и отдаем копию для чтения.

Как преобразовать слайс моделей в карту для быстрого поиска по ID

Если вам нужно делать частые поиска по ID, удобно построить карту:

func IndexByID(users []User) map[int64]User {
    m := make(map[int64]User, len(users))
    for _, u := range users {
        m[u.ID] = u
    }
    return m
}

// Теперь поиск по ID становится O(1) вместо линейного прохода по слайсу.

Стрелочка влевоСлайс UI - паттерн ui-slice и работа со списками в пользовательском интерфейсеСлайс либы lib-slice - удобные утилиты для работы со срезами в GoСтрелочка вправо

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий