Привет!
Буду с вами честен, для меня читать сухие переводы документации или смотреть 40-часовые видеокурсы - то еще удовольствие. Поэтому я решил устроить эксперимент и разбирать Go прямо в публичном поле, шаг за шагом документируя весь процесс.
Я не новичок в разработке - повседневно пишу на Python и TypeScript, знаком с базовым devops. Но вот на территорию Go осознанно захожу впервые.
Я возьму за основу официальный трек обучения от создателей языка и постараюсь подробно разобрать как Go устроен. Мне интересно не только то, как реализованы конкретные механизмы языка, но и почему/зачем так сделано, попутно проводя параллели с теми технологиями, к которым я уже привык.
Этот цикл статей - мой интерактивный учебник, который я пишу в реальном времени параллельно с изучением языка. Я не Go-гуру и не архитектор с многолетним опытом в этом стеке.
Поэтому если вы опытный Go-разработчик и видите в тексте неточность или косяк - буду искренне рад конструктивным подсказкам в комментариях.
Иногда я буду осознанно сворачивать с официальной тропы, чтобы узнать тему чуть глубже и точнее, чем это предлагает базовый туториал
Пакеты и импорты
Официальный интерактивный курс встречает нас знакомством с таким понятием как «package». Услышав данное слово, у меня в голове сразу вспоминается Java. Как и в Java, пакет - это директория. Но различий тут больше, чем сходств.
package main import ( "fmt" "math/rand" ) func main() { fmt.Println("My favorite number is", rand.Intn(10)) }
Демонстрационный код документации сразу знакомит нас с двумя примерами импорта.
rand и fmt- стандартные пакеты, поэтому сложно представить как бы импортировались пользовательские пакеты. Чтобы это понять, давайте нарисуем структуру гипотетического проекта, где мы попытались воссоздать логику стандартных библиотек локально:
project/ <-- Корневая папка проекта ├── main.go <-- Go файл корневого пакета ├── fmt/ <-- Пакет fmt │ └── printer.go <-- Go файл пакета fmt └── math/ <-- Пакет "math" └── rand/ <-- *Вложенный* пакет "rand" └── generator.go <-- Go файл пакета rand
В Go нет вложенных пакетов, как может показаться из строки import "math/rand". Вложенность папок существует исключительно как инструкция для компилятора - по какому пути на диске искать код.
Т.е. мы не можем обратиться к пакету через родительский пакет:
package main import "math" func main() { math.rand.Intn() // ОШИБКА! rand не импортирован }
undefined: math.rand
Импортируем только по прямому пути к пакету:
package main import ( "math/rand" "fmt" ) func main() { fmt.Println(rand.Intn(5)) }
В самих Go-файлах при объявлении пакета всегда пишется просто плоское имя пакета без какой-либо вложенности или путей. Вы никогда не увидите конструкцию package math/rand. При этом, хоть в 99% случаев это имя совпадает с названием текущей папки, технически оно от него не зависит и имя директории, в которой лежат файлы пакета, может отличаться от названия пакета.
Ссылаясь на официальную документацию:
«Пакет — это набор исходных файлов в одном каталоге, которые компилируются вместе»
Значит, можно сказать, что пакет - это атомарная, неделимая единица компиляции. Именно то, что компилируются вместе в единый неделимый блок, объясняет, почему в Go нельзя импортировать конкретную переменную или функцию (как мы привыкли делать в Python через from module import func или в TS через import { точечно })
Несмотря на то, что мы импортируем целый пакет, это не влияет на вес скомпилированного бинарника. За удаление неиспользуемого кода отвечает линкер Go. Данный процесс называется Dead Code Elimination и является одним из финальных этапов компиляции.
Цитируя вторую часть трактовки пакетов в официальной документации:
«Функции, типы, переменные и константы, определенные в одном исходном файле, видны всем остальным исходным файлам в том же пакете»
Делаем вывод, что абсолютно всё, что объявлено на верхнем уровне в Go-файлах одного пакета, доступно в любом другом файле этого же пакета по умолчанию. Никаких взаимных импортов между файлами одной папки быть не должно.
Создадим рядом два файла в одной папке:
//test.go package main func test() string { return "test function from another go file" }
//main.go package main import "fmt" func main() { fmt.Println(test()) }
Результат выполнения:
test function from another go file
Важно упомянуть роль пакетa main и функции main. В некоторых статьях, упоминается, что каждая Go-программа, должна содержать пакет main и соответственно функцию main. Это отнюдь неверное утверждение. Если ваш проект предполагает компиляцию в исполняемый файл, то ,несомненно, у программы должна быть точка входа - для этого и есть пакет main с функцией main. Но если ваш проект - это библиотека, то зачем ей входной пакет с соответствующей функцией, кроме как какого-то тестирования? Отсюда, закрепим знание, что в проекте на Go нет обязательного правила наличия пакета main.
Зарезервированные функции пакетов
Их всего 2 - main()иinit().
Мы уже обсудили значение функции main(), но что будет если объявить ее в пакетах, отличных от main?
Ответ: данная функция станет обычной, не зарезервированной, т.е. потеряет свое свойство входной точки программы.
Значит, данная функция будет зарезервированной и не потеряет свои свойства только внутри пакета main.
Но что за функция init()?init() - это зарезервированная функция в Go, которая автоматически вызывается рантаймом (средой выполнения) при импорте пакета. Она не принимает никаких аргументов и ничего не возвращает.
Главная цель init() - это первоначальная настройка окружения и подготовка состояния пакета к работе до того, как начнет выполняться основной код программы (в том числе перед функцией main).
Примеры использования: проверка переменных окружения, инициализация глобальных состояний, регистрация драйвера базы данных и т.п.
Функция init()может быть объявлена в пакете множество раз, это допускается, в таком случае они просто выполнятся друг за другом в том виде, в котором представлены компилятору. Согласно документации передача происходит в алфавитном порядке.
Если мы импортируем несколько разных независимых друг от друга пакетов с функциями init(), они будут инициализированы в алфавитном порядке путей этих пакетов.
Использование функции
init()не рекомендуется. Подробнее об этом можно прочитать тут
В случае, если между пакетами есть зависимость (например, пакет Б импортирует пакет А), порядок будет определяться деревом зависимостей: сначала полностью инициализируется самый глубокий (независимый ни от кого в этой цепочке) пакет, и только потом рантайм пойдет вверх по дереву к пакету main.
Отсюда стоит упомянуть важный вариант импорта - Side-Effect Import и тему Blank identifier.
Blank identifier
package main import ( "fmt" // Импортируем пакет только ради функции init() _ "internal/plugins/crypto" // нижнее подчеркивание = "blank identifier" ) func main() { fmt.Println("Основной цикл программы запущен") }
Здесь нас встречает новый инструмент - _ - это нижнее подчеркивание называется «blank identifier» и имеет много сценариев применения в Go. Сама документация трактует его как некий «плейсхолдер», сравнивания с "/dev/null".
Конструкция import _ *package_name* позволяет нам вызвать функцию init() пакета без создания локальной переменной-ссылки на него. Но зачем нам такой импорт если мы можем импортировать обычным способом, зная что линкер почистит неиспользуемый код?
Тут важно помнить о строгости компилятора: импортируя пакет мы обязаны его использовать:
package main import ( "math/rand" ) func main() { return } // Результат: "math/rand" imported and not used
Компилятор видит неиспользуемый импорт и аварийно останавливает сборку, даже не доходя до линкера.
Это значит, что side effect import - это «разрешение» нам не использовать импортируемый пакет:
package main import ( _ "math/rand" ) func main() { return }
Но, официальная документация предлагает нам еще один вариант: полноценный импорт, но с игнорированием ошибки.
Это достигается опять же путем использования blank identifier, но уже в блоке глобальных объявлений.
package main import ( "fmt" "io" "log" "os" ) var _ = fmt.Printf // Для отладки; var _ io.Reader // Для отладки; func main() { }
Согласно документации, данный пример рекомендуется использовать только в процессе разработки.
Архитектурные нюансы импортов
Кратко пройдемся по некоторым важным знаниям по импортам, которые не говорятся сразу в документации и интерактивном курсе, но очень важны для раскрытия в главе "Структура проекта".
Циклические зависимости
В отличие от многих других ЯП, в Go строго запрещены циклические зависимости (пакет А не может импортировать пакет B, если пакет B импортирует пакет А).
Это объясняется все тем же суждением, что пакет - это неделимая единица компиляции, если компилятор в процессе движения по дереву зависимостей обнаружит цикл, он не поймет какой пакет собрать первым.
Если допустить такую ситуацию, компилятор даже не начнет сборку. Он выдаст ошибку: import cycle not allowed.
Решается данная проблема добавлением промежуточного пакета C.
Package aliasing
Раз уж компилятор Go требует абсолютной однозначности, мы неизбежно столкнемся с конфликтом имен, если попытаемся импортировать два пакета с одинаковым финальным названием.
package main import ( "html/template" "text/template" ) func main() { // ... }
Компилятор моментально остановит сборку и выдаст ошибку text/template redeclared as imported package name
Всё логично: в рамках одного файла имя template уже занято первым импортом. Чтобы разрулить этот конфликт, в Go как раз и существует механизм package aliasing. Мы обязаны явно указать компилятору, под какими именами эти пакеты будут жить в нашем файле:
package main import ( htmlTemplate "html/template" textTemplate "text/template" ) func main() { htmlTemplate.New("web") textTemplate.New("log") }
Dot import
Данный механизм позволяет все экспортируемые функции, типы и переменные импортируемого пакета поместить в область видимости нашего файла так, будто они объявлены прямо здесь.
Для этого достаточно поставить "." перед путем к пакету.
package main import . "fmt" func main() { Println("Куда делся префикс пакета?") // Вызываем функцию напрямую }
Сообщество разработчиков не рекомендует использовать данный вариант импорта ради сохранения читаемости кода.
Видимость элементов
Мы уже не один раз вызывали методы импортируемых пакетов, но так и не разобрали тему видимости данных элементов.
В Go, в отличие от стандартных ЯП с ООП, нет ключевых слов для определения модификатора доступа.
Всё решает регистр первой буквы идентификатора (будь то функция, структура, переменная или кастомный тип):
Имя начинается с заглавной (Большой) буквы — сущность является экспортируемой (exported). Это аналог
public. Её увидят и смогут вызвать любые другие пакеты, которые импортируют пакет. Например:fmt.Println(),rand.Intn().Имя начинается со строчной (Маленькой) буквы — сущность является неэкспортируемой (unexported). Это аналог
private. Она видна абсолютно всем файлам внутри её родного пакета, но наглухо скрыта от любого внешнего мира.
Инкапсуляция на уровне папок: Пакет internal
Еще один механизм определения видимости элементов - создание пакета internal.
Представим ситуацию: ты пишешь большую библиотеку, у тебя там 10 пакетов, и ты хочешь, чтобы они видели код друг друга, но внешний мир (те, кто скачает твою библиотеку с GitHub) этот код вызвать не мог?
Для этого авторы Go встроили в компилятор поддержку специального имени папки — internal.
Если ты создаешь пакет по пути myproject/internal/auth, то компилятор разрешит импортировать этот пакет только тем пакетам, которые находятся внутри дерева myproject. Если чужой проект попытается написать import "github.com/user/myproject/internal/auth", компилятор выдаст ошибку.
Стоп! А как у нас в пути к пакету оказался адрес GitHub?
Тут мы наконец перейдем к главному герою главы про структуру проекта на Go - модули.
Модули
Об относительных импортах
В основном блоке статьи, где мы говорили про импорты, я упустил еще один вариант - относительный импорт.
import "./relative/path"
Данный способ невозможно использовать в режиме модуля, о чем нам сообщает компилятор:
"./relative/path" is relative, but relative import paths are not supported in module mode
Оставим данный вариант импорта просто для некого "знания", что такой способ когда-то использовался, сейчас, вероятно, из-за того, что большинство проектов пишутся в модульном варианте такой способ не используется.
Но что это за такой режим, режим модуля?
Что такое модуль?
Модуль — это набор пакетов Go, хранящихся в виде дерева файлов, в корне которого лежит файл go.mod. Этот файл определяет путь модуля (module path) (который также является префиксом для импорта корневого каталога) и требования к зависимостям — то есть другие модули, необходимые для успешной сборки. Каждое требование записывается в виде пути к модулю и его конкретной семантической версии (подробнее о версионировании модулей, подробнее о семантическом версионировании).
Для нас это означает появление новой вложенности, стоящей перед пакетом. Теперь мы понимаем почему в примерах кода на Go локальные пакеты чаще всего импортируются не относительным импортом и не как стандартные пакеты, а с припиской какого-то Github-репозитория (или любого иного варианта именования проекта).
Для создания модуля достаточно написать go mod init github.com/example/repo
(Принято указывать именно путь к вашему будущему репозиторию на GitHub. Это гарантирует, что имя вашего модуля будет уникальным, и никто другой не столкнется с конфликтом имён).
После выполнения данной команды и появляется долгожданный файл go.mod:
module github.com/example/repo go 1.25.3
Таким образом, импорты имеют такой вид:
project/ ├── main.go ├── packageA/ │ └── main.go └── packageB/ └── subpackage/ └── main.go
package main import ( "github.com/example/repo/packageA" "github.com/example/repo/packageB/subpackage" ) func main() { ... }
В современном Go режим модуля стоит по умолчанию и уже почти каждый проект использует эту концепцию.
Стоит сказать, что модули пришли в язык не сразу, это своего рода аналог Python virtualenv, node_modules в NodeJS (скорее pnpm). До модулей было просто большое глобальное пространство (GOPATH), где все проекты и их зависимости хранились в одной куче в локальной памяти.
С приходом Go Modules система стала похожа на то, к чему мы привыкли в других языках: проект изолирован, а в его корне лежит манифест.
Вот мы и закончили первую часть. Для меня написание подобных статей - это совершенно новый, неизведанный опыт. Понятно, что публикацией таких текстов я не открою Америку - на просторах интернета полно разборов и документации. Но у каждого автора своя система, свой бэкграунд и свой порядок повествования. У меня нет грандиозной цели кого-то поучать, скорее это подробный разбор для таких же людей, которые хотят осознанно разобраться в экосистеме Go. Спасибо, что дочитали, и до встречи во второй части!
Комментарии (8)

Octagon77
09.07.2026 10:18go 1.25.3Сам изучал да писал или несвежий текст скопипастил?
├── main.go <-- Go файл корневого пакета├── fmt/ <-- Пакет fmt│ └── printer.go <-- Go файл пакета fmtСтандартную библиотеку переписывать кто подучил? Какой такой гипотетический бред? Сами хотите запутаться - отлично, но держите такое не на виду.
Оставим данный вариант импорта просто для некого "знания", что такой способ когда-то использовался, сейчас, вероятно, из-за того, что большинство проектов пишутся в модульном варианте такой способ не используется.
ЧТО ЗНАЧИТ ВЕРОЯТНО?! Какой такой вариант импорта? Импорт в Go всегда один и тот же, а вот режимов поиска два - режим модуля и режим GOPATH. И оба, как это ни сложно себе представить после долгтх лет копипасты на Python и JavaScript, используются оба.
Поэтому я решил устроить эксперимент и разбирать Go прямо в публичном поле, шаг за шагом документируя весь процесс.
Идея прекрасная, а процесс дурацкий.
Я возьму за основу официальный трек обучения от создателей языка и постараюсь подробно разобрать как Go устроен.
Наверно дело как раз в этом. То, что Вы приняли за трек обучения, это никакой не трек, никакого обучения от него не будет, это tour, то есть экскурсионная развлекательная поездка. Её задача - дать тому, кому Go не зайдёт, возможность почувствовать это как можно раньше. А тому, кому зайдёт, возможность в этом убедиться тоже как можно раньше, что-то в процессе написав, глупо или нет - пока не важно.
Переписывать такое своими словами - цензурных слов у меня нет. Но напомню, я писал - идея прекрасная. Потому, что на сайте Go недавно произошла катастрфа, последовательная документация уровня одновременно доступного и достаточного для изучения - исчезла, вместо неё трудно читаемая reference и другие, в смысле кроме tour, весёлые истории.
Я могу понять это двумя способами. Первый, от разглядывания страницы https://go.dev/learn/ - это чтобы лучше продовались курсы и книжки, нет причин выкладывать бесплатно то, что есть надежда продать. Второй, от желания оправдать дьявола - это потому, что набор рассказов по поводу достаточен для освоения чтения reference, которому всякому не мечтающему о статусе вечного джуна, особенно внезапно не вечного в силу ИИ, всё равно придётся придаться.
Разбираться с Go нужно с ввода
go help go help environment go help run go help packages go help modules go help modules-auth go help buildПосле чего можно или продолжить изучение, или написать о том, о чём в статье и написано, только совсем иначе. Без такого вот
Значит, можно сказать, что пакет - это атомарная, неделимая единица компиляции. Именно то, что компилируются вместе в единый неделимый блок,
ибо так сказать можно только с целью опозориться, ибо неделимая единица компиляции в Go, кто бы мог подумать, есть файл.
А лучше - начинать не с того, какая там "структура проекта", а как выполнить код на Go. Когда он в одном файле. Или в трёх. А случай когда в тысяче, вместе со структурой проекта, отложить на потом. Ну, разве что упомянуть почему зависимости структуре проекта ортогональны.

argrigo Автор
09.07.2026 10:18Здравствуйте, спасибо за ваш комментарий!
Сам изучал да писал или несвежий текст скопипастил?
Изучал самостоятельно. Пример
go.modвзят с моего локального проекта, а прописанная в нем версияgoникак не говорит о копипасте.Стандартную библиотеку переписывать кто подучил? Какой такой гипотетический бред? Сами хотите запутаться - отлично, но держите такое не на виду.
Как я прямо указал в тексте, переписыванием стандартных библиотек никто не занимался. Цель этой схемы - наглядно показать как устроена физическая вложенность папок и пути импортов на диске. Если она показалась Вам запутанной, это ваше право, но техническую суть работы путей компилятора пример передает точно.
ЧТО ЗНАЧИТ ВЕРОЯТНО?! Какой такой вариант импорта? Импорт в Go всегда один и тот же, а вот режимов поиска два - режим модуля и режим GOPATH. И оба, как это ни сложно себе представить после долгтх лет копипасты на Python и JavaScript, используются оба.
"Вероятно" - это значит, что довод может не являться истиной, а служит лишь выводом, к которому пришел автор в конкретном блоке статьи.
"Вариант импорта" - значит вариант написания импорта. Про режим поиска в статье не было упомянуто, спасибо за дополнение.Идея прекрасная, а процесс дурацкий.
Далее вы поднимаете тему о "неверности" выбора пути, по которому я пошел. Начиная от выбора источника направления изучения, заканчивая структурой и примерами.
В самом начале статьи я четко обозначил формат: это мой интерактивный личный учебник, а не академическое пособие. Здесь я описываю то, с чем мне было интересно познакомиться в процессе изучения, особенности языка и то, в каком порядке я бы их поставил. Заметьте, везде я использую слово "Я" и не говорю, что такая структура и такой подход - единственно правильные.
Этим я хочу сказать, что "истинного" подхода к изучению нет, особенно в формате таких статей. Это как давний спор, с какого языка программирования "правильнее" начинать.
То, что Вы приняли за трек обучения, это никакой не трек, никакого обучения от него не будет, это tour, то есть экскурсионная развлекательная поездка. Её задача - дать тому, кому Go не зайдёт, возможность почувствовать это как можно раньше
Я посчитал
tourструктурированным и простым подходом к изучению. Да, действительно, он не подходит для глубокого изучения, но взять его как основу никто не запрещает, тем более я не останавливался на поверхностном изучении глав тура.ибо неделимая единица компиляции в Go, кто бы мог подумать, есть файл.
В языке C/C++ single compilation unit - это, действительно, файл.
Но в Go - это пакет согласно официальной документации компилятора и обсуждениям.
Если у вас есть иные архитектурные источники, утверждающие обратное, буду рад с ними ознакомиться.
Octagon77
09.07.2026 10:18Но в Go - это пакет согласно официальной документации компилятора и обсуждениям.
В этом месте я внезапно понял где же имнно мы фундаментально расходимся. Для Вас обсуждения - не пустой звук, как для меня, как альтернатива - бред, я привык к этому слову, хотя сейчас можно сказать красивее - галлюцинации. Для Вас документация в виде текста - Истина, для меня - третья, в лучшем случае - вторая, в исключительных случаях типа Rust - первая, но тень Истины. А Истина - это то, что выводится в терминал. Вам нужны источники - мне они интересны, иногда полезны, но никогда не нужны.
Go run делает исполняемый файл из одного файла? Делает. На этом, вообще-то, дискуссия может и закончиться. А может и нет, если недоумевающий заметит - но ведь в этом файле явно написано package main, это же пакет. Так вот, этот main, сколько ни пиши перед ним package, это не пакет, это команда. Об этом даже написано, если правильно помню, в go help, подкоманду не помню. Был бы пакет - его бы импортировали в другие пакеты...
В документации, например, написано, что файлы для go run должны быть в одной папке. А по жизни вовсе и нет, ибо go run следует символическим ссылкам. На практике, помнится, go run со ссылками идеально подходит для обучения, осбенно пока часты "сначала попробую так, а потом эдак и ещё через задницу непременно", а документация тупо портит experience путаясь под ногами.
Этим я хочу сказать, что "истинного" подхода к изучению нет, особенно в формате таких статей. Это как давний спор, с какого языка программирования "правильнее" начинать.
А вот это фундаментально. Да, люди отличаются целями и способностями, единого истинного (почему в кавычках, чего тут стесняться?) подхода нет. Но стоит отсечь по способностям на достаточно высоком уровне и хотя бы примерно определиться с целями, как истинный подход немедленно появляется, просто потому, что разумных подходов мало.
Абсолютно аналогично, тут Вы правы, и с языком с которого правильнее начинать. В отличии от подходов к обучению, языков много, что позволяет заметить, что отсутствие самого правильного, в том числе в силу размытия критериев, не исключает наличия отношений "всегда лучше" и "заведомо хуже", пусть и не между любой парой языков.
В самом начале статьи я четко обозначил формат: это мой интерактивный личный учебник, а не академическое пособие.
На 100% личного в публичном поле не бывает и никакая "личностность" право творить что угодно не даёт. Ещё в далёком прошлом врачи это сформулировали как принцип "не навреди".

olivera507224
09.07.2026 10:18В самих Go-файлах при объявлении пакета нужно писать только название текущего каталога.
Таки нет, это не так. В общем случае пакет будет иметь то же имя, что и каталог, в котором он расположен. Но можно назвать и иначе. Например, пакет, расположенный в директории cmd/app с вероятностью 0.999 будет иметь имя main.

foggy-f
09.07.2026 10:18Я , кстати, когда начинал, то именовать каталог по пакету приходилось в ручную. А сейчас заметил толи тулзы гошные, толи IDE при переименовании пакета каталог переименуют тоже.
НО так то да: имя пакета не обязано совпадать с именем папки.

argrigo Автор
09.07.2026 10:18Да, вы абсолютно правы, спасибо за это важное уточнение! Действительно, жесткой связи на уровне компилятора нет, и имя пакета определяется строго инструкцией
package.
foggy-f
https://habr.com/ru/articles/771858/?ysclid=mrdcn1ly1q112340934
Init() не приветсвуется
argrigo Автор
Спасибо, добавил цитату в блок про init().