Skip to content

Latest commit

 

History

History
875 lines (625 loc) · 26.3 KB

File metadata and controls

875 lines (625 loc) · 26.3 KB
title Структура программы
description Основные элементы Go программы
tableOfContents
minHeadingLevel maxHeadingLevel
2
3
author godojo
authorName Godojo Master
updatedAt 2025-12-14
readingTime 13

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

Сравнение:

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

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

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


Источники


← Предыдущий Hello World Следующий → Компиляция и запуск