null

Go вместе изучать Go. Часть 3

Введение

В этой статье мы подробно разберем одни из самых фундаментальных аспектов языка Go: указатели, структуры, коллекции данных (массивы и срезы), карты, а также функции как значения и замыкания. Эти концепции являются основой для написания профессионального кода на Go и используются повседневно в разработке веб-приложений, микросервисов и систем обработки данных.

Давайте начнем наше путешествие в мир Go, начиная с одной из самых важных и часто неправильно понимаемых концепций — указателей.

1. Указатели (Pointers): Управление памятью и ссылками на данные

Что такое указатель и зачем он нужен?

Указатель — это переменная, которая хранит адрес памяти другой переменной. Это один из самых мощных и одновременно самых запутанных инструментов в программировании. Указатели позволяют нам работать непосредственно с памятью, что дает нам больший контроль над тем, как программа использует ресурсы.

Представьте себе реальный пример: в почтовой системе письмо содержит не саму квартиру, а адрес квартиры. Указатель работает по тому же принципу — это "адрес" переменной в памяти, а не сама переменная.

Синтаксис указателей в Go

В Go указатели объявляются с помощью префикса *.

Вот основные операции:

package main

import "fmt"

func main() {
	// Объявляем переменную типа int
	i := 42
	
	// Создаем указатель на переменную i
	// & оператор "взять адрес" — возвращает адрес памяти переменной
	p := &i
	
	// *p — разыменование указателя, получение значения по адресу
	fmt.Println(*p)  // Выведет: 42
	
	// Можем изменить значение через указатель
	*p = 21
	fmt.Println(i)   // Выведет: 21, так как i изменилась через указатель
}

Объяснение кода:

  •  var p *int — объявляет переменную p как указатель на значение типа int. Начальное значение nil (нулевое значение указателя).
  •  &i — оператор взятия адреса (address-of operator). Возвращает адрес памяти, где хранится переменная i.
  •  *p — оператор разыменования (dereference operator). Получает значение, на которое указывает p.

Практический пример: Работа с указателями

package main

import "fmt"

func main() {
	i, j := 42, 2701

	p := &i         // p указывает на i
	fmt.Println(*p) // Читаем значение i через указатель: 42
	*p = 21         // Изменяем значение i через указатель
	fmt.Println(i)  // Видим новое значение i: 21

	p = &j         // p теперь указывает на j
	*p = *p / 37   // Делим значение j на 37 через указатель
	fmt.Println(j) // Видим новое значение j: 73
}

//Вывод:
//42
//21
//73

Еще пример: Система управления банковскими счетами

В банковской системе часто требуется изменять баланс счета через различные операции (снятие, пополнение, переводы). Указатели позволяют безопасно передавать и изменять данные счета:

type BankAccount struct {
	Balance float64
}

func Withdraw(account *BankAccount, amount float64) bool {
	if account.Balance >= amount {
		*(&account.Balance) = account.Balance - amount
		return true
	}
	return false
}

// Использование:
account := &BankAccount{Balance: 1000}
Withdraw(account, 250) // Баланс теперь 750

Важные особенности указателей в Go

  • Отсутствие арифметики указателей: В отличие от C, Go не позволяет выполнять арифметические операции с указателями (например, p++). Это безопаснее и предотвращает многие типичные ошибки.
  • Нулевое значение: Нулевое значение указателя — это nil, что означает, что указатель не указывает ни на что.
  • Безопасность памяти: Go использует сборку мусора, поэтому вам не нужно беспокоиться об освобождении памяти, как в C.

Зачем нужны указатели если есть обычные переменные?

Коротко: указатели в Go нужны, чтобы работать не с копиями данных, а с самими данными по их адресу в памяти. Обычные переменные передаются и присваиваются по значению (копируются). Если вам нужно изменить исходный объект, избежать дорогостоящих копий или выразить «отсутствие значения» через nil — нужны указатели.

Рассмотрим примеры:

а) Изменение данных внутри функции

Если передать значение, изменится только копия. С указателем меняется оригинал.

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func RenameByValue(p Person, newName string) {
	p.Name = newName // меняем копию, оригинал останется прежним
}

func RenameByPointer(p *Person, newName string) {
	p.Name = newName // меняем оригинал через указатель
}

func main() {
	alex := Person{Name: "Alex", Age: 30}

	RenameByValue(alex, "Alexandr")
	fmt.Println(alex.Name) // Alex (не изменилось)

	RenameByPointer(&alex, "Alexandr")
	fmt.Println(alex.Name) // Alexandr (изменилось)
}

б) Производительность: избегаем дорогостоящих копий

Копирование больших структур может заметно поднимать нагрузку на CPU и GC.

BigReport struct {
	Title string
	Data  [1_000_000]byte // условно большой массив
}

func ProcessByValue(r BigReport) {
	// r скопируется целиком: дорого
}

func ProcessByPointer(r *BigReport) {
	// копируется только адрес: дёшево
}

в) Выразить «отсутствие» через nil

Nil — нулевое значение указателя. Удобно для «нет результата».

type User struct {
	ID   int
	Name string
}

func FindUser(id int) (*User, error) {
	// ... поиск
	return nil, nil // не найдено: nil даёт понять, что пользователя нет
}

func main() {
	u, err := FindUser(42)
	if err != nil { /* ... */ }
	if u == nil {
		fmt.Println("Пользователь не найден")
	}
}

г) Совместное состояние между частями программы

Указатель позволяет нескольким функциям/модулям работать с одним и тем же объектом.

type Config struct {
	Locale string
	Debug  bool
}

func EnableDebug(cfg *Config) { cfg.Debug = true }
func SetLocale(cfg *Config, loc string) { cfg.Locale = loc }

func main() {
	cfg := &Config{Locale: "ru"}
	EnableDebug(cfg)
	SetLocale(cfg, "ka")
	// cfg теперь {Locale:"ka", Debug:true}
}

Важно: в многопоточном коде (goroutines) совместное состояние требует синхронизации (sync.Mutex), иначе будут гонки данных.

д) Структуры данных со связями (списки, деревья)

Связанные структуры строятся на указателях — узлы ссылаются друг на друга.

type Node struct {
	Value int
	Next  *Node
}

func Push(head **Node, v int) {
	newNode := &Node{Value: v, Next: *head}
	*head = newNode
}

func main() {
	var head *Node
	Push(&head, 1)
	Push(&head, 2)
	// head -> 2 -> 1
}

списки задач, деревья категорий, графы маршрутов — всё это удобно моделировать на указателях.

е) Методы с pointer receiver и интерфейсы

Если методы объявлены на *T, то их нельзя вызвать на T (значении) без взятия адреса.

type Counter struct{ n int }

func (c *Counter) Inc() { c.n++ } // pointer receiver: меняет состояние

func main() {
	var c Counter
	// c.Inc() // работает, компилятор сам возьмёт адрес
	fmt.Println(c.n) // 1
}

типы со внутренним состоянием (счётчик, буфер, соединение) обычно имеют методы на *T, чтобы изменять состояние без лишних копий. Интерфейсы в Go чувствительны к метод-сетам: если интерфейс требует метода с pointer receiver, то реализует его только *T, не T.

2. Структуры (Structs): Организация связанных данных

Что такое структура?

Структура — это композитный тип данных, который группирует несколько переменных (полей) разных типов в одну единицу. Структуры являются фундаментом объектно-ориентированного программирования в Go и используются для представления реальных объектов и сущностей.

Если указатели — это "адреса в памяти", то структуры — это "чертежи зданий". Структура определяет, какие данные должны быть связаны вместе.

Объявление и использование структур

package main

import "fmt"

// Определяем структуру Vertex (вершина)
type Vertex struct {
	X int  // первое поле
	Y int  // второе поле
}

func main() {
	// Создаем экземпляр структуры
	fmt.Println(Vertex{1, 2})
}

//Вывод:
//{1 2}

Объяснение:

  • type Vertex struct { ... } — определяет новый тип структуры с именем Vertex
  • У структуры два поля: X и Y, оба типа int
  • При создании Vertex{1, 2} первое значение присваивается X, второе — Y

Доступ к полям структуры

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func main() {
	v := Vertex{1, 2}
	
	// Доступ к полям структуры через точку
	v.X = 4
	fmt.Println(v.X)  // Вывод: 4
}

Поля структуры в Go доступны через оператор точки (.). Это простой и интуитивный синтаксис.

Указатели на структуры

Часто в Go передают структуры через указатели, особенно если структура большая или нужно изменять ее данные:

package main

import "fmt"

type Vertex struct {
	X int
	Y int
}

func main() {
	v := Vertex{1, 2}
	p := &v  // Указатель на структуру
	
	// Важно: в Go можно использовать p.X вместо (*p).X
	// Это синтаксический сахар для удобства
	p.X = 1e9  // Присваиваем 1 миллиард
	fmt.Println(v)
}
//Вывод:
//{1000000000 2}

Объяснение:

  • p := &v создает указатель на структуру v
  • Вместо (*p).X = 1e9 (явное разыменование) Go позволяет писать просто p.X = 1e9
  • Это делает код более читаемым и удобным

Литералы структур: создание структур с частичной инициализацией

package main

import "fmt"

type Vertex struct {
	X, Y int
}

var (
	v1 = Vertex{1, 2}  // оба поля инициализированы
	v2 = Vertex{X: 1}  // Y получит значение по умолчанию (0)
	v3 = Vertex{}      // оба поля = 0
	p  = &Vertex{1, 2} // указатель на структуру
)

func main() {
	fmt.Println(v1, p, v2, v3)
}
//Вывод:
//{1 2} &{1 2} {1 0} {0 0}

Объяснение:

  • Vertex{X: 1} — именованная инициализация. Это удобно, когда у структуры много полей и нужно явно указать, какому полю какое значение
  • Поля без явного значения получают нулевое значение типа (для int это 0)
  • &Vertex{1, 2} создает новую структуру и сразу возвращает на нее указатель

Пример: Система управления недвижимостью

В системе управления недвижимостью структуры используются для представления объектов:

type Property struct {
	ID           int
	Address      string
	Price        float64
	IsAvailable  bool
	Owner        string
	Bedrooms     int
	SquareMeters float64
}

type Location struct {
	Latitude  float64
	Longitude float64
	City      string
}

type RealEstateAgent struct {
	Name        string
	PhoneNumber string
	Email       string
	Properties  []Property
	Locations   []Location
}

// Использование:
agent := RealEstateAgent{
	Name:        "John Smith",
	PhoneNumber: "+995 555 555 555",
	Email:       "john@realty.ge",
	Properties:  []Property{},
}

Структуры в Go идеально подходят для моделирования сложных сущностей и отношений между ними.

 

Next