Перейти к содержимому

Поиск доступен только в продакшен-сборках. Выполните сборку и запустите превью, чтобы протестировать поиск локально.

Структура программы

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 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: приглашаем гостей на вечеринку

После объявления пакета идут импорты. Это как список гостей на вечеринке — только те, кого вы явно пригласили, смогут войти.

Базовый синтаксис

// Один гость
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, стабильный формат
}

Сравнение:

printlnfmt.Println
ИмпортНе нуженimport "fmt"
Выводstderrstdout
ФорматЗависит от версии 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

Правила хорошего тона:

  1. Начинайте с имени того, что документируете:

    // NewUser создаёт... ✅
    // Эта функция создаёт... ❌
  2. Пишите полными предложениями с точкой

  3. Для пакетов — первая строка особенно важна:

    // 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@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 это физически невозможно.

Blank identifier для намеренного игнорирования

Иногда вам правда нужно проигнорировать значение:

// Нужен только второй результат
_, err := strconv.Atoi("123")
// Итерация только по значениям
for _, value := range myMap {
fmt.Println(value)
}

Типичные грабли новичков

За годы код-ревью я собрал коллекцию:

1. “Почему main не запускается?”

package main
func Main() { // С большой буквы!
fmt.Println("Привет")
}

Mainmain. 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("Привет")
}
Решение
  1. package main должен быть первым
  2. func Main()func main()
  3. Переменная x объявлена, но не используется
  4. (Бонус) Нет пустой строки между package и import — не ошибка, но gofmt поправит

Исправленный код:

package main
import "fmt"
func main() {
x := "Готово"
fmt.Println(x)
}

Задача 3: CLI калькулятор ⭐⭐⭐

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

Окно терминала
$ 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)
}

Задача 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-программа. В следующем уроке разберём компиляцию и запуск — как превратить код в исполняемый файл и что происходит под капотом.


Источники