Структура программы
Go — язык со строгими правилами. Код либо написан правильно и работает, либо даже не запустится. Поначалу это напрягает, но потом понимаешь: меньше думаешь о мелочах, больше о том, что программа должна делать.
Давайте разберём, из каких “деталей” состоит любая Go-программа и почему они должны стоять именно в таком порядке.
Package: ваш код живёт не в вакууме
Начнёте с import — компилятор выдаст: expected ‘package’, found ‘import’.
package mainЧто такое пакет?
Пакет — это просто способ сгруппировать код. Пока что не заморачивайтесь — просто пишите package main в начале файла. Что такое пакеты и зачем они нужны, разберём позже, когда проект станет больше одного файла.
Почему 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 userpackage authpackage 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: приглашаем гостей на вечеринку
После объявления пакета идут импорты. Это как список гостей на вечеринке — только те, кого вы явно пригласили, смогут войти.
Базовый синтаксис
// Один гостьimport "fmt"
// Несколько гостей (так принято в Go)import ( "fmt" "os" "strings")Группировка в скобках — не просто красиво, это идиоматический Go. Один импорт на строку допустим, но коллеги будут коситься.
Анатомия импорта
import ( // Стандартная библиотека — местные жители "fmt" "os" "strings"
// Пустая строка — разделитель
// Сторонние пакеты — гости из других городов "github.com/gin-gonic/gin" "github.com/jmoiron/sqlx")Это не просто конвенция — инструмент goimports автоматически сортирует импорты именно так. Настройте его в редакторе, и забудьте об этом навсегда.
Пять видов импорта (от нормального до странного)
1. Обычный импорт — ваш ежедневный хлеб
import "fmt"
fmt.Println("Привет!") // Используем с префиксом2. Импорт с псевдонимом — когда имена конфликтуют
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")3. Blank import — приглашаем ради побочных эффектов
Иногда пакет нужен не ради функций, а ради того, что он делает при загрузке:
import ( "database/sql" _ "github.com/lib/pq" // Регистрирует PostgreSQL драйвер)
// Теперь sql.Open("postgres", ...) работает// Хотя мы напрямую pq не вызываемНижнее подчёркивание говорит: “Да, я знаю, что не использую этот пакет напрямую. Так задумано.”
Где встречается:
- Драйверы баз данных (
pq,mysql,sqlite3) - Форматы изображений (
image/png,image/jpeg) - Профилирование (
net/http/pprof)
4. Dot import — не делайте так
import . "fmt"
Println("Без префикса!") // Работает, но...Выглядит удобно, пока не откроете файл через полгода: “Откуда взялась функция Println? Это наша? Импортированная? Встроенная?”
:::danger Просто не надо Единственное легитимное применение — тесты, когда тестируемый пакет нельзя импортировать напрямую из-за циклических зависимостей. И даже тогда подумайте дважды. :::
5. Именованный импорт пакета — для особых случаев
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 — он сам удалит лишнее.
func main(): здесь всё начинается
Каждая исполняемая программа на 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 — разные идентификаторы.
fmt.Println vs println: битва титанов
В Go есть две функции для вывода текста, и новички часто путаются.
println — встроенная функция-призрак
func main() { println("Привет!") // Работает без импорта}Удобно для быстрой отладки, но:
- Пишет в stderr, не в stdout
- Формат вывода не гарантирован — может измениться
- Официально: “может быть удалена в будущих версиях”
fmt.Println — взрослый выбор
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 поддерживает два вида комментариев:
// Однострочный — используется чаще всего
/* Многострочный — для больших блоков или временного отключения кода*/Doc-комментарии: ваш код документирует сам себя
Комментарий прямо перед объявлением — это документация:
// 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
gofmt: один стиль, чтобы править всеми
В Go нет войн из-за табов vs пробелов. Есть gofmt — и точка.
gofmt -w main.go # Форматирует и перезаписываетgo fmt ./... # Форматирует весь проектЧто делает gofmt?
- Табы для отступов (не пробелы!)
- Выравнивание операторов и комментариев
- Скобки в правильных местах
- Пробелы где надо, и никаких лишних
Почему фигурная скобка на той же строке?
Go автоматически вставляет точки с запятой в конце строк. Поэтому этот код сломан:
// Go видит: if x > 0;if x > 0{ // Это уже новый statement! doSomething()}А этот — работает:
if x > 0 { doSomething()}Не пытайтесь спорить с этим. Просто примите как данность, настройте автоформатирование в редакторе и забудьте.
goimports = gofmt + магия импортов
go install golang.org/x/tools/cmd/goimports@latestgoimports -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 это физически невозможно.
Blank identifier для намеренного игнорирования
Иногда вам правда нужно проигнорировать значение:
// Нужен только второй результат_, err := strconv.Atoi("123")
// Итерация только по значениямfor _, value := range myMap { fmt.Println(value)}Типичные грабли новичков
За годы код-ревью я собрал коллекцию:
1. “Почему main не запускается?”
package main
func Main() { // С большой буквы! fmt.Println("Привет")}Main ≠ main. Go регистрозависим.
2. “Почему go build ничего не создаёт?”
package utils // Не main!
func DoSomething() {}Только package main создаёт исполняемый файл.
3. “Index out of range”
func main() { fmt.Println(os.Args[1]) // Паника если нет аргументов}Всегда проверяйте len(os.Args).
4. “Defer не сработал”
func main() { defer fmt.Println("Конец") os.Exit(1) // defer игнорируется!}os.Exit обходит все defer. Используйте паттерн с run().
5. Файлы в одной папке с разными package
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 | Один стиль, настройте автоформатирование |
Задачи
Задача 1: Разминка ⭐
Что выведет эта программа?
package main
import "fmt"
func main() { fmt.Print("Го") fmt.Print("лан") fmt.Println("г") fmt.Println("!")}Решение
Голанг!Print не добавляет перенос строки, Println — добавляет.
Задача 2: Найди 4 ошибки ⭐⭐
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)}Задача 3: CLI калькулятор ⭐⭐⭐
Напишите программу, которая принимает два числа как аргументы и выводит их сумму.
$ go run main.go 5 38
$ 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)}Задача 4: Реверс аргументов ⭐⭐⭐
Напишите программу, которая выводит аргументы в обратном порядке.
$ 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 — встроенные функции