Константные массивы в Go: как объявить и не сойти с ума

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: вечный вопрос из Stack Overflow

Если вы когда-нибудь гуглили "Declare a constant array in Go", то наверняка видели ту самую тему на Stack Overflow с тысячами просмотров. И да, вы не одиноки в своём замешательстве. В Go действительно нельзя просто написать const arr = [...]int{1, 2, 3} — компилятор скажет вам вежливое "нет".

Почему так? И как тогда быть, если нам нужен неизменяемый список данных? В этой статье мы разберёмся с природой констант в Go, узнаем, почему массивы не могут быть константами, и найдём лучшие обходные пути. Спойлер: решение есть, и оно даже красивее, чем вы думаете.

Почему Go не позволяет объявить константный массив?

Чтобы понять это, нужно заглянуть под капот языка. В Go константы — это неизменяемые значения, вычисляемые на этапе компиляции. Они должны быть строго типизированными и известны до запуска программы. Массивы же — это составные типы данных, которые занимают память и могут быть изменены.

Вот простой пример, который НЕ скомпилируется:

package main

const arr = [3]int{1, 2, 3} // Ошибка: const initializer [3]int{1, 2, 3} is not a constant

func main() {
    fmt.Println(arr)
}

Компилятор говорит: "Эй, это не константа!". И он прав. Даже если массив объявлен как const, Go не гарантирует, что его содержимое не изменится через указатели или слайсы. Поэтому разработчики языка решили: никаких константных массивов.

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

Способ 1: Используем переменную с комментарием "не трогать"

Самый простой и наивный способ — просто объявить переменную и надеяться, что никто её не изменит. Но мы же серьёзные разработчики, верно?

package main

import "fmt"

// arr - константный массив. Пожалуйста, не меняйте его.
var arr = [3]int{1, 2, 3}

func main() {
    fmt.Println(arr)
    // arr[0] = 100 // Раскомментируйте — и всё сломается
}

Минусы очевидны: это не настоящая константа. Любой может изменить массив через присваивание. Но для быстрых прототипов или маленьких проектов — сойдёт.

Способ 2: Функция-геттер (правильный путь)

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

package main

import "fmt"

// getColors возвращает копию массива цветов.
// Массив защищён от изменений извне.
func getColors() [3]string {
    return [3]string{"Red", "Green", "Blue"}
}

func main() {
    colors := getColors()
    fmt.Println(colors)
    // colors[0] = "Yellow" // Это изменит копию, а не оригинал
}

Почему это хорошо? Потому что:

  • Массив не может быть изменён извне — функция возвращает копию.
  • Код читаемый и понятный.
  • Легко тестировать и поддерживать.

Но есть нюанс: каждый вызов функции создаёт новую копию массива. Если массив большой (например, 1000 элементов), это может быть затратно. В таком случае используйте следующий способ.

Способ 3: Пакетная переменная с приватным доступом

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

package constants

// Массив доступен только внутри пакета
var defaultPorts = [5]int{80, 443, 8080, 3000, 5000}

// GetDefaultPorts возвращает копию массива портов.
func GetDefaultPorts() [5]int {
    return defaultPorts
}

Теперь пользователи вашего пакета могут только читать данные, но не изменять их. Идеально для библиотек.

Способ 4: Использование iota для константных наборов

А что, если вам нужны не сами значения, а их индексы? Например, дни недели или статусы. Тут на помощь приходит iota — генератор последовательных констант.

package main

import "fmt"

const (
    Monday = iota
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
    Sunday
)

// dayNames — это не константный массив, а переменная, но она инициализируется один раз
var dayNames = [7]string{
    "Понедельник",
    "Вторник",
    "Среда",
    "Четверг",
    "Пятница",
    "Суббота",
    "Воскресенье",
}

func main() {
    fmt.Println(dayNames[Monday]) // "Понедельник"
    fmt.Println(dayNames[Friday]) // "Пятница"
}

Это не настоящий константный массив, но работает как задумано. Индексы — константы, а сами названия — переменная.

Способ 5: Слайс vs Массив — что выбрать?

Многие разработчики путают массивы и слайсы. В Go массив — это фиксированный набор элементов, а слайс — динамический. Если вам нужен константный список, возможно, лучше использовать слайс?

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

package main

import "fmt"

// Константный слайс? Почти.
var mySlice = []int{1, 2, 3}

func main() {
    fmt.Println(mySlice)
    // mySlice[0] = 100 // Можно, но не нужно
}

Лучше использовать функцию-геттер и для слайсов:

func getMySlice() []int {
    return []int{1, 2, 3}
}

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

Практический пример: конфигурация приложения

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

package main

import (
    "fmt"
    "strings"
)

// allowedMethods — приватный массив методов
var allowedMethods = [5]string{"GET", "POST", "PUT", "DELETE", "PATCH"}

// IsMethodAllowed проверяет, разрешён ли метод
func IsMethodAllowed(method string) bool {
    for _, m := range allowedMethods {
        if strings.EqualFold(m, method) {
            return true
        }
    }
    return false
}

func main() {
    fmt.Println(IsMethodAllowed("GET"))    // true
    fmt.Println(IsMethodAllowed("OPTIONS")) // false
}

Теперь allowedMethods защищён от изменений, а код остаётся читаемым и эффективным.

Что говорят гуру Go?

Блоги

Рекомендаций по книгам