Ссылочные и специальные типы: Указатели, Интерфейсы, Функции и Каналы
1. Указатели (*T)
Указатель — это переменная, которая хранит адрес в памяти другой переменной. Он не содержит само значение, а лишь "указывает" на то место, где это значение находится.
Аналогия: Адрес вашего дома, записанный на листке бумаги. Сам листок — это не дом. Но с его помощью любой может найти ваш дом и, например, доставить посылку (изменить что-то внутри).
Зачем нужны указатели?
- Для изменения данных в функциях: В Go аргументы в функции передаются по значению (создается копия). Если вы хотите, чтобы функция изменила оригинальную переменную, вы должны передать на нее указатель.
- Для повышения производительности: Копирование больших структур может быть затратным. Гораздо дешевле скопировать 8-байтный указатель (адрес), чем, например, 1-мегабайтную структуру.
- Для обозначения "отсутствующего" значения: Нулевое значение для указателя —
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)
}
}