Перейти к основному содержимому

Ссылочные и специальные типы: Указатели, Интерфейсы, Функции и Каналы

1. Указатели (*T)

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

Аналогия: Адрес вашего дома, записанный на листке бумаги. Сам листок — это не дом. Но с его помощью любой может найти ваш дом и, например, доставить посылку (изменить что-то внутри).

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

  1. Для изменения данных в функциях: В Go аргументы в функции передаются по значению (создается копия). Если вы хотите, чтобы функция изменила оригинальную переменную, вы должны передать на нее указатель.
  2. Для повышения производительности: Копирование больших структур может быть затратным. Гораздо дешевле скопировать 8-байтный указатель (адрес), чем, например, 1-мегабайтную структуру.
  3. Для обозначения "отсутствующего" значения: Нулевое значение для указателя — nil. Это часто используется, чтобы показать, что значение не установлено.
  • Частота использования: Часто. Это фундаментальный инструмент в Go.
  • Когда использовать: При работе с изменяемыми структурами в функциях и для оптимизации передачи больших объемов данных.
type Character struct {
Name string
Health int
}

// Функция принимает УКАЗАТЕЛЬ на Character
func takeDamage(c *Character, damage int) {
// Через указатель мы изменяем поле ОРИГИНАЛЬНОЙ структуры
c.Health -= damage
}

func main() {
player := Character{Name: "Gandalf", Health: 100}

fmt.Printf("Здоровье до атаки: %d\n", player.Health) // Здоровье до атаки: 100

// Мы передаем АДРЕС переменной player с помощью оператора &
takeDamage(&player, 25)

fmt.Printf("Здоровье после атаки: %d\n", player.Health) // Здоровье после атаки: 75
}

2. Интерфейсы (interface)

Интерфейс — это контракт, который определяет набор методов. Любой тип, который реализует все методы этого интерфейса, автоматически (неявно) удовлетворяет этому интерфейсу.

Аналогия: USB-порт. Порту все равно, что вы в него подключите — мышку, флешку или клавиатуру. Главное, чтобы устройство поддерживало протокол USB (реализовывало "методы" для передачи данных).

Ключевые особенности:

  • Полиморфизм: Позволяют писать функции, которые могут работать с разными типами, объединенными общим поведением.

  • Неявная реализация: Не нужно явно указывать, что ваш тип реализует интерфейс. Достаточно просто реализовать все его методы.

  • Пустой интерфейс (interface{}): Особый случай. Может хранить значение абсолютно любого типа. Это делает его мощным, но и опасным, так как теряется статическая проверка типов.

  • Частота использования: Постоянно. Это краеугольный камень идиоматичного Go-кода.

  • Когда использовать: Для создания гибких, слабо связанных систем, где компоненты взаимодействуют через "контракты" (поведение), а не через конкретные реализации.

import "fmt"

// Определяем поведение "умеет форматироваться в строку"
type Stringer interface {
String() string
}

type Product struct {
Name string
Price float64
}

// Product реализует интерфейс Stringer
func (p Product) String() string {
return fmt.Sprintf("%s - $%.2f", p.Name, p.Price)
}

type Discount struct {
Amount float64
}

// Discount тоже реализует интерфейс Stringer
func (d Discount) String() string {
return fmt.Sprintf("Скидка %.2f%%", d.Amount)
}

// Эта функция принимает любой тип, который удовлетворяет Stringer
func PrintInfo(s Stringer) {
fmt.Println("Информация:", s.String())
}

func main() {
p := Product{Name: "Молоко", Price: 1.59}
d := Discount{Amount: 10}

PrintInfo(p) // Информация: Молоко - $1.59
PrintInfo(d) // Информация: Скидка 10.00%
}

3. Функции (func)

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

Аналогия: Рецепт. Вы можете записать его на карточку (присвоить переменной), отдать карточку другу (передать как аргумент) или попросить друга придумать и вернуть вам новый рецепт (вернуть из функции).

  • Частота использования: Часто, особенно в таких паттернах, как middleware в веб-серверах, обработчики событий или стратегии.
// Эта функция принимает слайс и функцию-предикат,
// которая решает, подходит ли число.
func filter(numbers []int, test func(int) bool) []int {
var result []int
for _, num := range numbers {
if test(num) {
result = append(result, num)
}
}
return result
}

func main() {
nums := []int{1, 2, 3, 4, 5, 6}

// Передаем анонимную функцию как аргумент
evens := filter(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println("Четные:", evens) // Четные: [2 4 6]

// Или так
odds := filter(nums, func(n int) bool {
return n%2 != 0
})
fmt.Println("Нечетные:", odds) // Нечетные: [1 3 5]
}

4. Каналы (chan)

Канал — это типизированный "конвейер", через который горутины (легковесные потоки) могут безопасно обмениваться данными. Это основной механизм для коммуникации и синхронизации в конкурентном программировании на Go.

Аналогия: Пневматическая почта в банке. Вы кладете капсулу с деньгами (данные) в трубу (канал), и она безопасно доставляется кассиру в другой комнате (другой горутине). Система гарантирует, что только одна капсула перемещается в один момент времени.

  • Частота использования: Часто (в конкурентных программах).
  • Когда использовать: Всегда, когда нужно организовать безопасное взаимодействие между горутинами.
// Эта функция-"рабочий" будет получать задания из канала jobs
// и отправлять результаты в канал results.
func worker(jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("Обработано задание:", j)
results <- j * 2 // Отправляем результат в другой канал
}
}

func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)

// Запускаем горутину-рабочего
go worker(jobs, results)

// Отправляем 3 задания в канал jobs
for j := 1; j <= 3; j++ {
jobs <- j
}
close(jobs) // Закрываем канал, показывая, что заданий больше не будет

// Получаем 3 результата
for r := 1; r <= 3; r++ {
fmt.Println("Получен результат:", <-results)
}
}