Перейти к содержанию

Лекция 2. Переменные, типы и управляющие конструкции

Объявление переменных

В Go три способа объявить переменную:

// 1. Полная форма с явным типом
var x int = 42

// 2. С выводом типа (тип компилятор берёт из правой части)
var y = 42       // тип int

// 3. Короткое объявление (только внутри функций)
z := 42          // тип int

Оператор := — комбинация объявления и инициализации. Самая частая форма. Работает только внутри функций (на уровне пакета — нельзя, там только var/const).

Несколько переменных сразу

var a, b int = 1, 2
x, y := 3.14, "hello"

// Блок объявлений
var (
    name    string
    age     int
    active  bool
)

Zero values

Если переменная объявлена без инициализации, она получает нулевое значение соответствующего типа:

Тип Zero value
числовые 0
bool false
string "" (пустая)
указатели, срезы, карты, каналы, функции, интерфейсы nil

Это важное отличие от Python: переменная всегда инициализирована, обращение к ней не упадёт с ошибкой «не определена». Но если вы забыли её присвоить — получите 0 или "", и баг будет тихим.

Неиспользуемые переменные — это ошибка компиляции

func main() {
    x := 42  // declared and not used
}

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

_, err := strconv.Atoi("42")

То же правило для импортов: неиспользуемый импорт — ошибка компиляции.

Константы

const Pi = 3.14159
const Greeting = "Привет"

const (
    StatusOK       = 200
    StatusNotFound = 404
)

Константы вычисляются во время компиляции. Тип можно не указывать — он будет «нетипизированной константой» и приведётся к нужному типу в момент использования.

iota — генератор последовательностей

const (
    Sunday = iota   // 0
    Monday          // 1
    Tuesday         // 2
    Wednesday       // 3
    Thursday        // 4
    Friday          // 5
    Saturday        // 6
)

iota начинается с 0 на каждом const-блоке и инкрементируется на каждой следующей строке. Удобно для enum-подобных конструкций (полноценных enum в Go нет).

const (
    KB = 1 << (10 * (iota + 1))  // 1024
    MB                            // 1048576
    GB                            // 1073741824
)

Базовые типы

Целочисленные

Тип Размер Диапазон
int8 1 байт -128..127
int16 2 байта -32768..32767
int32 4 байта -2³¹..2³¹−1
int64 8 байт -2⁶³..2⁶³−1
int 4 или 8 байт (платформо-зависимо) как int32/int64
uint8 (= byte) 1 байт 0..255
uint16, uint32, uint64, uint беззнаковые аналоги
uintptr размер указателя для unsafe-кода

В отличие от Python (где int — произвольной точности), в Go размер фиксирован. Это даёт скорость и предсказуемое потребление памяти, но при переполнении получаете wrap-around: var x int8 = 127; x++ // -128. Никаких ошибок — компилятор молчит.

Числа с плавающей точкой

  • float32 — IEEE-754 single precision;
  • float64 — IEEE-754 double precision (это «нормальный» float, используется по умолчанию).
var pi float64 = 3.14159
e := 2.71828  // тип float64 (default)

Комплексные

complex64 и complex128 — комплексные числа. Редко нужны, упоминаем для полноты.

Логический

var ok bool = true
done := false

Никаких неявных преобразований из числа в bool (как в Python, где 0 == False). if 1 { ... } — ошибка компиляции.

Строки и руны

s := "Привет, мир!"
fmt.Println(len(s))  // 22 — длина в БАЙТАХ, не в символах

// Перебор по рунам (Unicode code points)
for i, r := range s {
    fmt.Printf("%d: %c (%U)\n", i, r, r)
}

string в Go — это неизменяемая последовательность байт (UTF-8 по соглашению). byte — это псевдоним uint8. rune — псевдоним int32, представляет одну Unicode code point.

s := "Привет"
fmt.Println(len(s))                     // 12 (каждая кириллическая буква = 2 байта в UTF-8)
fmt.Println(utf8.RuneCountInString(s))  // 6

Подробно про кодировки и байты — в лекции 4 темы 3.

Преобразование типов

Никаких неявных приведений. Всё пишется явно:

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

Это раздражает поначалу, но спасает от множества тонких багов. Сравните с Python, где 1 + 1.0 == 2.0 работает молча — в Go это int + float64 и не скомпилируется.

Числа в строки и обратно

Через пакет strconv:

import "strconv"

s := strconv.Itoa(42)             // "42"
n, err := strconv.Atoi("42")       // 42, nil
f, err := strconv.ParseFloat("3.14", 64)
str := strconv.FormatFloat(3.14, 'f', 2, 64)  // "3.14"

Любой strconv.Parse* возвращает второе значение error — это обязательная конвенция Go (см. лекцию 3).

if с инициализацией

Базовый синтаксис без скобок вокруг условия:

if x > 10 {
    fmt.Println("big")
} else if x > 0 {
    fmt.Println("small")
} else {
    fmt.Println("non-positive")
}

Уникальная фича Go — короткая инициализация прямо в условии:

if n, err := strconv.Atoi("42"); err == nil {
    fmt.Println("got number:", n)
} else {
    fmt.Println("error:", err)
}
// здесь n и err уже не видны — их scope ограничен if/else

Это идиоматичный паттерн. Переменные живут только внутри блока if/else, не загрязняют внешний scope.

Тернарного оператора нет

В Go сознательно отказались от ?:. Пишите полноценный if или используйте функции-обёртки. С Go 1.18 (дженерики) можно написать свою:

func If[T any](cond bool, a, b T) T {
    if cond {
        return a
    }
    return b
}

msg := If(ok, "yes", "no")

Но идиоматично — обычный if.

for — единственный цикл

В Go нет while, нет do-while. Есть только for, который умеет всё.

// классический for
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// while-эквивалент
for x < 100 {
    x *= 2
}

// бесконечный цикл
for {
    if done {
        break
    }
}

// for-range по коллекции
nums := []int{10, 20, 30}
for i, v := range nums {
    fmt.Println(i, v)
}

// только индекс
for i := range nums {
    fmt.Println(i)
}

// только значение (индекс игнорируем через _)
for _, v := range nums {
    fmt.Println(v)
}

range работает по срезам, массивам, строкам (выдаёт руны!), картам и каналам.

Метки и break из вложенного цикла

outer:
for i := 0; i < 10; i++ {
    for j := 0; j < 10; j++ {
        if i*j > 50 {
            break outer
        }
    }
}

Аналог continue тоже работает с метками. Используйте редко — чаще лучше вынести в функцию и сделать return.

switch

switch в Go мощнее, чем в C/Python.

switch day {
case "monday", "tuesday", "wednesday", "thursday", "friday":
    fmt.Println("будний")
case "saturday", "sunday":
    fmt.Println("выходной")
default:
    fmt.Println("неизвестно")
}

Особенности:

  1. Нет неявного fallthrough — каждый case сам по себе. Это противоположность C, где забытый break — частый баг.
  2. Можно перечислять несколько значений через запятую.
  3. Можно использовать выражения вместо тегов:
switch {
case x < 0:
    fmt.Println("отрицательное")
case x == 0:
    fmt.Println("ноль")
case x > 0:
    fmt.Println("положительное")
}

Это эквивалент длинной цепочки if/else if/else.

  1. fallthrough доступен явно, если очень нужен:
switch x {
case 1:
    fmt.Println("один")
    fallthrough
case 2:
    fmt.Println("два или один")
}
  1. Type switch — особый вид, разбирается в лекции 5 (методы и интерфейсы).

defer — отложенный вызов

Уникальная конструкция Go. defer f() гарантирует, что функция f будет вызвана при выходе из текущей функции (нормальном или через panic).

func readFile(name string) error {
    f, err := os.Open(name)
    if err != nil {
        return err
    }
    defer f.Close()  // выполнится перед return, что бы дальше ни случилось

    // ... работа с f ...
    return nil
}

Это аналог Python with (context manager) или Java try-with-resources. Но удобнее: вы видите открытие и закрытие рядом, а сама работа может занимать любое количество строк.

Правила:

  1. Аргументы вычисляются в момент defer, а не в момент вызова:
i := 1
defer fmt.Println(i)  // напечатает 1
i = 2
return                 // несмотря на i=2
  1. Несколько defer выполняются в обратном порядке (LIFO):
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
// вывод: 3, 2, 1
  1. defer выполнится даже при panic. Это основа механизма recover (лекция 3).

Типичные применения: закрытие файлов, разблокировка мьютексов, восстановление состояния (debug-логи).

Указатели

В Go есть указатели, но без арифметики (как в C). Это просто адрес переменной.

x := 42
p := &x      // p — указатель на x (тип *int)
fmt.Println(*p)  // 42 — разыменование
*p = 100
fmt.Println(x)   // 100

Зачем указатели нужны:

  1. Передать большую структуру в функцию без копирования.
  2. Дать функции возможность изменять переданный аргумент.
  3. Различать «значение есть, оно нулевое» и «значения нет» (через *int = nil).

Указатели в Go безопасны: нельзя получить адрес «куда попало», нельзя сделать p++. Сборщик мусора отслеживает их и не освобождает память, пока есть живые ссылки.

func zero(p *int) {
    *p = 0
}

x := 42
zero(&x)
fmt.Println(x)  // 0

Параллель с Python

Python Go
x = 42 x := 42
x: int = 42 var x int = 42
PI = 3.14 const Pi = 3.14
динамическая типизация статическая, явные преобразования
int — bignum int фиксированного размера, переполнения молчат
if x: if x != 0 { ... } (не приводит число к bool)
if x > 0: ... elif y > 0: ... else: if x > 0 { ... } else if y > 0 { ... } else { ... }
x if cond else y только полноценный if
while, for ... in ... только for (в разных формах)
match (3.10+) switch (без fallthrough)
with open(...) as f: defer f.Close()
ссылки на объекты везде значения + указатели &x / *p

Итог

В Go явная типизация, фиксированные размеры чисел, нет неявных преобразований. Единственный цикл — for, мощный switch без fallthrough, уникальный defer для отложенной очистки. Указатели есть, но без арифметики и под GC. В следующей лекции — функции, ошибки как значения и panic/recover.