Гид по технологиям

Реализация графа на Go

6 min read Программирование Обновлено 11 Apr 2026
Графы на Go: реализация и примеры
Графы на Go: реализация и примеры

Рисунок графа на бумаге рядом с ручками и линейкой

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

Что такое граф

Граф — это нелинейная структура данных, состоящая из набора узлов (вершин) и связей между ними (рёбер). Простыми словами:

  • Узел (vertex) — сущность (например, пользователь, устройство, точка на карте).
  • Ребро (edge) — связь или отношение между двумя узлами.

Классификация по направленности:

  • Неориентированный граф: ребро (A—B) означает двустороннюю связь.
  • Ориентированный граф: ребро (A->B) имеет направление.

Краткое определение: граф — универсальная модель связей, удобная для выражения отношений между объектами.

Важно: выбор представления графа (список смежности, матрица смежности, edge-list) сильно влияет на эффективность операций и использование памяти.

Представление графа в Go — список смежности

В примерах ниже мы реализуем неориентированный граф с использованием списка смежности: map[int]*Node, где ключ — уникальный ID узла, а значение — указатель на структуру узла.

Оригинальная структура узла (Node) для простого неориентированного графа выглядит так:

type Node struct {  
    Neighbors []*Node  
}  

Для ориентированного графа Node можно расширить так:

type Node struct {  
    OutNeighbors []*Node // outgoing edges  
    InNeighbors  []*Node // incoming edges  
}  

Структура графа (Graph) на базе списка смежности:

type Graph struct {  
    nodes map[int]*Node  
}  

Функция-конструктор для Graph:

func NewGraph() *Graph {  
    return &Graph{  
        nodes: make(map[int]*Node),  
    }  
}  

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

Добавление узла

Функция добавления узла проверяет, существует ли уже узел с таким ID, и если нет — создаёт его:

func (g *Graph) AddNode(nodeID int) {  
    if _, exists := g.nodes[nodeID]; !exists {  
        newNode := &Node{  
            Neighbors: []*Node{},  
        }  
        g.nodes[nodeID] = newNode  
        fmt.Println("New node added to graph")  
    } else {  
        fmt.Println("Node already exists!")  
    }  
}  

Примечание: в реальном приложении вместо логов через fmt.Println разумно возвращать ошибку или булево значение (ошибки удобнее для тестирования и обработки).

Добавление ребра (неориентированный граф)

Для неориентированного графа добавление ребра сводится к тому, чтобы добавить друг друга в срезы Neighbors:

func (g *Graph) AddEdge(nodeID1, nodeID2 int) {  
    node1 := g.nodes[nodeID1]  
    node2 := g.nodes[nodeID2]  
  
    node1.Neighbors = append(node1.Neighbors, node2)  
    node2.Neighbors = append(node2.Neighbors, node1)  
}  

Замечание по безопасности: код предполагает, что оба узла существуют. В продакшен-реализации стоит проверять существование и избегать добавления дубликатов ребра.

Удаление ребра

Реализация удаления ребра представлена двумя функциями: приватной removeEdge для удаления соседа из среза и публичной RemoveEdge для симметричного удаления в неориентированном графе:

func (g *Graph) removeEdge(node, neighbor *Node) {  
    index := -1  
    for i, n := range node.Neighbors {  
        if n == neighbor {  
            index = i  
            break  
        }  
    }  
    if index != -1 {  
        node.Neighbors =   
            append(node.Neighbors[:index], node.Neighbors[index+1:]...)  
    }  
}  
  
func (g *Graph) RemoveEdge(node, neighbor *Node) {  
    g.removeEdge(node, neighbor)  
    g.removeEdge(neighbor, node)  
    fmt.Println("Edge successfully removed")  
}  

Этот подход работает, но имеет линейную сложность по размеру списка соседей для поиска индекса. Для графов с большим числом соседей можно использовать вспомогательную структуру (map) для O(1)-удаления.

Удаление узла

Удаление узла требует удаления всех входящих/исходящих рёбер и удаления записи из map:

func (g *Graph) RemoveNode(nodeID int) {  
    node, exists := g.nodes[nodeID]  
    if !exists {  
        fmt.Println("Node doesn't exist")  
        return  
    }  
  
    for _, neighbor := range node.Neighbors {  
        g.RemoveEdge(node, neighbor)  
    }  
    delete(g.nodes, nodeID)  
    fmt.Println("Node deleted successfully")  
}  

Обходы графа: DFS и BFS (шаблоны)

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

Пример DFS (рекурсивно):

func (g *Graph) DFS(startID int, visit func(id int, node *Node)) {  
    start, ok := g.nodes[startID]  
    if !ok {  
        return  
    }  
    visited := make(map[*Node]bool)  
    var dfs func(n *Node, id int)  
    dfs = func(n *Node, id int) {  
        if visited[n] {  
            return  
        }  
        visited[n] = true  
        visit(id, n)  
        for _, nb := range n.Neighbors {  
            dfs(nb, id) // в реальной реализации передавайте реальный ID соседа  
        }  
    }  
    dfs(start, startID)  
}  

Пример BFS (итеративно):

func (g *Graph) BFS(startID int, visit func(id int, node *Node)) {  
    start, ok := g.nodes[startID]  
    if !ok {  
        return  
    }  
    queue := []*Node{start}  
    visited := make(map[*Node]bool)  
    visited[start] = true  
    for len(queue) > 0 {  
        node := queue[0]  
        queue = queue[1:]  
        visit(startID, node) // как и в DFS, в реальности передавайте ID текущего узла  
        for _, nb := range node.Neighbors {  
            if !visited[nb] {  
                visited[nb] = true  
                queue = append(queue, nb)  
            }  
        }  
    }  
}  

Примечание: в шаблонах выше мы используем map[*Node]bool для трекинга посещённых узлов. Если у вас есть уникальные ID, лучше использовать map[int]bool и явно передавать ID при посещении.

Ограничения и когда такой подход не подходит (Counterexamples)

  • Для плотных графов (много рёбер близко к n^2) матрица смежности будет оперативнее для некоторых операций (проверка существования ребра O(1)).
  • Если важно быстро проверять существование ребра/удалять ребро за O(1), храните соседей не в срезе []*Node, а в map[int]struct{}.
  • Для распределённых/долговременных хранилищ стоит выбирать графовые БД (Neo4j, JanusGraph) вместо in-memory структур.

Альтернативные представления графа

  1. Матрица смежности (adjacency matrix): квадратная матрица n×n, где элемент [i][j] показывает наличие/вес ребра. Удобно для плотных графов.

Пример: матрица в Go

// matrix := make([][]bool, n)
// for i := range matrix {
//     matrix[i] = make([]bool, n)
// }
  1. Edge-list: список рёбер (tuple list). Удобно для передачи/сериализации и некоторых алгоритмов (например, алгоритмы минимального остовного дерева).

  2. Модифицированный список смежности: вместо среза соседей — map[int]struct{} или map[int]Edge{…} для хранения веса и быстрых проверок.

Оптимизации и шаблоны (cheat sheet)

  • Проверка существования ребра: вместо перебора среза используйте map[int]struct{} (O(1)).
  • Удаление ребра: с map — O(1); со срезом — O(k) где k = степень узла.
  • Хранение дополнительной информации: если узел хранит payload (например, профиль пользователя), добавьте поле Data interface{} или конкретную структуру в Node.
  • Для взвешенных графов храните структуру Edge {to *Node; weight int} или map[int]float64.

Пример структуры узла с map-соседями:

type NodeMap struct {
    Neighbors map[int]*NodeMap
    ID        int
}

// Тогда Graph: nodes map[int]*NodeMap

Пример: быстрый edge-check и удаление с помощью map

// Добавление соседей как map[int]struct{}
func (g *Graph) AddEdgeFast(a, b int) {
    na := g.nodes[a]
    nb := g.nodes[b]
    // предполагается, что na и nb существуют
    // вместо слайса у каждого Node есть map[int]struct{}
    // na.Adj[b] = struct{}{}
    // nb.Adj[a] = struct{}{}
}

Такой подход значительно упрощает операции наличия/удаления ребра для узлов с большим числом соседей.

Модель принятия решений: когда выбирать список смежности

  • Если граф разрежённый (количество рёбер m значительно меньше n^2): используйте список смежности.
  • Если часто нужна проверка наличия ребра и граф плотный: матрица смежности будет лучше.
  • Для динамических графов с частыми вставками/удалениями рёбер: используйте map-основанное представление соседей.

Mermaid-диаграмма (решающее дерево):

flowchart TD
    A[Начать: нужно хранить граф?] --> B{Граф плотный?}
    B -- Да --> C[Матрица смежности]
    B -- Нет --> D{Требуется быстрый check edge?}
    D -- Да --> E[Список смежности с map соседей]
    D -- Нет --> F[Список смежности со срезами]

Чеклист по ролям

Инженер (разработка):

  • Оцени размер n и m.
  • Выбери представление (list/matrix/edge-list).
  • Добавь проверки существования узлов в методах AddEdge/RemoveEdge.
  • Напиши модульные тесты на добавление/удаление и обходы.

Интервьюер:

  • Попросите кандидата объяснить trade-offs (память vs. скорость).
  • Попросите реализовать AddNode, AddEdge и простую проверку существования пути (BFS).

Код-ревьюер:

  • Проверь корректность обработки ошибок (nil-посылки).
  • Убедись, что нет дубликатов рёбер при добавлении.
  • Оцени сложность операций и ожидаемый объём данных.

Мини-методология: как проектировать граф для приложения

  1. Описать сущности и связи (узлы и виды рёбер).
  2. Оценить ожидаемое количество узлов и рёбер.
  3. Выбрать представление (list/matrix/map-based).
  4. Определить операции (только чтение, частые вставки, удаление).
  5. Подготовить тесты на крайние случаи и производительность.

Критерии приёмки

  • Корректная работа методов AddNode/AddEdge/RemoveNode/RemoveEdge.
  • Отсутствие утечек памяти и гонок (если граф используется параллельно).
  • Наличие тестов для основных сценариев (вставка, удаление, обход).

Безопасность и приватность

  • В памяти граф может содержать персональные данные. Убедитесь, что доступ к структурам контролируется.
  • Для логирования не выводите в логи чувствительные поля узлов.
  • При сериализации графа применяйте подходящие меры удаления PII в соответствии с требованиями локального законодательства.

Примеры использования и сценарии

  • Социальная сеть: узлы — пользователи, рёбра — дружба.
  • Маршрутизация: узлы — остановки/узлы сети, рёбра — пути с весом (время/расстояние).
  • Рекомендации: узлы — пользователи и товары, рёбра — взаимодействия.

Сопутствующие советы по производительности

  • Профилируйте память и CPU: простая оптимизация структур данных часто даёт больше выигрыша, чем микрооптимизации в алгоритмах.
  • При большой нагрузке рассмотрите специализированные графовые СУБД или хранение части графа на диске.
  • Для многопоточного доступа используйте защиту (mutex) или неизменяемые структуры и копирование при необходимости.

Небольшой набор тест-кейсов (для unit-тестов)

  • Добавление узла, затем проверка его существования.
  • Добавление ребра между двумя узлами, проверка соседей.
  • Удаление ребра и проверка, что связь разорвана в обе стороны.
  • Удаление узла и проверка, что он удалён из всех соседних списков.
  • BFS/DFS на маленьком графе с известной структурой.

Краткая сводка: когда использовать графы

  • Используйте графы, когда у вас есть ясные объекты и отношения между ними.
  • Не используйте графы, если связи просты и не требуют сложных навигаций (иногда SQL-таблица связей проще).

WИТОГ

Графы — мощный инструмент моделирования связей. В Go легко реализовать базовый неориентированный граф через список смежности. Для продакшен-решений важно учитывать требования к проверке существования рёбер, количеству данных и необходимости в быстрых операциях удаления. Всегда выбирайте представление графа исходя из ожидаемой плотности, характера операций и ограничений по памяти.

Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

Движение игрока в Godot — 2D руководство
Разработка игр

Движение игрока в Godot — 2D руководство

Как печатать с Android: простое руководство
Инструкции

Как печатать с Android: простое руководство

Как удалить DRM с музыки — инструменты и инструкции
Аудио

Как удалить DRM с музыки — инструменты и инструкции

Защита паролем файлов и папок на Mac
macOS безопасность

Защита паролем файлов и папок на Mac

Редактирование фотографий в OneDrive
Облако

Редактирование фотографий в OneDrive

Изменить DNS в Windows — шаги и советы
Сеть

Изменить DNS в Windows — шаги и советы