| title | Структура программы | ||||
|---|---|---|---|---|---|
| description | Основные элементы Go программы | ||||
| tableOfContents |
|
||||
| author | godojo | ||||
| authorName | Godojo Master | ||||
| updatedAt | 2025-12-14 | ||||
| readingTime | 13 |
Go — язык со строгими правилами. Код либо написан правильно и работает, либо даже не запустится. Поначалу это напрягает, но потом понимаешь: меньше думаешь о мелочах, больше о том, что программа должна делать.
Давайте разберём, из каких "деталей" состоит любая Go-программа и почему они должны стоять именно в таком порядке.
Начнёте с import — компилятор выдаст: expected 'package', found 'import'.
package mainПакет — это просто способ сгруппировать код. Пока что не заморачивайтесь — просто пишите package main в начале файла. Что такое пакеты и зачем они нужны, разберём позже, когда проект станет больше одного файла.
В мире Go есть VIP-пакет — package main. Это как главный вход в здание:
package main // "Я — исполняемая программа!"
package utils // "Я — библиотека, используй меня"Если вы напишете package main и добавите функцию main(), Go создаст исполняемый файл. Любое другое имя пакета — и вы получите библиотеку, которую нельзя запустить напрямую.
Реальный случай из практики: Когда сам начинал, убил полчаса на ошибку "cannot run non-main package". Скопировал код из чужого проекта, там было package handlers. Переименовал в package main — заработало. Тупо, но бывает.
Go любит минимализм. Имена пакетов должны быть:
- Строчными — никаких
Package MainилиMAIN - Односложными —
http,json,time, а неhttpHelpers - Без подчёркиваний —
mypackage, а неmy_package
// Хорошо 👍
package user
package auth
package store
// Плохо 👎
package userHelpers // слишком длинно
package user_service // подчёркивание
package Utilities // заглавная буква
package common // что внутри? всё подряд?:::tip Лайфхак Если не можете придумать короткое имя — возможно, ваш пакет делает слишком много. Разбейте его. :::
Когда кто-то импортирует ваш пакет, он будет писать имяпакета.Функция(). Подумайте об этом:
// Пакет называется "http"
http.Get("https://...") // Читается хорошо
http.HTTPGet("https://...") // HTTPGet? Серьёзно?
// Пакет называется "strings"
strings.ToUpper("hello") // Окей
strings.StringToUpper("hello") // Масло масляноеПосле объявления пакета идут импорты. Это как список гостей на вечеринке — только те, кого вы явно пригласили, смогут войти.
// Один гость
import "fmt"
// Несколько гостей (так принято в Go)
import (
"fmt"
"os"
"strings"
)Группировка в скобках — не просто красиво, это идиоматический Go. Один импорт на строку допустим, но коллеги будут коситься.
import (
// Стандартная библиотека — местные жители
"fmt"
"os"
"strings"
// Пустая строка — разделитель
// Сторонние пакеты — гости из других городов
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
)Это не просто конвенция — инструмент goimports автоматически сортирует импорты именно так. Настройте его в редакторе, и забудьте об этом навсегда.
import "fmt"
fmt.Println("Привет!") // Используем с префиксомimport (
"crypto/rand" // Криптографический рандом
mrand "math/rand" // Математический рандом
)
// Теперь можно использовать оба
cryptoBytes := make([]byte, 32)
rand.Read(cryptoBytes) // crypto/rand
number := mrand.Intn(100) // math/randРеальный кейс: В одном проекте было три пакета config — свой, из фреймворка и из библиотеки логирования. Без алиасов — никак:
import (
appconfig "myapp/config"
ginconfig "github.com/gin-gonic/gin/config"
logconfig "go.uber.org/zap/config"
)Иногда пакет нужен не ради функций, а ради того, что он делает при загрузке:
import (
"database/sql"
_ "github.com/lib/pq" // Регистрирует PostgreSQL драйвер
)
// Теперь sql.Open("postgres", ...) работает
// Хотя мы напрямую pq не вызываемНижнее подчёркивание говорит: "Да, я знаю, что не использую этот пакет напрямую. Так задумано."
Где встречается:
- Драйверы баз данных (
pq,mysql,sqlite3) - Форматы изображений (
image/png,image/jpeg) - Профилирование (
net/http/pprof)
import . "fmt"
Println("Без префикса!") // Работает, но...Выглядит удобно, пока не откроете файл через полгода: "Откуда взялась функция Println? Это наша? Импортированная? Встроенная?"
:::danger Просто не надо Единственное легитимное применение — тесты, когда тестируемый пакет нельзя импортировать напрямую из-за циклических зависимостей. И даже тогда подумайте дважды. :::
import (
yaml "gopkg.in/yaml.v3" // Длинный путь, короткое имя
)
yaml.Unmarshal(data, &config)import "fmt" // Импортировали
func main() {
println("Использую встроенный println") // fmt не нужен
}imported and not used: "fmt"
Go не компилирует код с мусором. Это раздражает первые пять минут, а потом вы понимаете: в проекте никогда не будет 50 неиспользуемых импортов, которые замедляют компиляцию.
Временное решение при отладке:
import "fmt"
var _ = fmt.Println // Заглушка — удалить перед коммитом!Или просто используйте goimports — он сам удалит лишнее.
Каждая исполняемая программа на Go начинается с функции main в пакете main. Это как public static void main в Java, только без боли.
package main
func main() {
// Вселенная вашей программы начинается здесь
}В C вы пишете int main(int argc, char *argv[]). В Go — просто func main().
Почему? Потому что Go любит явность. Если вам нужны аргументы командной строки — импортируйте os и возьмите их сами:
package main
import (
"fmt"
"os"
)
func main() {
// os.Args — срез строк
// [0] — путь к программе
// [1:] — ваши аргументы
fmt.Println("Программа:", os.Args[0])
fmt.Println("Аргументы:", os.Args[1:])
}$ go run main.go привет мир 123
Программа: /tmp/go-build123/main
Аргументы: [привет мир 123]Типичная ошибка новичка:
name := os.Args[1] // Паника, если аргументов нет!Всегда проверяйте длину:
if len(os.Args) < 2 {
fmt.Println("Использование: программа <имя>")
os.Exit(1)
}
name := os.Args[1]main() ничего не возвращает. Для кодов завершения используйте os.Exit():
func main() {
if err := doSomething(); err != nil {
fmt.Fprintln(os.Stderr, "Ошибка:", err)
os.Exit(1) // Выход с кодом ошибки
}
// os.Exit(0) не нужен — успешное завершение по умолчанию
}:::danger Ловушка с defer
os.Exit() завершает программу немедленно. Отложенные функции не выполняются!
:::
func main() {
defer fmt.Println("Это никогда не напечатается!")
os.Exit(1)
}Паттерн для реальных проектов:
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
// Вся логика здесь
// defer работает нормально
// Можно тестировать отдельно
defer cleanup()
if err := initialize(); err != nil {
return fmt.Errorf("init failed: %w", err)
}
return nil
}Этот паттерн используют в продакшене — он позволяет тестировать run() отдельно и гарантирует выполнение defer.
func Main() {} // Это НЕ точка входа
func MAIN() {} // И это тоже
func main() {} // Только такGo регистрозависим. Main и main — разные идентификаторы.
В Go есть две функции для вывода текста, и новички часто путаются.
func main() {
println("Привет!") // Работает без импорта
}Удобно для быстрой отладки, но:
- Пишет в stderr, не в stdout
- Формат вывода не гарантирован — может измениться
- Официально: "может быть удалена в будущих версиях"
import "fmt"
func main() {
fmt.Println("Привет!") // stdout, стабильный формат
}Сравнение:
println |
fmt.Println |
|
|---|---|---|
| Импорт | Не нужен | import "fmt" |
| Вывод | stderr | stdout |
| Формат | Зависит от версии Go | Документирован, стабилен |
| Возврат | Ничего | (n int, err error) |
| Для продакшена | ❌ | ✅ |
Реальная история: Сервис писал логи через println. Всё работало локально. На проде логи шли в stderr, который никто не собирал. Дебажили неделю.
println — для "сейчас быстро гляну и удалю". Как console.log в JavaScript, который вы забываете убрать. Только Go заставит вас убрать неиспользуемый import "fmt", а println — нет. Опасная штука.
Для всего остального — fmt.Println и его друзья (Printf, Sprintf, Fprintf).
Go поддерживает два вида комментариев:
// Однострочный — используется чаще всего
/*
Многострочный — для больших блоков
или временного отключения кода
*/Комментарий прямо перед объявлением — это документация:
// User представляет пользователя системы.
// Нулевое значение не готово к использованию — вызовите NewUser.
type User struct {
ID int
Name string
}
// NewUser создаёт пользователя с указанным именем.
// Возвращает ошибку, если имя пустое.
func NewUser(name string) (*User, error) {
if name == "" {
return nil, errors.New("имя не может быть пустым")
}
return &User{Name: name}, nil
}Эти комментарии:
- Видны в
go doc - Отображаются на pkg.go.dev
- Подсвечиваются в IDE
Правила хорошего тона:
-
Начинайте с имени того, что документируете:
// NewUser создаёт... ✅ // Эта функция создаёт... ❌
-
Пишите полными предложениями с точкой
-
Для пакетов — первая строка особенно важна:
// Package auth предоставляет аутентификацию через JWT. package auth
В Go нет войн из-за табов vs пробелов. Есть gofmt — и точка.
gofmt -w main.go # Форматирует и перезаписывает
go fmt ./... # Форматирует весь проект- Табы для отступов (не пробелы!)
- Выравнивание операторов и комментариев
- Скобки в правильных местах
- Пробелы где надо, и никаких лишних
Go автоматически вставляет точки с запятой в конце строк. Поэтому этот код сломан:
// Go видит: if x > 0;
if x > 0
{ // Это уже новый statement!
doSomething()
}А этот — работает:
if x > 0 {
doSomething()
}Не пытайтесь спорить с этим. Просто примите как данность, настройте автоформатирование в редакторе и забудьте.
go install golang.org/x/tools/cmd/goimports@latest
goimports -w main.goДелает всё то же, что gofmt, плюс:
- Добавляет недостающие импорты
- Удаляет неиспользуемые
- Сортирует по группам
Настройте редактор на автозапуск goimports при сохранении. VS Code с расширением Go делает это из коробки. После этого вы просто пишете fmt.Println, сохраняете, и import "fmt" появляется сам.
Go компилятор — не нянька. Он не будет показывать "warnings" и надеяться, что вы их почините. Он просто не скомпилирует.
import "fmt"
import "os" // Не используем
func main() {
fmt.Println("Привет")
}imported and not used: "os"
func main() {
x := 5 // Объявили
y := 10 // И это тоже
fmt.Println(x) // Используем только x
}y declared and not used
Был у меня коллега, который работал на Python-проекте с 2000+ неиспользуемых импортов (да, они считали). Время запуска тестов — 40 секунд только на импорты. В Go это физически невозможно.
Иногда вам правда нужно проигнорировать значение:
// Нужен только второй результат
_, err := strconv.Atoi("123")
// Итерация только по значениям
for _, value := range myMap {
fmt.Println(value)
}За годы код-ревью я собрал коллекцию:
package main
func Main() { // С большой буквы!
fmt.Println("Привет")
}Main ≠ main. Go регистрозависим.
package utils // Не main!
func DoSomething() {}Только package main создаёт исполняемый файл.
func main() {
fmt.Println(os.Args[1]) // Паника если нет аргументов
}Всегда проверяйте len(os.Args).
func main() {
defer fmt.Println("Конец")
os.Exit(1) // defer игнорируется!
}os.Exit обходит все defer. Используйте паттерн с run().
myproject/
├── main.go // package main
└── utils.go // package utils ← ОШИБКА
Все файлы в одной директории должны иметь одинаковый package.
// Package main — точка входа в приложение greeter.
package main
import (
"fmt"
"os"
"strings"
)
// defaultName используется, когда имя не передано.
const defaultName = "Мир"
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "Ошибка:", err)
os.Exit(1)
}
}
// run содержит основную логику программы.
// Возвращает ошибку, если что-то пошло не так.
func run() error {
name := defaultName
if len(os.Args) > 1 {
name = strings.Join(os.Args[1:], " ")
}
greeting := fmt.Sprintf("Привет, %s!", name)
fmt.Println(greeting)
return nil
}$ go run main.go
Привет, Мир!
$ go run main.go Вася
Привет, Вася!
$ go run main.go дорогой друг
Привет, дорогой друг!| Элемент | Что помнить |
|---|---|
package |
Первая строка, main = исполняемый файл |
import |
После package, группируйте в скобках |
func main() |
Без аргументов, без возврата, только в package main |
os.Args |
Аргументы CLI, проверяйте длину! |
os.Exit(n) |
Для кода завершения, но defer не выполнится |
fmt.Println |
Для продакшена |
println |
Только для отладки |
gofmt |
Один стиль, настройте автоформатирование |
Что выведет эта программа?
package main
import "fmt"
func main() {
fmt.Print("Го")
fmt.Print("лан")
fmt.Println("г")
fmt.Println("!")
}Решение
Голанг
!
Print не добавляет перенос строки, Println — добавляет.
import "fmt"
package main
func Main() {
x := "Готово"
fmt.Println("Привет")
}Решение
package mainдолжен быть первымfunc Main()→func main()- Переменная
xобъявлена, но не используется - (Бонус) Нет пустой строки между package и import — не ошибка, но gofmt поправит
Исправленный код:
package main
import "fmt"
func main() {
x := "Готово"
fmt.Println(x)
}Напишите программу, которая принимает два числа как аргументы и выводит их сумму.
$ go run main.go 5 3
8
$ go run main.go
Использование: calc <число1> <число2>Подсказка
Вам понадобится strconv.Atoi() для конвертации строки в число.
Решение
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
if len(os.Args) != 3 {
fmt.Println("Использование: calc <число1> <число2>")
os.Exit(1)
}
a, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Println("Первый аргумент не число:", os.Args[1])
os.Exit(1)
}
b, err := strconv.Atoi(os.Args[2])
if err != nil {
fmt.Println("Второй аргумент не число:", os.Args[2])
os.Exit(1)
}
fmt.Println(a + b)
}Напишите программу, которая выводит аргументы в обратном порядке.
$ go run main.go раз два три
три
два
разРешение
package main
import (
"fmt"
"os"
)
func main() {
args := os.Args[1:] // Без имени программы
// Идём с конца к началу
for i := len(args) - 1; i >= 0; i-- {
fmt.Println(args[i])
}
}Теперь вы знаете, из чего состоит Go-программа. В следующем уроке разберём компиляцию и запуск — как превратить код в исполняемый файл и что происходит под капотом.
- The Go Programming Language Specification — официальная спецификация языка
- Effective Go — рекомендации по написанию идиоматичного кода
- How to Write Go Code — структура проектов и модули
- Go Doc Comments — правила документирования кода
- go fmt your code — статья о форматировании
- Package fmt — документация пакета fmt
- Package builtin — встроенные функции
← Предыдущий Hello World Следующий → Компиляция и запуск