JavaRush /Курсы /Go SELF /Zero values — значения по умолчанию

Zero values — значения по умолчанию

Go SELF
6 уровень , 3 лекция
Открыта

1. Введение

Когда вы только начинаете программировать, кажется, что «объявил переменную» и «записал в неё значение» — это одно и то же действие, просто иногда в две строчки. На практике это разные вещи: объявление создаёт переменную (имя + тип), а присваивание меняет её содержимое. В Go между этими шагами никогда не образуется пустота: как только переменная создана, она автоматически получает значение по умолчанию для своего типа.

Это значение и называется zero value. Оно не «нулевое» в смысле «всегда 0», а «нулевое» в смысле «самое базовое значение типа, которое безопасно использовать». Для чисел это будет ноль, для boolfalse, для строки — пустая строка. Главная практическая выгода: вы можете сразу читать переменную после объявления — без риска вытащить случайный мусор и получить поведение «сегодня работает, завтра нет».

2. Zero values базовых типов

Если zero value не зафиксировать в голове как таблицу, то он превращается в «магическое знание», а магия в программировании обычно заканчивается одинаково: кто-то плачет над дебаггером. Поэтому давайте один раз явно посмотрим, какие значения по умолчанию имеют базовые типы, с которыми мы сейчас работаем.

Тип в Go Zero value Что обычно означает в коде Где может быть ловушка
int
0
«ничего не накопили», «счётчик пуст» 0 может быть реальным вводом, а не “не задано”
float64
0
«нет суммы», «пока ноль» 0.0 может быть валидным значением (температура, баланс)
bool
false
«флаг выключен», «условие не выполнено» false может означать как “нет”, так и “ещё не проверяли”
string
""
«пустой текст», «ничего не задано» пустая строка иногда допустима как реальное значение

А теперь маленькая демонстрация: объявляем переменные и сразу печатаем. Вывод помогает мозгу закрепить факт.

package main

import "fmt"

func main() {
	var n int
	var f float64
	var ok bool
	var s string

	fmt.Println(n)  // 0
	fmt.Println(f)  // 0
	fmt.Println(ok) // false
	fmt.Println(s)  // (пустая строка, визуально "ничего")
}

Обратите внимание на строку: пустая строка печатается как «ничего», и это иногда сбивает с толку. Можно сделать простой трюк: поставить маркеры вокруг строки.

package main

import "fmt"

func main() {
	var name string
	fmt.Println(">", name, "<") // >  <
}

int: счётчики, суммы и почему 0 — отличный старт

С целыми числами zero value особенно приятен, потому что огромный пласт программ — это счётчики, суммы и прочие накопители. Мы постоянно пишем «сложи N чисел», «посчитай количество подходящих элементов», «найди максимальное». И почти всегда естественная стартовая точка — это ноль. Go делает так, что даже если вы забыли «инициализировать сумму нулём», она всё равно будет нулём (но лучше не превращать это в привычку).

Посмотрим на мини-фрагмент, который выглядит скучно, но на самом деле является фундаментом половины алгоритмов на циклах: накопление суммы.

package main

import "fmt"

func main() {
	var sum int // zero value = 0
	sum = sum + 5
	sum = sum + 7
	fmt.Println(sum) // 12
}

Здесь важно уловить мысль: sum уже был корректным числом в момент объявления. Мы не боялись прибавлять, потому что «0 + 5» — нормальная арифметика.

Теперь сделаем маленький шаг к реальности: считаем сумму нескольких введённых чисел (мы уже умеем for и fmt.Scan).

package main

import "fmt"

func main() {
	var sum int
	for i := 0; i < 3; i++ {
		var x int
		fmt.Scan(&x)
		sum = sum + x
	}
	fmt.Println(sum) // сумма трёх чисел
}

Здесь sum — классический пример переменной, которая вполне законно живёт на zero value и ждёт, пока вы начнёте что-то накапливать.

bool: флаги, условия и состояние «ничего не найдено»

Булевы переменные — любимый инструмент, когда нам нужно зафиксировать факт: «нашли ли мы что-то», «выполнили ли условие хотя бы раз», «всё ли в порядке». И почти всегда удобно, что по умолчанию флаг выключен (false). Это логично: пока мы ничего не сделали, мы ещё ничего не нашли и ничего не подтвердили.

Представим задачу: мы читаем несколько чисел и хотим понять, встречалось ли среди них число 10. Флаг found можно объявить через var без инициализации: он и так будет false.

package main

import "fmt"

func main() {
	var found bool // zero value = false
	for i := 0; i < 3; i++ {
		var x int
		fmt.Scan(&x)
		if x == 10 {
			found = true
		}
	}
	fmt.Println(found) // true, если среди чисел было 10
}

Фактически, zero value даёт нам честное состояние «ещё не было повода сказать true». Это удобно и читаемо.

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

string: пустая строка как нормальное состояние

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

Давайте посмотрим на самый простой пример: объявили строку и проверили, пустая ли она.

package main

import "fmt"

func main() {
	var title string // zero value = ""
	fmt.Println(title == "") // true
}

Пустая строка удобна ещё и тем, что обычно её легко проверить. Если вам нужно различать «пользователь ничего не ввёл» и «пользователь ввёл какое-то имя», то "" — естественный кандидат на состояние «пока пусто».

Ещё один простой, но важный момент: конкатенация с пустой строкой безопасна.

package main

import "fmt"

func main() {
	var prefix string // ""
	result := prefix + "Go"
	fmt.Println(result) // Go
}

Пока что мы не строим сложные строковые пайплайны, но сам факт «строка всегда корректна» сильно упрощает жизнь: никаких null, никаких падений из-за того, что кто-то забыл инициализировать переменную.

float64: ноль с точкой и мелкие ловушки

Вещественные числа (float64) — это числа с дробной частью, и их zero value тоже выглядит как ноль. В выводе через fmt.Println он обычно печатается как 0 (без «.0»), но по смыслу это 0.0. И это удобно по тем же причинам, что и с int: суммы, накопление, сравнения с порогом — всё это стартует с нуля естественно.

Например, мы считаем суммарное время учёбы за несколько дней (пусть часы могут быть дробными, типа 1.5).

package main

import "fmt"

func main() {
	var total float64
	for i := 0; i < 2; i++ {
		var hours float64
		fmt.Scan(&hours)
		total = total + hours
	}
	fmt.Println(total) // сумма часов
}

Тут тоже видно преимущество: total можно не «подготавливать», он уже корректен.

Но с float64 есть особенно неприятная зона, о которой стоит помнить даже на базовом уровне: деление на ноль. Даже если вы пока не углубляетесь в то, как именно ведут себя вещественные числа, практическое правило простое: если вы собираетесь делить на значение, которое могло остаться zero value (например, счётчик дней), проверьте, что оно не ноль. Это не «паранойя», а разумное программирование.

3. Когда zero value превращается в баг

Zero value — это подарок, но, как и многие подарки, он может быть с сюрпризом. Главный сюрприз: zero value часто используют как «признак того, что значения не было», но 0, "" и false вполне могут быть настоящими данными. И тогда программа начинает вести себя логически неверно: компилируется, работает, но косячит.

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

Вот пример «опасного» кода — он выглядит разумно, но логика зависит от входных данных:

package main

import "fmt"

func main() {
	var max int // 0
	for i := 0; i < 3; i++ {
		var x int
		fmt.Scan(&x)
		if x > max {
			max = x
		}
	}
	fmt.Println(max) // может напечатать 0, даже если все числа отрицательные
}

Как с этим жить, не вводя «магические числа» и не забегая вперёд в сложные темы? Один из простых приёмов — завести отдельный флаг, который показывает, что значение уже «инициализировано данными».

package main

import "fmt"

func main() {
	var max int
	var hasMax bool // false: ещё не установили max из ввода

	for i := 0; i < 3; i++ {
		var x int
		fmt.Scan(&x)
		if !hasMax || x > max {
			max = x
			hasMax = true
		}
	}
	fmt.Println(max) // корректно даже для отрицательных
}

Здесь zero value для hasMax как раз работает нам на руку: false честно означает «пока не было ни одного числа». А потом мы сами переводим флаг в true, когда получаем первые данные.

Небольшой интересный факт из мира стандартной библиотеки: подход «обнулить старые значения до zero value» используется и в реальных API. Например, некоторые операции над срезами (мы их разберём позже) специально «очищают хвост», устанавливая элементы в zero value, чтобы не держать лишние ссылки в памяти. Это не просто теория — это инженерная практика.

Ещё один интересный факт: в некоторых форматах сериализации «нулевые» поля можно не передавать, потому что при чтении они всё равно получат zero value. Например, в обсуждении дизайна gob прямо подчёркивается идея: «если не установить значение, оно и так будет zero value, и его не нужно передавать».

4. Мини‑программа DailyStats

Теперь соберём всё в один небольшой пример, который выглядит как «настоящая программа», но при этом использует только уже знакомые конструкции: var, for, if, fmt.Scan, fmt.Println. Программа будет читать количество дней и ежедневное число часов (дробное), считать общую сумму, максимальное значение и флаг «был ли день, где часов не меньше заданной цели».

Сначала — версия «простая и честная», где мы предполагаем, что часы не бывают отрицательными (для учебного трекера это разумно).

package main

import "fmt"

func main() {
	var days int
	var goal float64
	fmt.Scan(&days, &goal)

	var total float64
	var max float64
	var reached bool

	for i := 0; i < days; i++ {
		var hours float64
		fmt.Scan(&hours)

		total = total + hours
		if hours > max {
			max = hours
		}
		if hours >= goal {
			reached = true
		}
	}

	fmt.Println(total)   // например: 7.5
	fmt.Println(max)     // например: 3
	fmt.Println(reached) // например: true
}

Здесь total, max, reached стартуют с zero values (0, 0, false) и это выглядит естественно. Пока мы не прочитали ни дня, сумма равна нулю, максимум равен нулю, цель не достигнута.

Но давайте сделаем программу устойчивее и заодно покажем идею «не злоупотребляйте магическим нулём». Добавим флаг hasMax, чтобы максимум корректно работал даже если данные могут быть любыми (вдруг вы переиспользуете код для температур, баланса или чего-то ещё).

package main

import "fmt"

func main() {
	var days int
	var goal float64
	fmt.Scan(&days, &goal)

	var total float64
	var max float64
	var hasMax bool
	var reached bool

	for i := 0; i < days; i++ {
		var hours float64
		fmt.Scan(&hours)

		total = total + hours
		if !hasMax || hours > max {
			max = hours
			hasMax = true
		}
		if hours >= goal {
			reached = true
		}
	}

	fmt.Println(total)
	fmt.Println(max)
	fmt.Println(reached)
}

И вот здесь можно заметить красивую вещь: zero value стал частью дизайна. hasMax по умолчанию false, и это не «случайность языка», а понятный смысл: «максимум ещё не определён данными».

5. Типичные ошибки при работе с zero values

Ошибка №1: думать, что “неинициализированная переменная” содержит мусор.
В Go это не так: как только переменная объявлена, она уже хранит корректное значение по умолчанию. Из-за этого многие программы на Go проще читать: вы реже видите «ритуальные инициализации», которые в других языках нужны только ради безопасности.

Ошибка №2: использовать 0, "" или false как универсальный маркер “значение не задано”.
Иногда это работает, иногда ломает логику так тихо, что вы заметите проблему только по неправильным результатам. Если ноль (или пустая строка) могут быть реальными данными, лучше отделять «значение есть» от «значения нет» отдельным флагом bool, как мы сделали с hasMax.

Ошибка №3: случайно полагаться на zero value там, где переменная должна была получить данные из ввода.
Это коварная ситуация: fmt.Scan мог ничего не прочитать (неправильный ввод, пустой stdin), а переменная останется равной zero value, и программа продолжит считать так, будто пользователь ввёл ноль. Снаружи это выглядит как «программа работает», а по факту — молча принимает неверные данные.

Ошибка №4: неверная инициализация максимума или минимума из-за «удобного нуля».
Если вы ищете максимум, инициализировать его нулём можно только если вы уверены, что числа неотрицательны. Иначе получите max = 0 даже тогда, когда во входе нет ни одного нуля. В учебных задачах это встречается настолько часто, что можно считать отдельным жанром: «почему мой максимум всегда ноль».

Ошибка №5: путаница из-за печати пустой строки.
fmt.Println(s) для s == "" выглядит так, будто ничего не вывелось, и начинающий программист решает, что строка «не существует». Строка существует, просто она пустая. Если нужно увидеть это глазами, используйте маркеры вроде fmt.Println(">", s, "<"), чтобы пустота стала заметной.

1
Задача
Go SELF, 6 уровень, 3 лекция
Недоступна
Нулевые значения
Нулевые значения
1
Задача
Go SELF, 6 уровень, 3 лекция
Недоступна
Поиск десятки
Поиск десятки
1
Задача
Go SELF, 6 уровень, 3 лекция
Недоступна
Честный максимум
Честный максимум
1
Задача
Go SELF, 6 уровень, 3 лекция
Недоступна
Среднее плюсов
Среднее плюсов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ