Введение
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 сразу режет глаз рецензентам. Настрой форматирование при сохранении в редакторе один раз и забудь.
Практика
- Установи Go, проверь версию через
go versionи выведи значениеgo env GOPATH. Убедись, что версия не ниже 1.22. - Напиши «Hello, world», запусти его через
go run, затем собери бинарник черезgo buildи запусти получившийся файл. Сравни поведение. - Инициализируй модуль
go mod init, вынеси функциюReverseв отдельный пакетstringutilи вызови её изmain. Проверь, что произойдёт, если переименовать функцию вreverseсо строчной буквы. - Напиши программу, которая печатает число символов и число байтов в строке
"Привет, мир", и объясни себе, почему они различаются. - Собери кросс-компиляцией бинарник под Linux (
GOOS=linux) со своей машины и убедись, что файл создался.
Итог
Ты установил toolchain, написал и запустил первую программу, понял разницу между go run и go build, познакомился с модулями и пакетами, усвоил правило видимости по регистру имени и разобрался, почему строки в Go — это байты, а символы — руны. Это скелет, на который дальше нарастает всё остальное. В следующей главе мы плотно займёмся переменными, типами и константами: научимся объявлять переменные двумя способами, разберём нулевые значения, примитивные типы, пустой идентификатор и мощный механизм iota для генерации константных наборов.
Комментарии 0
Пока нет комментариев. Станьте первым!