Сегмент utils - подробное объяснение возможностей utils-segment

19 февраля 2026
Автор

Олег Марков

Введение

Сегменты данных часто встречаются в задачах обработки массивов, строк, временных интервалов и любых диапазонов. Вам может понадобиться:

  • разбить большой массив на части;
  • сгруппировать элементы по какому-то признаку;
  • найти пересечения диапазонов;
  • эффективно обходить или модифицировать подпоследовательности.

Здесь и появляется потребность в наборе утилит, которые умеют работать с сегментами. Пакет (или модуль) utils-segment решает именно эти задачи: предоставляет удобные функции для создания, преобразования, поиска и объединения сегментов. В результате вы меньше пишете шаблонный код и меньше ошибаетесь на граничных условиях.

В статье я разберу:

  • какие сущности обычно есть в utils-segment;
  • как создавать сегменты и работать с ними;
  • как искать пересечения и объединять диапазоны;
  • как использовать сегменты для обработки массивов и строк;
  • типичные ошибки и граничные случаи.

Я буду опираться на условный, но реалистичный API библиотеки utils-segment. Даже если ваша реализация немного отличается по названиям, общие идеи и подходы будут теми же.


Что такое сегмент в контексте utils-segment

Базовое представление сегмента

В типичном варианте сегмент описывает полуоткрытый диапазон:

  • начало включительно;
  • конец не включительно.

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

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

// Segment описывает полуоткрытый диапазон [Start, End)
type Segment struct {
    Start int // Левая граница диапазона включительно
    End   int // Правая граница диапазона не включительно
}

// Length возвращает длину сегмента
func (s Segment) Length() int {
    // Если границы заданы корректно, длина неотрицательна
    return s.End - s.Start
}

// IsEmpty проверяет, пустой ли сегмент
func (s Segment) IsEmpty() bool {
    // Пустым считается сегмент нулевой длины
    return s.Start >= s.End
}

Смотрите, я сразу добавляю методы Length и IsEmpty, потому что они используются почти в каждом реальном коде, где есть сегменты.

В utils-segment вы чаще всего встретите:

  • структуру Segment с полями начала и конца;
  • вспомогательные функции проверки и нормализации;
  • операции над одним сегментом и коллекциями сегментов.

Создание и нормализация сегментов

Безопасное создание сегмента

Новички часто делают ошибку: меняют границы местами или передают некорректные значения. Поэтому в utils-segment обычно есть функция-конструктор, которая нормализует входные данные.

// NewSegment создает сегмент и при необходимости нормализует границы
func NewSegment(start, end int) Segment {
    if start > end {
        // Обратите внимание - мы меняем границы местами,
        // чтобы всегда получать "правильный" отрезок
        start, end = end, start
    }
    return Segment{Start: start, End: end}
}

Теперь вы можете писать:

s1 := NewSegment(10, 5)
// Здесь s1 будет иметь границы [5, 10)

s2 := NewSegment(0, 0)
// Пустой сегмент

Наличие такой функции упрощает всю дальнейшую работу, потому что вы можете опираться на инвариант: Start всегда меньше или равен End.

Проверка валидности сегмента

Полезно иметь быстрый способ понять, корректен ли сегмент:

// IsValid проверяет что границы заданы логично
func (s Segment) IsValid() bool {
    // В самом простом варианте - Start не должен быть больше End
    return s.Start <= s.End
}

В utils-segment нередко добавляют расширенные проверки (например, что границы не отрицательные), но базовый вариант обычно именно такой.


Отношения между сегментами

Одна из ключевых возможностей utils-segment — это операции сравнения и анализа взаимного расположения сегментов. Давайте разберемся, какие они бывают.

Пересечение сегментов

Чтобы узнать, пересекаются ли два сегмента, можно использовать простую проверку:

// Intersects проверяет пересечение двух сегментов
func (s Segment) Intersects(other Segment) bool {
    // Сегменты пересекаются если есть хоть одна общая точка
    return s.Start < other.End && other.Start < s.End
}

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

  • s = [2, 6)
  • t = [4, 10)

Пересечение есть, потому что:

  • 2 < 10 (левая граница первого меньше правой второго)
  • 4 < 6 (левая граница второго меньше правой первого)

А вот сегменты [0, 2) и [2, 5) не пересекаются, так как общая точка 2 не входит ни в один сегмент (у обоих правая граница не включительна).

Вычисление пересечения

Проверка — это хорошо, но вам чаще нужно получить сегмент-пересечение.

// Intersection возвращает сегмент-пересечение
// Если пересечения нет - возвращается пустой сегмент
func (s Segment) Intersection(other Segment) Segment {
    start := maxInt(s.Start, other.Start)
    end := minInt(s.End, other.End)

    if start >= end {
        // Возвращаем пустой сегмент
        return Segment{Start: 0, End: 0}
    }

    return Segment{Start: start, End: end}
}

func maxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func minInt(a, b int) int {
    if a < b {
        return a
    }
    return b
}

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

a := NewSegment(2, 6)
b := NewSegment(4, 10)

c := a.Intersection(b)
// c будет [4, 6)

Обратите внимание, как проверка start >= end аккуратно отсекает случаи отсутствия пересечения.

Объединение (слияние) двух сегментов

Иногда вам нужно объединить два сегмента в один, если они пересекаются или соприкасаются:

// CanMerge проверяет можно ли слить сегменты в один
func (s Segment) CanMerge(other Segment) bool {
    // Допускаем слияние пересекающихся или соприкасающихся сегментов
    return s.End >= other.Start && other.End >= s.Start
}

// Merge объединяет два сливаемых сегмента
// Предполагается что CanMerge вернул true
func (s Segment) Merge(other Segment) Segment {
    return Segment{
        Start: minInt(s.Start, other.Start),
        End:   maxInt(s.End, other.End),
    }
}

Теперь можно писать:

a := NewSegment(0, 5)
b := NewSegment(5, 10)

if a.CanMerge(b) {
    merged := a.Merge(b)
    // merged будет [0, 10)
}

Коллекции сегментов и их упорядочивание

В реальном коде вы почти всегда работаете не с одним, а с набором сегментов. Например, у вас есть множество занятых интервалов времени, или набор диапазонов индексов, которые нужно обработать.

Тип для слайса сегментов

Часто вводится отдельный тип:

// Segments это срез сегментов
type Segments []Segment

Это дает возможность вешать на него методы.

Сортировка сегментов

Почти все алгоритмы с коллекциями сегментов опираются на то, что данные отсортированы по левой границе.

import "sort"

// SortByStart сортирует сегменты по полю Start
func (segs Segments) SortByStart() {
    sort.Slice(segs, func(i, j int) bool {
        // Сортируем по возрастанию Start
        if segs[i].Start == segs[j].Start {
            // При равных стартах сортируем по End
            return segs[i].End < segs[j].End
        }
        return segs[i].Start < segs[j].Start
    })
}

Теперь можно делать так:

items := Segments{
    NewSegment(5, 10),
    NewSegment(0, 3),
    NewSegment(2, 8),
}

items.SortByStart()
// После сортировки порядок будет примерно таким:
// [0, 3), [2, 8), [5, 10)

Нормализация и слияние набора сегментов

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

Алгоритм нормализации

Смотрите, я покажу вам стандартную реализацию:

// Normalize сливает пересекающиеся и соприкасающиеся сегменты
func (segs Segments) Normalize() Segments {
    if len(segs) == 0 {
        // Возвращаем пустой список если сегментов нет
        return segs
    }

    // Сначала сортируем
    segs.SortByStart()

    // Результат будем накапливать в новом слайсе
    result := make(Segments, 0, len(segs))
    current := segs[0]

    for i := 1; i < len(segs); i++ {
        next := segs[i]

        if current.CanMerge(next) {
            // Сегменты соприкасаются или пересекаются - сливаем
            current = current.Merge(next)
        } else {
            // Текущий сегмент завершен добавляем его в результат
            result = append(result, current)
            current = next
        }
    }

    // Не забываем добавить последний сегмент
    result = append(result, current)

    return result
}

Теперь давайте посмотрим, что происходит в следующем примере:

segs := Segments{
    NewSegment(0, 2),
    NewSegment(1, 5),
    NewSegment(10, 12),
    NewSegment(12, 15),
}

normalized := segs.Normalize()
// normalized будет содержать:
// [0, 5), [10, 15)

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


Вычитание и разность сегментов

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

Вычитание одного сегмента из другого

Разберемся с простейшим случаем: есть базовый сегмент base и сегмент cut, который нужно убрать.

// Subtract вычитает из сегмента s сегмент other
// Возвращает список сегментов которые остаются после вычитания
func (s Segment) Subtract(other Segment) Segments {
    // Если нет пересечения - возвращаем исходный сегмент как есть
    if !s.Intersects(other) {
        return Segments{s}
    }

    // На этом этапе пересечение точно есть
    result := Segments{}

    // Левая часть если она есть
    if other.Start > s.Start {
        // Добавляем левый "хвост" [s.Start, other.Start)
        result = append(result, Segment{
            Start: s.Start,
            End:   other.Start,
        })
    }

    // Правая часть если она есть
    if other.End < s.End {
        // Добавляем правый "хвост" [other.End, s.End)
        result = append(result, Segment{
            Start: other.End,
            End:   s.End,
        })
    }

    return result
}

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

base := NewSegment(0, 10)
cut := NewSegment(3, 7)

rest := base.Subtract(cut)
// rest будет содержать:
// [0, 3) и [7, 10)

Теперь посмотрим крайние варианты:

  • вычитаемый сегмент полностью покрывает исходный — вы получите пустой список;
  • вычитаемый сегмент не пересекается — получите список из одного исходного сегмента;
  • вычитаемый сегмент касается только края — останется только одна сторона.

Работа с массивами через сегменты

Часто utils-segment используется как вспомогательный инструмент при обработке массивов или срезов. Вместо того чтобы хранить сами подмассивы, вы оперируете диапазонами индексов.

Применение сегмента к срезу

Давайте посмотрим, как аккуратно применять сегмент к срезу:

// ApplyToSlice безопасно применяет сегмент к срезу
// и возвращает подмассив соответствующий диапазону
func ApplyToSlice[T any](data []T, s Segment) []T {
    // Ограничиваем границы размером среза
    if s.Start < 0 {
        s.Start = 0
    }
    if s.End > len(data) {
        s.End = len(data)
    }
    if s.Start >= s.End {
        // Если после нормализации сегмент пуст - возвращаем пустой срез
        return []T{}
    }
    // Возвращаем подпоследовательность
    return data[s.Start:s.End]
}

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

nums := []int{10, 20, 30, 40, 50, 60}
seg := NewSegment(1, 4)

part := ApplyToSlice(nums, seg)
// part будет []int{20, 30, 40}

Обратите внимание, что функция аккуратно обрезает сегмент, если он выходит за границы среза.

Итерация по массиву по сегментам

В utils-segment часто добавляют функцию, которая позволяет пройтись по массиву сегментами фиксированного размера.

// SplitIntoSegments делит диапазон [0, n) на сегменты длиной step
// Последний сегмент может быть короче
func SplitIntoSegments(n, step int) Segments {
    result := Segments{}
    if step <= 0 || n <= 0 {
        // Если шаг некорректный или длина нулевая - возвращаем пустой список
        return result
    }

    for start := 0; start < n; start += step {
        end := start + step
        if end > n {
            // Последний сегмент обрезаем по n
            end = n
        }
        result = append(result, Segment{Start: start, End: end})
    }

    return result
}

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

data := []int{1, 2, 3, 4, 5, 6, 7}
segs := SplitIntoSegments(len(data), 3)
// segs будет:
// [0, 3), [3, 6), [6, 7)

for _, seg := range segs {
    chunk := ApplyToSlice(data, seg)
    // Здесь chunk - очередной подмассив размера до 3 элементов
    _ = chunk // используйте chunk по своему усмотрению
}

Такой подход часто применяют при пакетной обработке данных, например, при отправке батчами или при параллельной обработке.


Работа со строками через сегменты

Идея та же, но вместо числового массива у нас строка. Важно помнить, что в Go строка индексируется по байтам, а не по рунам.

Сегменты байтов строки

Если utils-segment реализован для целочисленных индексов, вы можете применять его к строкам как к набору байт. Но будьте аккуратны с Unicode.

// ApplyToString применяет сегмент к строке по байтовым позициям
func ApplyToString(s string, seg Segment) string {
    // Аналогичная защита от выхода за границы
    if seg.Start < 0 {
        seg.Start = 0
    }
    if seg.End > len(s) {
        seg.End = len(s)
    }
    if seg.Start >= seg.End {
        // Возвращаем пустую строку если сегмент пустой
        return ""
    }
    return s[seg.Start:seg.End]
}

Давайте посмотрим на пример:

text := "Hello world"
seg := NewSegment(0, 5)

sub := ApplyToString(text, seg)
// sub будет "Hello"

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


Поиск позиции и включенности

Проверка, содержит ли сегмент точку

Еще одна базовая операция — понять, входит ли индекс в сегмент.

// Contains проверяет входит ли точка x в сегмент [Start, End)
func (s Segment) Contains(x int) bool {
    return x >= s.Start && x < s.End
}

Теперь можно писать:

seg := NewSegment(10, 20)

seg.Contains(10) // true
seg.Contains(19) // true
seg.Contains(20) // false - правая граница не включительна

Поиск сегмента по точке в отсортированном списке

Когда у вас есть отсортированный список сегментов, удобно быстро находить, какой из них покрывает данную точку. Для этого часто используют бинарный поиск.

// FindByPoint ищет первый сегмент который содержит точку x
// Возвращает индекс и флаг найден ли сегмент
func (segs Segments) FindByPoint(x int) (int, bool) {
    left, right := 0, len(segs)-1

    for left <= right {
        mid := (left + right) / 2
        s := segs[mid]

        if s.Contains(x) {
            // Нашли сегмент который содержит точку
            return mid, true
        }
        if x < s.Start {
            // Точка левее сегмента - двигаемся влево
            right = mid - 1
        } else {
            // Точка правее сегмента - двигаемся вправо
            left = mid + 1
        }
    }

    // Не нашли подходящий сегмент
    return -1, false
}

Важно, чтобы сегменты были отсортированы и не пересекались (например, после Normalize). Тогда поиск будет работать корректно и быстро.


Сложные операции над множеством сегментов

Теперь давайте перейдем к более сложным задачам, которые utils-segment тоже помогает решать.

Пересечение двух наборов сегментов

Представьте, что у вас есть два множества интервалов A и B. Требуется найти их пересечение. Это похоже на алгоритм слияния двух отсортированных списков.

// IntersectSets находит пересечение двух наборов сегментов
func IntersectSets(a, b Segments) Segments {
    // Нормализуем и сортируем оба множества
    a = a.Normalize()
    b = b.Normalize()

    result := Segments{}
    i, j := 0, 0

    for i < len(a) && j < len(b) {
        sa := a[i]
        sb := b[j]

        if sa.Intersects(sb) {
            // Есть пересечение - добавляем его
            inter := sa.Intersection(sb)
            if !inter.IsEmpty() {
                result = append(result, inter)
            }
        }

        // Двигаем тот сегмент у которого правая граница меньше
        if sa.End < sb.End {
            i++
        } else {
            j++
        }
    }

    return result
}

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

a := Segments{
    NewSegment(0, 5),
    NewSegment(10, 20),
}

b := Segments{
    NewSegment(3, 12),
}

c := IntersectSets(a, b)
// c будет:
// [3, 5), [10, 12)

Вычитание набора сегментов из другого набора

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

// SubtractSets вычитает из множества base множество cuts
func SubtractSets(base, cuts Segments) Segments {
    base = base.Normalize()
    cuts = cuts.Normalize()

    result := Segments{}
    j := 0 // указатель по cuts

    for _, b := range base {
        // Текущий список частей сегмента после вычитаний
        parts := Segments{b}

        // Пока вычитающий сегмент потенциально пересекается с b
        for j < len(cuts) && cuts[j].End <= b.Start {
            // Пропускаем все сегменты которые точно слева от b
            j++
        }

        k := j
        for k < len(cuts) && cuts[k].Start < b.End {
            cut := cuts[k]

            newParts := Segments{}
            for _, p := range parts {
                // Вычитаем cut из текущей части
                sub := p.Subtract(cut)
                // Добавляем все оставшиеся куски
                newParts = append(newParts, sub...)
            }
            parts = newParts
            k++
        }

        // Добавляем все оставшиеся части базового сегмента
        result = append(result, parts...)
    }

    return result.Normalize()
}

Как видите, этот код выполняет поэтапное разбиение сегментов base всем набором cuts и собирает то, что осталось.


Типичные граничные случаи и ошибки

Путаница с включительностью правой границы

Самая распространенная проблема:

  • вы считаете, что сегмент [Start, End] (обе границы включительны),
  • а код работает с [Start, End) (правая граница не включительна).

Это приводит к:

  • неверной длине;
  • ошибкам в проверке пересечений;
  • пропущенным или дублирующимся элементам на стыках.

Решение: явно фиксируйте в документации и в комментариях, что сегмент полуоткрытый. Например:

// Segment описывает диапазон индексов [Start, End)
// Правая граница не включительна
type Segment struct { ... }

И всегда проверяйте Contains и Intersects с учетом этого правила.

Несортированные и ненормализованные коллекции

Многие алгоритмы из utils-segment предполагают:

  • коллекции отсортированы;
  • коллекции нормализованы (нет пересечений и "дыр" между сливаемыми сегментами).

Если вы забыли вызвать Normalize или SortByStart, вы можете получить:

  • дубликаты в результате;
  • пропущенные участки;
  • некорректные данные на выходе.

Решение простое: перед сложными операциями вроде IntersectSets или SubtractSets всегда приводите коллекции к нормальной форме.

Отрицательные границы и выход за пределы массива

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

  • Start не был отрицательным;
  • End не выходил за размер коллекции.

Именно поэтому функции ApplyToSlice и ApplyToString из примеров делают дополнительную нормализацию.


Как встроить utils-segment в реальный проект

Разделение ответственности

Практика показывает, что удобнее:

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

utils-segment хорошо ложится на уровень бизнес-логики: вы оперируете абстрактными сегментами, не думая о конкретной структуре хранения.

Примеры задач

Где сегменты особенно полезны:

  1. Обработка логов и временных интервалов:

    • фильтрация интервалов по рабочему времени;
    • исключение периодов простоя;
    • объединение пересекающихся временных окон.
  2. Работа с текстами:

    • подсветка фрагментов (сегменты по индексам символов или байт);
    • скрытие или маскирование отдельных частей строки;
    • вырезание и перестановка блоков текста.
  3. Обработка больших массивов:

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

Заключение

utils-segment — это не про сложные алгоритмы, а про аккуратную и безопасную работу с диапазонами. Библиотека обычно предоставляет:

  • тип Segment с четко определенной семантикой;
  • вспомогательные функции создания и нормализации;
  • операции над одиночными сегментами (пересечение, объединение, вычитание);
  • операции над коллекциями сегментов (сортировка, нормализация, пересечение наборов, вычитание наборов);
  • интеграцию с массивами и строками через индексы.

Если вы будете:

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

то utils-segment станет удобным и надежным инструментом для обработки любых "отрезков" данных в вашем приложении.


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

Как хранить вместе сегменты по разным типам данных (строки, числа, время)

Лучше не смешивать разные типы в одном Segment. Создайте несколько типизированных оберток:

  • IntSegment с полями int;
  • TimeSegment с полями time.Time;
  • ByteSegment для работы с байтовыми смещениями.

Внутри можно использовать общую логику через дженерики или вспомогательные функции. Так вы избежите путаницы единиц измерения.

Как эффективно сериализовать сегменты в JSON

Используйте структуру с явными полями Start и End. Например:

type Segment struct {
    Start int `json:"start"`
    End   int `json:"end"`
}

Для коллекций сегментов просто используйте []Segment. Если нужны инварианты (например, Start <= End), добавьте отдельный шаг валидации после десериализации и вызывайте его там, где принимаете данные извне.

Как лучше логировать коллекции сегментов чтобы не засорять логи

Старайтесь логировать:

  • только нормализованные коллекции;
  • только агрегированную информацию при больших объемах (количество сегментов, суммарную длину).

При необходимости подробного логирования используйте отладочный уровень (debug) и ограничивайте количество выводимых элементов.

Как избежать лишних аллокаций при частых операциях Normalize и Merge

Оптимизация в два шага:

  1. Переиспользуйте слайсы: передавайте буфер результата с нужной емкостью и очищайте его через срез с нулевой длиной.
  2. Не создавайте временные срезы внутри циклов без необходимости. Если Normalize вызывается очень часто, стоит вынести sort и слияние в один проход, используя существующий массив.

Как тестировать функции работы с сегментами чтобы не пропустить граничные случаи

Сфокусируйтесь на:

  • пустых сегментах (Start == End);
  • соприкасающихся сегментах (End == Start);
  • полностью вложенных сегментах;
  • сегментах без пересечения;
  • отрицательных и нулевых границах если они допустимы.

Для каждой функции (Intersects, Intersection, Merge, Normalize, SubtractSets) составьте таблицу тестов с этими сценариями. Это сильно снижает риск логических ошибок на границах.

Сегмент types - работа со срезами и типами сегментов в GoСтрелочка вправо

Все гайды по Feature-sliced_design

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

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