Основы синтаксиса
Добро пожаловать во второй урок! Теперь, когда вы установили Go и написали свою первую программу, пора разобраться с основами этого языка. В этом урокевы изучите: переменные, типы данных, операторы, указатели и комментарии.
Чтобы лучше понять синтаксис Go, мы советуем вам пробовать самим экспериментировать с кодом и изучать его на практике, так вы сможете лучше понять, как работает язык и как его использовать.
Комментарии
Комментарии - это текст, который не выполняется при запуске программы. Они используются для объяснения кода и документирования его функциональности.
// Однострочный комментарий — объясняем одну строку
/*
Многострочный комментарий.
Полезен для описания сложной логики
или временного отключения кода.
*/
// TODO: реализовать кэширование результатов
// FIXME: исправить утечку памяти при большом трафике
// HACK: временное решение до обновления библиотеки
Go имеет инструментgodoc— он генерирует документацию из комментариев перед функциями/типами.
Переменные
Переменные — это именованные ячейки памяти, куда мы кладём данные. Переменные могут быть объявлены несколькими способами:
- Полное объявление с типом
- Без типа — Go сам догадается (type inference)
- Короткое объявление := (самый популярный способ внутри функций)
- Множественное объявление — полезно, когда нужно объявить несколько переменных в функции
- Групповое объявление — удобно для объявления нескольких переменных в одной строке
// 1. Полное объявление с типом
var name string = "Алексей"
var age int = 27
// 2. Без типа — Go сам догадается (type inference)
var city = "Санкт-Петербург"
var height = 178.5
// 3. Короткое объявление := (самый популярный способ внутри функций)
name := "Алексей" // string
age := 27 // int
isDeveloper := true // bool
// 4. Множественное объявление
var (
firstName string = "Мария"
lastName string = "Иванова"
salary float64 = 150000.0
employed bool = true
)
// 5. Групповое короткое объявление
product, price, inStock := "Ноутбук", 89990.0, true
Важно: := работает только внутри функций. Снаружи используйте var.
Константы
Константы — значения, которые нельзя изменить. Объявляются через const. Хорошая практика — использовать определение групповых констант в начале файла для дальнейшего обращения к ним в этом же файле (Пример с HTTP статусами).
const Pi = 3.14159
const AppName = "Мой калькулятор"
// Константы HTTP статусов
const (
HTTPStatusOK = 200
HTTPStatusNotFound = 404
MaxRetries = 5
)
Константы вычисляются на этапе компиляции — это делает код быстрее и безопаснее!
Строки
Строки в Go — это последовательность байтов (UTF-8).
greeting := "Привет, Go! 🚀"
multiline := `
Это многострочная строка.
Очень удобно для SQL-запросов
или длинных сообщений.
`
// Длина строки в байтах
length := len(greeting) // посчитает байты, не символы!
Unicode и UTF-8
Unicode — это международный стандарт, который присваивает уникальный номер (code point) каждому символу из всех письменностей мира: буквы, цифры, эмодзи, иероглифы, математические знаки и т.д.
- Примеры кодов:
'A'→ U+0041'Я'→ U+042F'👋'→ U+1F44B
- На сегодняшний день Unicode содержит более 149 000 символов (и продолжает расти).
- Код поинт — это просто число (обычно записывается как U+XXXXXX).
Unicode отвечает только на вопрос: какой номер у символа? Он не говорит, как эти номера хранить в памяти или на диске.
UTF-8 — это способ кодирования символов Unicode в байты (самый популярный в мире).
Как работает UTF-8
UTF-8 кодирует один символ переменным количеством байт (от 1 до 4):
| Диапазон символов | Количество байт | Пример |
|---|---|---|
| ASCII (0–127) | 1 байт | 'A' → 1 байт (0x41) |
| Русские, европейские буквы | 2 байта | 'Я' → 2 байта |
| Основные эмодзи, китайские | 3 байта | '😊' → 3 байта |
| Редкие символы, сложные эмодзи | 4 байта | '👋' → 4 байта |
Главные преимущества UTF-8:
- Обратная совместимость с ASCII: первые 128 символов — это обычный ASCII (1 байт).
- Экономия памяти: английский текст занимает столько же места, сколько в старых кодировках.
- Безопасность: нет проблем с "битыми" символами при обрезке строки.
- Универсальность: используется в интернете, файлах, базах данных (более 98% веб-сайтов на UTF-8).
Почему это важно для Go
В Go строки — это байты в кодировке UTF-8 по умолчанию:
len("Привет")→ 12 (байт, а не символов!)for i, r := range "Привет"→ правильно перебирает 6 рун (символов)
Unicode — это огромный каталог всех символов мира с уникальными номерами. UTF-8 — умный и экономный способ сохранить эти символы в байтах, который стал стандартом современного мира.
Конкатенация строк
В Go строки — неизменяемые (immutable). Каждый раз, когда ты "склеиваешь" строки, создаётся новая строка в памяти. Это важно понимать, потому что от выбора метода зависит производительность, особенно при большом количестве операций.
Вот все основные способы конкатенации — от простых до самых эффективных.
1. Оператор + (самый простой, но не самый быстрый)
s := "Привет" + ", " + "мир!" + " 🌍"
fmt.Println(s) // Привет, мир! 🌍
Когда использовать:
- Для 2–5 строк — удобно и читаемо.
- В большинстве случаев в обычном коде.
Проблема: При большом количестве конкатенаций создаётся много промежуточных строк → много аллокаций памяти и копирований.
2. fmt.Sprintf — форматирование
name := "Алексей"
age := 30
s := fmt.Sprintf("Меня зовут %s, мне %d лет", name, age)
fmt.Println(s) // Меня зовут Алексей, мне 30 лет
Плюсы:
- Очень читаемо.
- Безопасно для разных типов.
Минусы:
- Медленнее
+при простых случаях. - Выделяет память под буфер.
Когда использовать: когда смешиваешь строки с другими типами (int, float и т.д.).
3. strings.Join — склеивание слайса строк
parts := []string{"Go", "—", "это", "круто!"}
s := strings.Join(parts, " ")
fmt.Println(s) // Go — это круто!
Плюсы:
- Эффективнее, чем многократный
+в цикле. - Один проход по слайсу.
Когда использовать: когда у тебя уже есть слайс строк.
4. strings.Builder — САМЫЙ ЭФФЕКТИВНЫЙ для большого количества операций
Это рекомендуемый способ в Go для конкатенации в цикле.
import "strings"
var builder strings.Builder
for i := 1; i <= 5; i++ {
builder.WriteString("Строка ")
builder.WriteString(strconv.Itoa(i))
builder.WriteString("\n")
}
s := builder.String()
fmt.Println(s)
// Вывод:
// Строка 1
// Строка 2
// Строка 3
// Строка 4
// Строка 5
Почему Builder самый быстрый?
- Выделяет буфер один раз и растит его по мере необходимости.
- Ноль лишних аллокаций (в отличие от
+). - Минимальное копирование.
5. bytes.Buffer — альтернатива Builder (почти то же самое)
import "bytes"
var buf bytes.Buffer
buf.WriteString("Привет")
buf.WriteString(" от Go!")
s := buf.String()
strings.Builder — это упрощённая версия bytes.Buffer, оптимизированная именно для строк.
Сравнение производительности (пример для 1000 конкатенаций)
| Метод | Время (примерно) | Аллокаций памяти | Рекомендация |
|---|---|---|---|
+ в цикле | Очень медленно | ~1000 | Не использовать в циклах! |
fmt.Sprintf | Медленно | Много | Только для простых случаев |
strings.Join | Хорошо | Несколько | Когда есть готовый слайс |
strings.Builder | ⚡ Очень быстро | 1–2 | Лучший выбор для циклов |
Итог: что использовать и когда?
- 2–3 строки → просто
+ - Смешивание типов →
fmt.Sprintf - Слайс строк →
strings.Join - Цикл или много операций → всегда
strings.Builder
Пример "правильного" кода в цикле:
var builder strings.Builder
for _, word := range words {
builder.WriteString(word)
builder.WriteString(" ")
}
result := builder.String()
byte и rune
В Go строки — это не просто массив символов, как в многих других языках. Чтобы правильно работать с текстом (особенно с русским, китайским, эмодзи и т.д.), нужно понимать разницу между byte и rune.
1. byte — это 8-битный байт (uint8)
byte— это псевдоним дляuint8(беззнаковое целое от 0 до 255).- Строка в Go внутри — это слайс байт (
[]byte). - Когда ты пишешь:
s := "Hello"
fmt.Println(s[0]) // 72 — это код буквы 'H' в ASCIIs[0]возвращает byte, то есть один байт строки.
Проблема с byte: Многие символы (русские буквы, эмодзи, китайские иероглифы) не помещаются в один байт. Они кодируются в UTF-8 несколькими байтами (от 2 до 4).
Пример:
s := "Привет 👋"
fmt.Println(len(s)) // 13 — длина в байтах!
fmt.Println(s[0]) // 208 — первый байт буквы 'П'
fmt.Println(s[1]) // 159 — второй байт буквы 'П'
Буква «П» занимает 2 байта, а эмодзи 👋 — 4 байта. Если перебирать строку по байтам — получишь "мусор".
2. rune — это один символ Unicode (int32)
rune— это псевдоним дляint32.- Один
runeпредставляет один юникод-символ (code point). - Чтобы правильно работать с символами — преобразуй строку в слайс рун:
s := "Привет 👋"
runes := []rune(s)
fmt.Println(len(runes)) // 8 — реальное количество символов!
fmt.Println(runes[0]) // 1055 — код буквы 'П'
fmt.Println(runes[6]) // 128075 — код эмодзи 👋
Сравнение byte и rune
| Характеристика | byte | rune |
|---|---|---|
| Тип | uint8 | int32 |
| Размер | 1 байт | 4 байта |
| Что представляет | Один байт строки (в UTF-8) | Один символ Unicode |
Длина строки len(s) | Количество байт | — |
| Правильная длина символов | Нет | len([]rune(s)) |
| Подходит для | Бинарные данные, ASCII | Текст с любыми символами (русский, эмодзи) |
Когда использовать что?
-
byteи[]byte:- Работа с бинарными данными (файлы, сеть, хэши).
- Когда точно знаешь, что текст только ASCII (например, имена файлов, URL).
- Для максимальной эффективности (меньше памяти).
-
runeи[]rune:- Когда нужно считать символы правильно.
- Перебор строки посимвольно.
- Работа с текстом пользователя (имена, сообщения, эмодзи).
Полезные примеры
-
Правильный перебор строки:
s := "Привет, world! 👋"
for i, r := range s { // range работает по рунам!
fmt.Printf("Позиция %d: %c (код %d)\n", i, r, r)
}rangeпо строке автоматически даёт руны — это самый удобный способ! -
Получение символа по индексу:
s := "Hello 👋"
runes := []rune(s)
fmt.Println(string(runes[6])) // 👋 — правильно!
// fmt.Println(string(s[6])) // ошибка! s[6] — байт -
Проверка длины в символах:
import "unicode/utf8"
s := "Hello 👋"
fmt.Println(len(s)) // 10 байт
fmt.Println(utf8.RuneCountInString(s)) // 7 символов — правильно!
Числовые типы
| Тип | Размер | Диапазон | Когда использовать |
|---|---|---|---|
int | 32/64 бита | Зависит от платформы | Обычные целые числа |
int8 | 8 бит | -128..127 | Когда точно знаете маленький диапазон |
int16 | 16 бит | -32768..32767 | |
int32 | 32 бита | -2.1 млрд..2.1 млрд | |
int64 | 64 бита | Очень большой диапазон | Большие числа, время в наносекундах |
uint* | Без знака | Только положительные | Счётчики, биты |
float32 | 32 бита | ~7 знаков после запятой | Когда важна экономия памяти |
float64 | 64 бита | ~15 знаков после запятой (рекомендуется) | Большинство расчётов с дробями |
Пример:
temperature := 23.5 // float64 по умолчанию
balance := 1000000 // int
byteValue := byte('A') // uint8, равно 65
Булевый тип
isActive := true
hasError := false
canProceed := isActive && !hasError
Нулевое значение (zero value)
Если переменную объявили, но не присвоили значение — Go даст "нулевое":
int→ 0float→ 0.0bool→ falsestring→ ""
Это спасает от случайных ошибок!
Преобразование типов
В Go типизация строгая и статическая — компилятор знает тип каждой переменной на этапе компиляции. В отличие от JavaScript или Python, автоматического (неявного) преобразования типов нет. Если хочешь использовать значение одного типа как другого — нужно явно преобразовать. Это делает код предсказуемым и защищает от скрытых ошибок.
Основные числовые преобразования
Между численными типами (int, float64, int32 и т.д.) преобразование простое — указываешь новый тип в скобках.
age := 25 // int
height := 1.75 // float64
// int → float64
total := float64(age) + height
fmt.Println(total) // 26.75
// float64 → int (отбрасывает дробную часть!)
truncated := int(height)
fmt.Println(truncated) // 1 (не округляет! просто отрезает)
// int32 → int64
var small int32 = 100
big := int64(small) // безопасно
Важно:
- При преобразовании из большего типа в меньший (например, int64 → int32) возможна потеря данных или переполнение.
- Из float в int — всегда отбрасывается дробная часть (не округляется).
Число → строка
Для этого используем пакет strconv (standard convert).
import "strconv"
age := 25
ageStr := strconv.Itoa(age) // Itoa = "Integer to ASCII"
fmt.Println(ageStr) // "25"
// Для float
price := 99.90
priceStr := strconv.FormatFloat(price, 'f', 2, 64)
// 'f' — формат без экспоненты
// 2 — количество знаков после запятой
// 64 — битность float64
fmt.Println(priceStr) // "99.90"
// Другие форматы
scientific := strconv.FormatFloat(1234.56, 'e', 2, 64) // "1.23e+03"
withPlus := strconv.FormatFloat(-12.34, 'f', 2, 64) // "-12.34"
Строка → число
Тоже через strconv, но с обработкой ошибок — потому что строка может быть некорректной.
import "strconv"
// Строка → int
num, err := strconv.Atoi("42")
if err != nil {
fmt.Println("Ошибка преобразования:", err)
} else {
fmt.Println("Число:", num) // 42
}
// Ошибки
_, err = strconv.Atoi("abc")
fmt.Println(err) // strconv.Atoi: parsing "abc": invalid syntax
// Строка → float64
f, err := strconv.ParseFloat("3.14159", 64)
if err != nil {
fmt.Println("Ошибка:", err)
} else {
fmt.Println("Float:", f) // 3.14159
}
// ParseFloat умеет и с экспонентой
f, _ = strconv.ParseFloat("1.23e-4", 64) // 0.000123
Строка → bool
b, err := strconv.ParseBool("true")
fmt.Println(b) // true
// Поддерживает: "1", "t", "True", "TRUE", "true" и аналогично для false
Преобразование между []byte и string
Это особый случай — очень эффективный и без копирования (в большинстве случаев).
s := "Привет"
bytes := []byte(s) // string → []byte
back := string(bytes) // []byte → string
fmt.Println(bytes) // [208 159 209 128 208 184 208 178 208 181 209 130]
fmt.Println(back) // Привет
Полезно при работе с сетью, файлами, хэшами.
Когда преобразование невозможно
Go не позволит преобразовать несовместимые типы:
var i int = 42
var s string = string(i) // ошибка компиляции!
Это не символ, а число — прямое преобразование запрещено. Нужно через strconv.
Правила преобразования
| Что → Во что | Как делать | Примечание |
|---|---|---|
| Число → другой численный | newType(oldValue) | Может обрезать или переполниться |
| Число → строка | strconv.Itoa(), strconv.FormatFloat() | |
| Строка → число | strconv.Atoi(), strconv.ParseFloat() | Всегда проверяй ошибку! |
| Строка → []byte и обратно | []byte(s), string(bytes) | Очень быстро, часто без копии |
| Структура → другой тип | Невозможно напрямую | Нужно писать вручную или использовать encoding/json и т.п. |
Главное правило Go: «Явное лучше неявного». Преобразование типов должно быть очевидным и под контролем программиста.
Операторы
Арифметические
a, b := 10, 3
fmt.Println(a + b) // 13
fmt.Println(a - b) // 7
fmt.Println(a * b) // 30
fmt.Println(a / b) // 3 (целочисленное деление)
fmt.Println(a % b) // 1 (остаток)
a++ // a становится 11
b-- // b становится 2
Сравнения
x, y := 5, 10
x == y // false
x != y // true
x < y // true
x <= y // true
x > y // false
x >= y // false
Логические
hasTicket := true
hasPassport := false
canTravel := hasTicket && hasPassport // false (И)
canEnter := hasTicket || hasPassport // true (ИЛИ)
notAllowed := !hasTicket // false (НЕ)
Присваивания
score := 100
score += 20 // 120
score -= 10 // 110
score *= 2 // 220
score /= 5 // 44
score %= 7 // 2
Указатели
В Go есть два способа передавать данные: по значению (value) и по ссылке (через указатель). Указатели — это одна из самых важных тем, потому что они позволяют экономить память, изменять данные "на месте" и работать с большими структурами эффективно.
Что такое указатель?
Указатель — это переменная, которая хранит адрес в памяти другой переменной.
var a int = 10
var p *int = &a // p — указатель на a
&a— оператор "взять адрес" → возвращает указатель наa.*int— тип "указатель на int".*p— оператор "разыменование" → получить значение, на которое указываетp.
Если вы хотите увидеть, как работает указатель, то вот

Зачем нужны указатели?
-
Изменение значения внутри функции По умолчанию Go передаёт аргументы по значению — функция получает копию.
func zero(x int) {
x = 0 // меняем только копию
}
func main() {
a := 5
zero(a)
fmt.Println(a) // всё ещё 5
}С указателем — можно изменить оригинал:
func zeroPtr(x *int) {
*x = 0 // меняем значение по адресу
}
func main() {
a := 5
zeroPtr(&a)
fmt.Println(a) // 0
} -
Экономия памяти при больших структурах Если структура большая (много полей), передача по значению создаёт полную копию — медленно и ест память.
type BigStruct struct {
Data [1000]int
Name string
// ...
}
func process(s BigStruct) { ... } // копируется вся структура — плохо!
func processPtr(s *BigStruct) { ... } // передаётся только адрес (8 байт) — быстро! -
Методы с изменением структуры Часто методы-ресиверы делают указателями, чтобы менять сам объект:
type Counter struct {
value int
}
func (c *Counter) Increment() { // указатель!
c.value++
}
func main() {
c := Counter{value: 5}
c.Increment()
fmt.Println(c.value) // 6
}Если бы ресивер был по значению
(c Counter)— изменение не сохранилось бы. -
nil как "пустое" значение Обычные переменные всегда имеют нулевое значение (0, "", false). Указатели могут быть
nil— это удобно для "опциональных" значений.var p *int // nil
if p == nil {
fmt.Println("указатель пустой")
}
Когда использовать указатели, а когда — нет?
| Ситуация | Рекомендация | Почему |
|---|---|---|
| Маленькие типы (int, bool, float64) | По значению | Копия дешёвая |
| Большие структуры (> ~100 байт) | Указатель | Экономия памяти и времени |
| Нужно изменить аргумент в функции | Указатель | Иначе изменится только копия |
| Метод должен изменить состояние объекта | Указатель-ресивер (*T) | Стандартная практика |
| Возврат структуры из функции | По значению (Go оптимизирует) | Компилятор часто избегает копии (escape analysis) |
| Возврат интерфейса или указателя | Указатель | Интерфейсы и так содержат указатель внутри |
Кастомные типы и type alias
В Go ты можешь создавать свои собственные типы на основе существующих. Это мощный инструмент для:
- Делать код читаемым и выразительным.
- Добавлять типобезопасность (компилятор проверяет правильность использования).
- Упрощать поддержку и рефакторинг.
Есть два способа: кастомный тип (type definition) и type alias (type alias, с Go 1.9).
Кастомный тип (type definition)
Это новый тип, который основан на существующем, но считается отдельным от базового.
type Age int // новый тип Age на основе int
type UserID int64 // новый тип UserID на основе int64
type Email string // новый тип Email на основе string
func celebrateBirthday(a Age) Age {
return a + 1
}
func main() {
var myAge Age = 25
var plainInt int = 30
myAge = celebrateBirthday(myAge) // OK
// myAge = plainInt // ОШИБКА! типы разные
// plainInt = myAge // ОШИБКА!
// Нужно явно преобразовать:
myAge = Age(plainInt)
plainInt = int(myAge)
}
Зачем это нужно?
- Типобезопасность: нельзя случайно передать обычный
intтуда, где нуженAgeилиUserID. - Самодокументирующийся код: читаешь
func createUser(id UserID, email Email)— сразу понятно, что это за данные. - Можно добавлять методы к новому типу:
type Celsius float64
func (c Celsius) ToFahrenheit() float64 {
return float64(c)*9/5 + 32
}
temp := Celsius(25)
fmt.Println(temp.ToFahrenheit()) // 77
Псевдоним типа (Type alias)
Это полный синоним существующего типа. Компилятор считает их одним и тем же типом.
type Second = int // = значит alias
type Meter = float64
func main() {
var s Second = 10
var i int = 20
s = i // OK — Second и int это одно и то же
i = s // OK
}
Зачем alias?
- Рефакторинг и миграция: хочешь поменять базовый тип в большом проекте — сначала делаешь alias, потом постепенно меняешь.
- Упрощение длинных типов:
type HandlerFunc = func(http.ResponseWriter, *http.Request)
type ConfigMap = map[string]interface{} - Совместимость пакетов: когда импортируешь тип из другого пакета и хочешь дать ему короткое имя.
Сравнение: кастомный тип vs alias
| Характеристика | Кастомный тип (type T Underlying) | Alias (type T = Underlying) |
|---|---|---|
| Это новый тип? | Да | Нет (полный синоним) |
| Совместимость с базовым типом | Нет (нужно преобразование) | Да (полная) |
| Можно добавлять методы? | Да | Да |
| Когда использовать | Для типобезопасности, доменных типов | Для рефакторинга, упрощения имен |
Реальные примеры из жизни
-
Домен-driven дизайн:
type UserID int64
type OrderID string
type Money float64 // с методами Round(), Add() и т.д. -
Известные пакеты:
time.Duration— это alias дляint64.sql.NullString— кастомный тип на основе string с полем Valid.
-
Ошибка новичков:
type Count int
func increment(c Count) { c++ } // не сработает! передаётся копияНужно указатель или метод с ресивером-указателем.
Заключение
- Кастомные типы (
type Age int) — используй всегда для доменных сущностей (ID, Email, Money, Temperature), чтобы код был безопасным и понятным. - Alias (
type Meter = float64) — используй для временных упрощений или плавного рефакторинга.
Пример
package main
import (
"fmt"
"strconv"
)
func main() {
// Информация о человеке
name := "Виктория"
age := 22
height := 168.3
isStudent := true
fmt.Printf("Имя: %s\n", name)
fmt.Printf("Возраст: %d лет\n", age)
fmt.Printf("Рост: %.1f см\n", height)
fmt.Printf("Студент: %t\n", isStudent)
// Небольшой расчёт
nextYear := age + 1
ageString := strconv.Itoa(nextYear)
fmt.Println("В следующем году будет:", nextYear)
fmt.Println("Возраст строкой:", ageString)
// Проверка совершеннолетия
isAdult := age >= 18
fmt.Printf("Совершеннолетний: %t\n", isAdult)
}
Запустите этот код — увидите красивый вывод.