Go: полный курс — от основ до современного языка · Глава 1 из 12

Глава 1. Введение в Go: установка, первая программа, инструменты

Прогресс сохранится в этом браузере (войдите, чтобы синхронизировать).

Введение

Go (его часто называют golang, чтобы не путать с настольной игрой и удобнее гуглить) — компилируемый язык от Google, который создавался как ответ на боль больших команд: медленная сборка, запутанные зависимости и «слишком умный» синтаксис, в котором легко утонуть. Философия Go — минимализм и предсказуемость. В языке нет наследования классов, нет исключений в привычном смысле, нет перегрузки операторов и всего десяток ключевых слов. Зато есть быстрая компиляция в один статический бинарник, встроенная многозадачность (горутины) и стандартная библиотека, на которой можно написать production-сервис без единой сторонней зависимости.

В этой главе мы установим toolchain, напишем классический «Hello, world», научимся запускать и собирать программы, разберёмся, что такое пакет и модуль, как работают импорты, и заглянем под капот строк — почему в Go текст это UTF-8 и что такое руна. Всё это фундамент: без него дальнейшие главы будут висеть в воздухе.

Зачем вообще нужен Go

Представь, что ты пишешь сетевой сервис. На интерпретируемом языке тебе нужен установленный рантайм на сервере, менеджер пакетов, виртуальное окружение и молитва, что версии совпадут. Go компилирует твой код в один исполняемый файл, который не зависит ни от чего: скопировал бинарник на сервер — и он работает. Это меняет подход к деплою радикально: Docker-образ с Go-приложением может весить несколько мегабайт, потому что внутри лежит ровно один файл.

Вторая причина — конкурентность. Go задумывался в эпоху многоядерных процессоров, и запустить тысячу параллельных задач здесь так же просто, как написать go doWork(). Мы дойдём до этого позже, но важно понимать: язык проектировался вокруг идеи «много дешёвых потоков», а не «один поток и колбэки».

Третья причина — читаемость. В Go намеренно один способ делать большинство вещей. Есть даже официальный форматтер gofmt, который приводит любой код к единому стилю, поэтому споры про табы, пробелы и расстановку скобок в Go-сообществе просто отсутствуют как класс.

Установка и проверка toolchain

Скачай инсталлятор с официального сайта go.dev либо поставь через пакетный менеджер (brew install go на macOS, apt install golang на Debian/Ubuntu). После установки в системе появляется команда go — это единая точка входа во весь инструментарий: компилятор, тесты, форматтер, менеджер зависимостей. Проверить, что всё встало, можно так:

go version

Команда выведет что-то вроде go version go1.22.0 darwin/arm64. Версия важна: некоторые возможности из этого курса (например, for i := range 10) появились только в Go 1.22, поэтому желательно иметь свежий toolchain. Также посмотри окружение:

go env GOPATH GOMODCACHE

Раньше весь код жил в жёстко заданной папке GOPATH, и это было источником постоянной путаницы. С появлением модулей (Go 1.11+) проект можно держать где угодно, а GOPATH теперь используется в основном как кэш скачанных зависимостей. Запомни этот момент: если тебе где-то советуют «положить проект в src внутри GOPATH» — это устаревший совет.

Первая программа

Создай папку под проект и файл main.go в ней. Минимальная программа на Go выглядит так:

package main

import "fmt"

func main() {
    fmt.Println("Привет, Go!")
}

Разберём построчно, потому что каждая строка тут неслучайна. package main объявляет, что этот файл принадлежит пакету main — особому пакету, из которого получается исполняемая программа. import "fmt" подключает пакет форматированного ввода-вывода из стандартной библиотеки. func main() — точка входа: именно эту функцию рантайм вызовет при запуске. fmt.Println печатает строку и переводит курсор на новую строку.

Обрати внимание на пару деталей, которые сразу отличают Go от многих языков. Во-первых, точки с запятой не нужны — компилятор расставляет их сам. Во-вторых, открывающая фигурная скобка ОБЯЗАНА стоять на той же строке, что и func; перенести её на следующую строку нельзя, код не скомпилируется. Это следствие автоматической расстановки точек с запятой и одна из первых граблей у новичков.

go run против go build

Есть два основных способа выполнить программу. Первый — запустить «на лету»:

go run main.go

Эта команда компилирует код во временный бинарник, тут же его запускает и удаляет. Идеально для экспериментов и отладки: ты правишь код и сразу видишь результат, не думая про артефакты сборки.

Второй способ — собрать настоящий исполняемый файл:

go build -o hello main.go
./hello

Здесь go build создаёт файл hello (на Windows — hello.exe), который можно скопировать на другую машину той же архитектуры и запустить без установленного Go. Именно так собирают программы для деплоя. А ещё Go умеет кросс-компиляцию: задав переменные окружения GOOS и GOARCH, ты на macOS соберёшь бинарник под Linux, не выходя из терминала.

GOOS=linux GOARCH=amd64 go build -o hello-linux main.go

Модули и структура проекта

Как только проект перерастает один файл, ему нужен модуль. Модуль — это единица версионирования и зависимостей, описанная файлом go.mod. Инициализируется он одной командой:

go mod init github.com/tim/weather-cli

Аргумент — это путь модуля, обычно совпадающий с адресом репозитория. Он же становится префиксом для импортов внутренних пакетов. После инициализации в корне появится go.mod примерно такого содержания:

module github.com/tim/weather-cli

go 1.22

Когда ты добавишь сторонние зависимости и соберёшь проект, рядом появится go.sum с контрольными суммами — его тоже коммитят в репозиторий, чтобы сборка была воспроизводимой. Скачать и почистить зависимости помогает команда:

go mod tidy

Она проходит по коду, добавляет в go.mod всё, что реально импортируется, и убирает то, что больше не используется. Хорошая привычка — запускать её перед коммитом.

Пакеты и импорты

Программа на Go состоит из пакетов. Пакет — это папка с .go-файлами, объявляющими одно и то же имя пакета в первой строке. Пакет main особенный: он даёт исполняемый файл. Все остальные пакеты — переиспользуемые библиотеки. Разложим утилиту по пакетам:

weather-cli/
├── go.mod
├── main.go
└── stringutil/
    └── reverse.go

Файл stringutil/reverse.go объявляет свой пакет и экспортирует функцию:

// Package stringutil содержит утилиты для работы со строками.
package stringutil

// Reverse возвращает строку, перевёрнутую по рунам.
func Reverse(s string) string {
    r := []rune(s)
    for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
        r[i], r[j] = r[j], r[i]
    }
    return string(r)
}

Ключевой момент видимости: в Go нет ключевых слов public и private. Всё решает регистр первой буквы имени. Идентификатор с ЗАГЛАВНОЙ буквы (Reverse) экспортируется и виден из других пакетов; со строчной (reverse) — приватен для своего пакета. Это простое правило заменяет собой целый уровень модификаторов доступа.

Теперь используем пакет в main.go:

package main

import (
    "fmt"

    "github.com/tim/weather-cli/stringutil"
)

func main() {
    fmt.Println(stringutil.Reverse("!oG ,тевирП"))
}

Импорты в круглых скобках — это блок импорта, удобный, когда пакетов несколько. Обращение к экспортированной функции всегда идёт через имя пакета: stringutil.Reverse. Это делает код читаемым — сразу видно, откуда взялась функция.

Компилятор, который спорит с тобой

Go принципиально строг к двум вещам: неиспользуемым импортам и неиспользуемым локальным переменным. Если ты подключил пакет и не вызвал из него ничего, код НЕ скомпилируется. То же с локальной переменной, которую объявил, но не прочитал. Для многих это шок, но идея здравая: мёртвый код и лишние зависимости — источник багов и раздувания, и лучше отловить их в момент компиляции, чем годами таскать в проекте.

import (
    "fmt"
    "strings" // ошибка компиляции: "strings" imported and not used
)

func main() {
    fmt.Println("hi")
}

Если пакет нужен только ради его побочного эффекта (например, регистрация драйвера БД), его импортируют с пустым идентификатором: import _ "github.com/lib/pq". Про пустой идентификатор подробно поговорим в главе 2.

Инструменты, которые стоит освоить сразу

Toolchain Go — это не только компилятор. Три команды, которые войдут в твою мышечную память:

gofmt -w main.go
go vet ./...
go doc fmt.Println

gofmt -w форматирует файл по каноническому стилю прямо на месте (флаг -w — write). Большинство редакторов делают это автоматически при сохранении. go vet — статический анализатор, который ловит подозрительные конструкции: неправильные форматные строки, недостижимый код, ошибки с блокировками. Запись ./... означает «текущий пакет и все вложенные». go doc показывает документацию прямо в терминале — быстрее, чем лезть в браузер.

Строки, байты и руны

Одна из вещей, которая удивляет новичков: в Go строка это неизменяемая последовательность БАЙТОВ, а исходники по умолчанию в UTF-8. Пока ты работаешь с латиницей, разницы не видно — один символ занимает один байт. Но кириллица, эмодзи и иероглифы кодируются несколькими байтами, и тут важно не запутаться.

package main

import "fmt"

func main() {
    s := "Привет"
    fmt.Println(len(s))          // 12, а не 6 — это число БАЙТОВ
    fmt.Println([]byte(s))       // срез байтов UTF-8
    fmt.Printf("%c
", s[0])     // Ð — половина буквы, мусор
}

len(s) возвращает 12, потому что каждая кириллическая буква в UTF-8 занимает два байта. Индексация s[0] даёт один байт, а не символ — поэтому распечатать «первую букву» так не получится. Чтобы работать с символами (в терминах Юникода — с код-поинтами), в Go есть тип rune. Руна — это псевдоним для int32, хранящий одну код-точку Юникода.

s := "Привет"
r := []rune(s)
fmt.Println(len(r))       // 6 — теперь по буквам
fmt.Printf("%c
", r[0])  // П

Когда ты пишешь for ... range по строке, Go сам декодирует UTF-8 и выдаёт руны, а не байты — это правильный способ идти по символам. Мы вернёмся к этому в главе 3, когда будем разбирать циклы. А пока запомни правило: байты — для сети и файлов, руны — когда важны именно символы.

Системы счисления и вывод чисел

Пакет fmt умеет печатать числа в разных системах счисления через глаголы форматирования. Это пригодится, когда будешь работать с битовыми масками, цветами или сетевыми протоколами:

package main

import "fmt"

func main() {
    for i := 60; i < 70; i++ {
        // %d — десятичное, %b — двоичное, %x — шестнадцатеричное, %q — символ в кавычках
        fmt.Printf("%d 	 %b 	 %x 	 %q 
", i, i, i, i)
    }
}

Здесь %d печатает привычное десятичное число, %b — двоичное представление, %x — шестнадцатеричное, а %q — символ, соответствующий этому коду, в кавычках. Такой цикл — отличный способ своими глазами увидеть, как одно и то же число выглядит в разных системах, и заодно потренировать работу с Printf.

Кейс из реального проекта

Соберём каркас маленькой CLI-утилиты greet, которая приветствует пользователя по имени из аргумента командной строки. Это типичная первая программа, которую пишут, разбираясь с новым языком, и она показывает, как складываются вместе пакеты, аргументы и вывод.

package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    // os.Args[0] — путь к самой программе, аргументы начинаются с индекса 1.
    args := os.Args[1:]
    if len(args) == 0 {
        fmt.Fprintln(os.Stderr, "использование: greet ИМЯ")
        os.Exit(1)
    }

    name := strings.Join(args, " ")
    fmt.Printf("Привет, %s! Букв в имени: %d
", name, len([]rune(name)))
}

Разберём, что здесь происходит и почему именно так. Аргументы командной строки лежат в срезе os.Args, причём нулевой элемент — это имя самой программы, поэтому берём срез начиная с первого. Если аргументов нет, печатаем подсказку в стандартный поток ОШИБОК os.Stderr (а не в обычный вывод — это важно, чтобы сообщения об ошибках можно было отделить при перенаправлении потоков) и выходим с ненулевым кодом os.Exit(1), сигнализируя оболочке о неуспехе. Имя собираем из всех аргументов через strings.Join, а длину считаем по рунам, чтобы кириллица посчиталась правильно. Собрать и запустить:

go build -o greet main.go
./greet Тим Маклауд

Типичные ошибки

Фигурная скобка на новой строке

Самая частая ошибка первого дня. Из-за автоматической расстановки точек с запятой Go не разрешает переносить открывающую скобку. Такой код не скомпилируется:

// НЕ надо — ошибка компиляции
func main()
{
    fmt.Println("hi")
}

Компилятор после main() вставит невидимую точку с запятой и решит, что тело функции пустое. Правильно — скобка на той же строке: func main() {. Смирись с этим сразу, спорить бесполезно.

len строки как число символов

Многие уверены, что len(s) для строки вернёт количество символов. Для не-ASCII текста это не так: возвращается число байтов. Если нужно посчитать символы, приводи к рунам или используй utf8.RuneCountInString.

import "unicode/utf8"

s := "café"
fmt.Println(len(s))                    // 5 — байт
fmt.Println(utf8.RuneCountInString(s)) // 4 — символа

GOPATH-мышление в эпоху модулей

Если ты нагуглил гайд, где велят создавать проект внутри $GOPATH/src — это устаревшая инструкция для Go до 1.11. Сегодня проект живёт где угодно, а зависимостями управляет go.mod. Начинай любой новый проект с go mod init, а не с копания в GOPATH.

Игнорирование go vet и форматтера

Новички часто отключают автоформатирование, потому что «мешает». В Go это антипаттерн: единый стиль — часть культуры языка, и код без gofmt сразу режет глаз рецензентам. Настрой форматирование при сохранении в редакторе один раз и забудь.

Практика

  1. Установи Go, проверь версию через go version и выведи значение go env GOPATH. Убедись, что версия не ниже 1.22.
  2. Напиши «Hello, world», запусти его через go run, затем собери бинарник через go build и запусти получившийся файл. Сравни поведение.
  3. Инициализируй модуль go mod init, вынеси функцию Reverse в отдельный пакет stringutil и вызови её из main. Проверь, что произойдёт, если переименовать функцию в reverse со строчной буквы.
  4. Напиши программу, которая печатает число символов и число байтов в строке "Привет, мир", и объясни себе, почему они различаются.
  5. Собери кросс-компиляцией бинарник под Linux (GOOS=linux) со своей машины и убедись, что файл создался.

Итог

Ты установил toolchain, написал и запустил первую программу, понял разницу между go run и go build, познакомился с модулями и пакетами, усвоил правило видимости по регистру имени и разобрался, почему строки в Go — это байты, а символы — руны. Это скелет, на который дальше нарастает всё остальное. В следующей главе мы плотно займёмся переменными, типами и константами: научимся объявлять переменные двумя способами, разберём нулевые значения, примитивные типы, пустой идентификатор и мощный механизм iota для генерации константных наборов.

Содержание серии (12)
{# Звук celebration (этап 4.3):