Если вы пришли в Go после Java, C# или C++, первое знакомство со структурами может вызвать лёгкое недоумение.
Где класс?
Где extends?
Где public, private, protected?
Где абстрактный фабричный менеджер фабрик, без которого невозможно вывести "Hello World"?
Go смотрит на всё это примерно так:
А может… просто данные и методы?
И достаёт struct.

Что такое struct в Go?
Структура — это тип, который объединяет несколько связанных значений.
Например, у нас есть пользователь:
type User struct {
Name string
Age int
Email string
}Теперь можно создать значение этого типа:
user := User{
Name: "Alex",
Age: 24,
Email: "alex@example.com",
}И обращаться к полям:
fmt.Println(user.Name)
fmt.Println(user.Age)Выглядит подозрительно просто.
Так и задумано.
Вместо того чтобы создавать класс, прописывать конструктор, поля, модификаторы доступа и ещё три сущности на случай пятницы, мы просто описываем данные.
Создание структуры
Полное заполнение
user := User{
Name: "Max",
Age: 21,
Email: "max@example.com",
}Это самый читаемый вариант. Особенно если структура большая.
Можно заполнить только часть полей
user := User{
Name: "Max",
}Остальные получат нулевые значения.
Для string:
""Для int:
0Для bool:
falseТо есть Go не заставляет вас срочно писать конструктор только потому, что иначе Вселенная развалится.
А где конструктор?
Вот здесь начинается один из любимых моментов людей, впервые изучающих Go.
Специального синтаксиса конструктора нет. Вообще.
Если нужен — пишем обычную функцию:
func NewUser(name string, email string) User {
return User{
Name: name,
Email: email,
}
}Используем:
user := NewUser("Alex", "alex@example.com")Часто функции-конструкторы в Go называют через New:
NewUser()
NewServer()
NewClient()
NewDatabase()Но это соглашение, а не магия языка.
Go не вызывает тайно пять скрытых методов за вашей спиной.
Методы у структур
Хорошо. Данные есть.
Но объект без поведения — это пока просто красивый пакет переменных.
Добавим метод:
type User struct {
Name string
Age int
}
func (u User) Greet() {
fmt.Println("Привет, меня зовут", u.Name)
}Теперь:
user := User{Name: "Alex"}
user.Greet()Получаем:
Привет, меня зовут AlexВот вам объектное поведение.
Без ключевого слова class.
Никто не пострадал.
Что такое receiver?
Вот эта часть:
(u User)называется receiver.
Она показывает, к какому типу относится метод.
Можно мысленно читать:
func (u User) Greet()как: функция Greet, принадлежащая типу User.
По смыслу похоже на:
user.greet();из мира Java.
Но технически Go не строит вокруг этого классическую систему классов.
Изменяем структуру: value receiver против pointer receiver
Допустим, хотим сделать пользователя старше:
func (u User) Birthday() {
u.Age++
}Пишем:
user := User{
Name: "Alex",
Age: 20,
}
user.Birthday()
fmt.Println(user.Age)И получаем:
20Начинающий Go-разработчик:
ЧЕГО
Причина проста.
u User получает копию структуры.
Вы изменили копию. Она честно изменилась. А затем исчезла.
Спасибо за участие.
Чтобы менять оригинал, используем указатель:
func (u *User) Birthday() {
u.Age++
}Теперь:
user.Birthday()
fmt.Println(user.Age)Получим:
21Простое правило
Если метод должен изменять структуру, обычно используйте:
func (u *User)Если метод только читает данные, возможен:
func (u User)Хотя в реальных проектах выбор может зависеть ещё и от размера структуры и единообразия API.
Почему нам не приходится писать (*u).Age
Потому что Go в некоторых местах специально делает работу с указателями удобнее.
Можно написать:
u.Age++вместо:
(*u).Age++Язык понимает, что вы имели в виду.
Редкий момент, когда компилятор говорит:
Ладно, брат, тут я сам.
Указатель на структуру
Структуру можно создать через &:
user := &User{
Name: "Alex",
Age: 20,
}Теперь user имеет тип:
*UserМожно проверить:
fmt.Printf("%T\n", user)И получить:
*main.UserТакой подход часто используют, когда объект должен изменяться или его не хочется постоянно копировать.
Поля структур и экспорт
В Go нет привычных:
public
private
protectedВместо этого есть очень простое правило.
Если имя начинается с заглавной буквы, оно экспортируется:
type User struct {
Name string
}Name доступно из других пакетов.
А:
type User struct {
name string
}name доступно только внутри текущего пакета.
Да. Модификатор доступа буквально определяется регистром первой буквы.
Java-разработчик в этот момент где-то роняет public static final.
Вложенные структуры
Структуры можно помещать друг в друга.
type Address struct {
City string
Country string
}
type User struct {
Name string
Address Address
}Создание:
user := User{
Name: "Alex",
Address: Address{
City: "Moscow",
Country: "Russia",
},
}Обращение:
fmt.Println(user.Address.City)Это уже позволяет собирать довольно сложные модели.
Композиция вместо наследования
Вот тут Go окончательно говорит классической ООП:
Мы решили остаться друзьями.
Допустим, есть логгер:
type Logger struct{}
func (Logger) Log(message string) {
fmt.Println("[LOG]", message)
}И сервер:
type Server struct {
Logger Logger
}Использование:
server := Server{}
server.Logger.Log("Server started")Вместо:
Server extends Loggerмы просто говорим: Server содержит Logger.
Это и есть композиция.
И довольно часто она приводит к более понятной архитектуре, чем глубокое наследование.
Потому что:
PremiumAdminUser extends AdminUser extends User extends Person extends Human extends Mammal— это уже не архитектура.
Это генеалогическое древо.
Embedded fields: когда можно ещё короче
Go позволяет встроить один тип в другой:
type Logger struct{}
func (Logger) Log(message string) {
fmt.Println(message)
}
type Server struct {
Logger
}Теперь можно написать:
server := Server{}
server.Log("Started")Вместо:
server.Logger.Log("Started")Такой механизм называется embedding.
Он немного напоминает наследование, но это не классическое наследование.
Server не становится Logger. Он содержит Logger и получает удобный доступ к его полям и методам.
Несколько встроенных структур
type Logger struct{}
func (Logger) Log(message string) {
fmt.Println(message)
}
type Metrics struct{}
func (Metrics) Track() {
fmt.Println("Tracking metrics")
}
type Server struct {
Logger
Metrics
}Теперь:
server := Server{}
server.Log("Started")
server.Track()Получается своеобразный конструктор LEGO.
Нужна функциональность — добавили компонент. Не нужна — не добавили.
Без наследования от AbstractEnterpriseBaseServerFactoryV2.
Структуры и интерфейсы — вот где начинается настоящая магия Go
Допустим, есть интерфейс:
type Speaker interface {
Speak()
}И структура:
type Dog struct {
Name string
}Добавляем метод:
func (d Dog) Speak() {
fmt.Println("Woof!")
}Всё.
Dog теперь удовлетворяет интерфейсу Speaker.
Не нужно писать:
implements SpeakerGo просто смотрит:
Метод есть? Есть. Ну и отлично.
Используем:
func MakeSpeak(s Speaker) {
s.Speak()
}
dog := Dog{Name: "Bob"}
MakeSpeak(dog)Вот почему интерфейсы и структуры в Go настолько хорошо работают вместе.
Связи между типами получаются слабее, а код — гибче.
Пример из реального проекта
Представим API интернет-магазина.
Есть товар:
type Product struct {
ID int
Name string
Price float64
}Добавим метод:
func (p Product) FinalPrice(discount float64) float64 {
return p.Price * (1 - discount)
}Используем:
product := Product{
ID: 1,
Name: "Mechanical Keyboard",
Price: 100,
}
price := product.FinalPrice(0.15)
fmt.Println(price)Получим:
85И никакой отдельный ProductServiceFactorySingleton для вычисления цены нам пока не понадобился.
Красота.
Анонимные структуры
Иногда отдельный тип создавать вообще не хочется.
user := struct {
Name string
Age int
}{
Name: "Alex",
Age: 23,
}Это анонимная структура.
В обычной бизнес-логике злоупотреблять такими штуками не стоит, но для небольших локальных данных, тестов или временных конструкций бывает удобно.
Структуры и JSON
Очень частый сценарий в backend-разработке на Go:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}Вот эти части:
`json:"id"`называются тегами структуры.
Они помогают пакетам вроде encoding/json понимать, как преобразовывать поля.
user := User{
ID: 1,
Name: "Alex",
Email: "alex@example.com",
}
data, err := json.Marshal(user)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))Получим примерно:
{"id":1,"name":"Alex","email":"alex@example.com"}А вот здесь уже начинается Go-бэкенд в его естественной среде обитания.
struct. JSON. API.
И где-то неподалёку обязательно сидит:
if err != nilomitempty
Можно попросить JSON не выводить пустые значения:
type User struct {
Name string `json:"name"`
Avatar string `json:"avatar,omitempty"`
}Если:
Avatar == ""поле можно не включать в JSON.
Очень удобно для API.
Сравнение структур
Некоторые структуры в Go можно сравнивать напрямую:
type Point struct {
X int
Y int
}
a := Point{X: 10, Y: 20}
b := Point{X: 10, Y: 20}
fmt.Println(a == b)Получим:
trueНо только если все поля структуры сами являются сравнимыми.
Например, структура со slice:
type User struct {
Tags []string
}уже не сравнивается обычным ==.
А ООП в Go вообще есть?
Это зависит от того, насколько священной для вас является формулировка из учебника.
В Go есть:
типы;
структуры;
методы;
интерфейсы;
инкапсуляция через пакеты;
композиция;
полиморфизм через интерфейсы.
Но нет классического наследования классов.
И нет классов в привычном Java/C#-смысле.
Поэтому правильнее сказать: Go позволяет использовать многие объектно-ориентированные идеи, но делает это без классической модели ООП.
И именно поэтому код часто получается довольно прямолинейным.
Где потренироваться со структурами в Go
Прочитать про struct — полезно.
Но настоящий момент истины наступает примерно здесь:
func (u User)против:
func (u *User)Пока несколько раз сам не напишешь код, указатели, методы и embedding могут выглядеть как магические руны древних гоферов.
В Кодике программирование можно изучать через практику: проходить уроки, сразу писать код и закреплять новые конструкции упражнениями.
Если изучаете Go, особенно полезно не просто читать синтаксис, а самостоятельно собирать небольшие структуры: пользователя, товар, персонажа игры, сервер, банковский счёт.
А ещё у нас есть Telegram-сообщество Кодика, где выходят короткие полезные посты про программирование, языки, инструменты и разработку. Удобный вариант регулярно повторять материал и узнавать новые штуки без очередной лекции на три часа.
Мини-практика
Попробуйте самостоятельно написать:
type Player struct {
Name string
Health int
Level int
}Добавьте методы:
TakeDamage()
Heal()
LevelUp()
IsAlive()Например:
func (p *Player) TakeDamage(damage int) {
p.Health -= damage
if p.Health < 0 {
p.Health = 0
}
}Метод проверки:
func (p Player) IsAlive() bool {
return p.Health > 0
}И тест:
player := Player{
Name: "Neo",
Health: 100,
Level: 1,
}
player.TakeDamage(40)
fmt.Println(player.Health)
fmt.Println(player.IsAlive())Если всё работает — поздравляем.
Вы только что создали объект с состоянием и поведением.
И ни один класс при этом не был создан.
