🚀 Go с нуля

Полное руководство: от установки до создания игрового сервера

Глава 1: Введение в Go

Что такое Go?

Go (или Golang) — это компилируемый язык программирования, созданный в Google в 2009 году. Его разработали Роб Пайк, Кен Томпсон и Роберт Гризмер — легенды программирования, создавшие UNIX и UTF-8.

Почему Go?

  • Простота — синтаксис минималистичен, легко учить
  • Скорость — компилируется в машинный код
  • Конкурентность — горутины делают параллельное программирование простым
  • Надёжность — строгая типизация, сборка мусора
  • Кроссплатформенность — один код для Windows, Linux, macOS

Где используется Go?

Компания Применение
Google Kubernetes, YouTube
Uber Геолокация, обработка данных
Twitch Система чатов
Docker Весь проект написан на Go
Cloudflare Прокси-серверы, DNS
Go идеально подходит для серверов, микросервисов, CLI-утилит и всего, что требует высокой производительности и надёжности.

Глава 2: Установка Go

Windows

  1. Перейдите на go.dev/dl
  2. Скачайте установщик go1.XX.X.windows-amd64.msi
  3. Запустите установщик и следуйте инструкциям
  4. Перезапустите терминал

macOS

# Через Homebrew (рекомендуется)
brew install go

# Или скачайте .pkg с официального сайта

Linux (Ubuntu/Debian)

# Обновляем пакеты
sudo apt update

# Скачиваем последнюю версию
wget https://go.dev/dl/go1.22.0.linux-amd64.tar.gz

# Распаковываем в /usr/local
sudo tar -C /usr/local -xzf go1.22.0.linux-amd64.tar.gz

# Добавляем в PATH (в ~/.bashrc или ~/.zshrc)
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.bashrc
source ~/.bashrc

Проверка установки

# Проверяем версию
go version
# Вывод: go version go1.22.0 linux/amd64

# Проверяем окружение
go env GOPATH
go env GOROOT

Настройка редактора кода

Рекомендую VS Code с расширением Go:

  1. Установите VS Code
  2. Откройте Extensions (Ctrl+Shift+X)
  3. Найдите "Go" от Go Team at Google
  4. Нажмите Install
  5. VS Code предложит установить Go tools — согласитесь
Также отличные варианты: GoLand (JetBrains), Vim с плагинами, Neovim.

Глава 3: Основы языка

Первая программа

Создайте папку для проекта и файл:

# Создаём папку проекта
mkdir hello-go
cd hello-go

# Инициализируем модуль
go mod init hello-go

# Создаём файл main.go

Содержимое main.go:

package main

import "fmt"

func main() {
    fmt.Println("Привет, Go!")
}

Запуск программы

# Запуск без компиляции
go run main.go

# Компиляция в исполняемый файл
go build -o hello main.go

# Запуск скомпилированного файла
./hello      # Linux/macOS
hello.exe    # Windows

Разбор программы

Код Объяснение
package main Объявляет главный пакет (точка входа)
import "fmt" Импортирует пакет форматированного ввода/вывода
func main() Главная функция, с неё начинается выполнение
fmt.Println() Выводит текст с переносом строки

Переменные

package main

import "fmt"

func main() {
    // Способ 1: явное объявление типа
    var name string = "Алексей"
    var age int = 25

    // Способ 2: автоматическое определение типа
    var city = "Москва"

    // Способ 3: краткая запись (только внутри функций)
    country := "Россия"
    score := 100.5
    isActive := true

    // Множественное объявление
    var (
        x = 10
        y = 20
        z = 30
    )

    // Множественное присваивание
    a, b, c := 1, 2, 3

    fmt.Println(name, age, city, country)
    fmt.Println(score, isActive)
    fmt.Println(x, y, z)
    fmt.Println(a, b, c)
}

Константы

package main

import "fmt"

// Константы на уровне пакета
const Pi = 3.14159
const MaxPlayers = 100

// Группа констант
const (
    StatusPending  = 0
    StatusActive   = 1
    StatusInactive = 2
)

// Автоинкремент с iota
const (
    Sunday = iota  // 0
    Monday         // 1
    Tuesday        // 2
    Wednesday      // 3
    Thursday       // 4
    Friday         // 5
    Saturday       // 6
)

func main() {
    fmt.Println("Pi =", Pi)
    fmt.Println("Понедельник =", Monday)
}
В Go неиспользуемые переменные — это ошибка компиляции! Если объявили переменную, обязательно используйте её.

Условные операторы

package main

import "fmt"

func main() {
    score := 85

    // Простой if
    if score >= 90 {
        fmt.Println("Отлично!")
    } else if score >= 70 {
        fmt.Println("Хорошо")
    } else {
        fmt.Println("Нужно подтянуть")
    }

    // if с инициализацией (переменная видна только внутри if)
    if bonus := score * 2; bonus > 150 {
        fmt.Println("Бонус:", bonus)
    }

    // switch
    day := "Monday"

    switch day {
    case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
        fmt.Println("Рабочий день")
    case "Saturday", "Sunday":
        fmt.Println("Выходной!")
    default:
        fmt.Println("Неизвестный день")
    }

    // switch без выражения (как цепочка if-else)
    temperature := 25

    switch {
    case temperature < 0:
        fmt.Println("Мороз")
    case temperature < 15:
        fmt.Println("Холодно")
    case temperature < 25:
        fmt.Println("Тепло")
    default:
        fmt.Println("Жарко")
    }
}

Циклы

package main

import "fmt"

func main() {
    // В Go есть только for, но он универсален!

    // Классический for
    for i := 0; i < 5; i++ {
        fmt.Println("Итерация:", i)
    }

    // for как while
    count := 0
    for count < 3 {
        fmt.Println("Count:", count)
        count++
    }

    // Бесконечный цикл
    iteration := 0
    for {
        iteration++
        if iteration > 3 {
            break  // Выход из цикла
        }
        fmt.Println("Бесконечный цикл:", iteration)
    }

    // for range для массивов и слайсов
    fruits := []string{"яблоко", "банан", "апельсин"}

    for index, fruit := range fruits {
        fmt.Printf("%d: %s\n", index, fruit)
    }

    // Только значения (индекс игнорируем)
    for _, fruit := range fruits {
        fmt.Println(fruit)
    }

    // Только индексы
    for i := range fruits {
        fmt.Println("Индекс:", i)
    }

    // continue — пропуск итерации
    for i := 0; i < 5; i++ {
        if i == 2 {
            continue
        }
        fmt.Println("i =", i)  // Выведет 0, 1, 3, 4
    }
}
В Go нет while и do-while. Конструкция for покрывает все случаи!

Глава 4: Типы данных

Базовые типы

package main

import "fmt"

func main() {
    // Целые числа
    var i8 int8 = 127           // -128 до 127
    var i16 int16 = 32767       // -32768 до 32767
    var i32 int32 = 2147483647  // ~-2 млрд до ~2 млрд
    var i64 int64 = 9223372036854775807
    var i int = 42              // Зависит от платформы (32 или 64 бит)

    // Беззнаковые целые
    var u8 uint8 = 255          // 0 до 255
    var u16 uint16 = 65535
    var u32 uint32 = 4294967295
    var u64 uint64 = 18446744073709551615

    // Числа с плавающей точкой
    var f32 float32 = 3.14
    var f64 float64 = 3.141592653589793

    // Булевый тип
    var isActive bool = true
    var isDeleted bool = false

    // Строки
    var message string = "Привет, мир!"

    // byte — псевдоним для uint8
    var b byte = 65  // ASCII код 'A'

    // rune — псевдоним для int32 (Unicode code point)
    var r rune = 'Я'  // Unicode символ

    fmt.Println(i8, i16, i32, i64, i)
    fmt.Println(u8, u16, u32, u64)
    fmt.Println(f32, f64)
    fmt.Println(isActive, isDeleted)
    fmt.Println(message)
    fmt.Println(b, string(b))
    fmt.Println(r, string(r))
}

Нулевые значения

package main

import "fmt"

func main() {
    // В Go все переменные инициализируются нулевыми значениями
    var i int       // 0
    var f float64   // 0.0
    var b bool      // false
    var s string    // "" (пустая строка)
    var p *int      // nil

    fmt.Printf("int: %d, float: %f, bool: %t, string: %q, pointer: %v\n", 
        i, f, b, s, p)
}

Массивы

package main

import "fmt"

func main() {
    // Массив — фиксированная длина!
    var numbers [5]int                     // [0 0 0 0 0]
    numbers[0] = 10
    numbers[1] = 20

    // Инициализация при объявлении
    primes := [5]int{2, 3, 5, 7, 11}

    // Автоопределение длины
    colors := [...]string{"red", "green", "blue"}

    fmt.Println(numbers)
    fmt.Println(primes)
    fmt.Println(colors)
    fmt.Println("Длина:", len(colors))
}

Слайсы (срезы)

package main

import "fmt"

func main() {
    // Слайс — динамический массив (самый используемый тип)

    // Создание слайса
    numbers := []int{1, 2, 3, 4, 5}

    // Создание через make
    scores := make([]int, 5)        // длина 5, ёмкость 5
    buffer := make([]byte, 0, 100) // длина 0, ёмкость 100

    // Добавление элементов
    numbers = append(numbers, 6, 7, 8)

    // Срезы от слайса
    first3 := numbers[:3]   // [1 2 3]
    last3 := numbers[5:]    // [6 7 8]
    middle := numbers[2:5]  // [3 4 5]

    // Копирование слайса
    copySlice := make([]int, len(numbers))
    copy(copySlice, numbers)

    fmt.Println("numbers:", numbers)
    fmt.Println("scores:", scores)
    fmt.Println("buffer len/cap:", len(buffer), cap(buffer))
    fmt.Println("first3:", first3)
    fmt.Println("last3:", last3)
    fmt.Println("middle:", middle)
}

Карты (maps)

package main

import "fmt"

func main() {
    // Map — словарь ключ-значение

    // Создание через литерал
    ages := map[string]int{
        "Алексей": 25,
        "Мария":   30,
        "Иван":    35,
    }

    // Создание через make
    scores := make(map[string]int)
    scores["player1"] = 100
    scores["player2"] = 200

    // Получение значения
    alexAge := ages["Алексей"]
    fmt.Println("Возраст Алексея:", alexAge)

    // Проверка существования ключа
    age, exists := ages["Пётр"]
    if exists {
        fmt.Println("Пётр найден:", age)
    } else {
        fmt.Println("Пётр не найден")
    }

    // Удаление ключа
    delete(ages, "Иван")

    // Итерация
    for name, age := range ages {
        fmt.Printf("%s: %d лет\n", name, age)
    }

    // Длина карты
    fmt.Println("Количество записей:", len(ages))
}

Указатели

package main

import "fmt"

func main() {
    // Указатель хранит адрес переменной

    x := 42

    // & — получить адрес
    p := &x

    fmt.Println("Значение x:", x)
    fmt.Println("Адрес x:", p)

    // * — получить значение по адресу (разыменование)
    fmt.Println("Значение по указателю:", *p)

    // Изменение через указатель
    *p = 100
    fmt.Println("Новое значение x:", x)  // 100

    // new — выделяет память и возвращает указатель
    ptr := new(int)
    *ptr = 50
    fmt.Println("new int:", *ptr)
}
В Go нет арифметики указателей, как в C. Это делает код безопаснее.

Глава 5: Функции

Базовые функции

package main

import "fmt"

// Функция без параметров и возврата
func sayHello() {
    fmt.Println("Привет!")
}

// Функция с параметрами
func greet(name string) {
    fmt.Println("Привет,", name)
}

// Функция с возвратом значения
func add(a, b int) int {
    return a + b
}

// Множественный возврат
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("деление на ноль")
    }
    return a / b, nil
}

// Именованные возвращаемые значения
func rectangle(width, height float64) (area, perimeter float64) {
    area = width * height
    perimeter = 2 * (width + height)
    return  // голый return возвращает именованные значения
}

func main() {
    sayHello()
    greet("Мария")

    sum := add(5, 3)
    fmt.Println("5 + 3 =", sum)

    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Ошибка:", err)
    } else {
        fmt.Println("10 / 3 =", result)
    }

    area, perim := rectangle(5, 3)
    fmt.Printf("Площадь: %.2f, Периметр: %.2f\n", area, perim)
}

Вариативные функции

package main

import "fmt"

// Принимает любое количество чисел
func sum(numbers ...int) int {
    total := 0
    for _, n := range numbers {
        total += n
    }
    return total
}

// Комбинация обычных и вариативных параметров
func printAll(prefix string, values ...interface{}) {
    for _, v := range values {
        fmt.Println(prefix, v)
    }
}

func main() {
    fmt.Println(sum(1, 2, 3))           // 6
    fmt.Println(sum(1, 2, 3, 4, 5))     // 15

    // Передача слайса
    nums := []int{10, 20, 30}
    fmt.Println(sum(nums...))         // 60

    printAll(">", "Hello", 42, true)
}

Анонимные функции и замыкания

package main

import "fmt"

func main() {
    // Анонимная функция
    greeting := func(name string) {
        fmt.Println("Привет,", name)
    }
    greeting("Мир")

    // Немедленный вызов
    result := func(a, b int) int {
        return a * b
    }(5, 3)
    fmt.Println("5 * 3 =", result)

    // Замыкание (closure)
    counter := createCounter()
    fmt.Println(counter())  // 1
    fmt.Println(counter())  // 2
    fmt.Println(counter())  // 3
}

// Функция, возвращающая функцию
func createCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

Defer, Panic, Recover

package main

import "fmt"

func main() {
    // defer — отложенное выполнение (LIFO - последний зарегистрированный выполняется первым)
    defer fmt.Println("3. Последний defer")
    defer fmt.Println("2. Второй defer")
    defer fmt.Println("1. Первый defer")

    fmt.Println("0. Основной код")

    // Вывод:
    // 0. Основной код
    // 1. Первый defer
    // 2. Второй defer
    // 3. Последний defer

    // Пример с файлом (типичное использование)
    processFile()

    // Panic и Recover
    safeCall()
    fmt.Println("Программа продолжается")
}

func processFile() {
    fmt.Println("Открываем файл...")
    defer fmt.Println("Закрываем файл")  // Выполнится при выходе из функции

    fmt.Println("Работаем с файлом...")
    // ... какой-то код
}

func safeCall() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Перехвачена паника:", r)
        }
    }()

    fmt.Println("Перед паникой")
    panic("что-то пошло не так!")
    fmt.Println("После паники")  // Не выполнится
}
Используйте panic только для неисправимых ошибок! Для обычных ошибок возвращайте error.

Глава 6: Структуры и методы

Определение структур

package main

import "fmt"

// Определение структуры
type Player struct {
    ID       int
    Username string
    Score    int
    IsOnline bool
    Position Position  // Вложенная структура
}

type Position struct {
    X, Y float64
}

func main() {
    // Создание экземпляра
    player1 := Player{
        ID:       1,
        Username: "Герой",
        Score:    100,
        IsOnline: true,
        Position: Position{X: 10.5, Y: 20.3},
    }

    // Сокращённая форма (по порядку полей)
    player2 := Player{2, "Воин", 50, false, Position{0, 0}}

    // Пустая структура (нулевые значения)
    var player3 Player
    player3.Username = "Новичок"

    // Указатель на структуру
    player4 := &Player{
        ID:       4,
        Username: "Маг",
    }

    // new возвращает указатель
    player5 := new(Player)
    player5.ID = 5

    fmt.Printf("Player1: %+v\n", player1)
    fmt.Printf("Player2: %+v\n", player2)
    fmt.Printf("Player3: %+v\n", player3)
    fmt.Printf("Player4: %+v\n", player4)
    fmt.Printf("Player5: %+v\n", player5)

    // Доступ к полям
    fmt.Println("Имя игрока 1:", player1.Username)
    fmt.Println("Позиция X:", player1.Position.X)

    // Go автоматически разыменовывает указатели
    fmt.Println("Имя игрока 4:", player4.Username)  // Не нужно (*player4).Username
}

Методы

package main

import (
    "fmt"
    "math"
)

type Player struct {
    ID       int
    Username string
    Health   int
    MaxHealth int
    X, Y     float64
}
// Метод с получателем по значению (копия структуры)
func (p Player) GetInfo() string {
    return fmt.Sprintf("%s (HP: %d/%d)", p.Username, p.Health, p.MaxHealth)
}

// Метод с получателем по указателю (может изменять структуру)
func (p *Player) TakeDamage(damage int) {
    p.Health -= damage
    if p.Health < 0 {
        p.Health = 0
    }
    fmt.Printf("%s получил %d урона! HP: %d\n", p.Username, damage, p.Health)
}

func (p *Player) Heal(amount int) {
    p.Health += amount
    if p.Health > p.MaxHealth {
        p.Health = p.MaxHealth
    }
    fmt.Printf("%s восстановил %d HP! HP: %d\n", p.Username, amount, p.Health)
}

func (p *Player) MoveTo(x, y float64) {
    p.X = x
    p.Y = y
}

func (p Player) DistanceTo(other Player) float64 {
    dx := p.X - other.X
    dy := p.Y - other.Y
    return math.Sqrt(dx*dx + dy*dy)
}

func (p Player) IsAlive() bool {
    return p.Health > 0
}

func main() {
    player := &Player{
        ID:        1,
        Username:  "Герой",
        Health:    100,
        MaxHealth: 100,
        X:         0,
        Y:         0,
    }

    enemy := Player{
        ID:        2,
        Username:  "Враг",
        Health:    50,
        MaxHealth: 50,
        X:         10,
        Y:         10,
    }

    fmt.Println(player.GetInfo())

    player.TakeDamage(30)
    player.Heal(20)

    distance := player.DistanceTo(enemy)
    fmt.Printf("Расстояние до врага: %.2f\n", distance)

    player.MoveTo(5, 5)
    fmt.Printf("Новая позиция: (%.1f, %.1f)\n", player.X, player.Y)
}
Используйте указатель в получателе (*Type), если метод изменяет структуру или структура большая (избегаем копирования).

Интерфейсы

package main

import (
    "fmt"
    "math"
)

// Интерфейс — набор методов
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Интерфейс с одним методом
type Drawable interface {
    Draw()
}

// Структура Rectangle
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func (r Rectangle) Draw() {
    fmt.Println("Рисуем прямоугольник")
}

// Структура Circle
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

func (c Circle) Draw() {
    fmt.Println("Рисуем круг")
}

// Функция работает с любой фигурой
func PrintShapeInfo(s Shape) {
    fmt.Printf("Площадь: %.2f, Периметр: %.2f\n", s.Area(), s.Perimeter())
}

// Пустой интерфейс — принимает любой тип
func PrintAnything(v interface{}) {
    fmt.Printf("Значение: %v, Тип: %T\n", v, v)
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    circle := Circle{Radius: 7}

    // Полиморфизм
    shapes := []Shape{rect, circle}

    for _, shape := range shapes {
        PrintShapeInfo(shape)
    }

    // Type assertion (утверждение типа)
    var s Shape = rect

    if r, ok := s.(Rectangle); ok {
        fmt.Println("Это прямоугольник с шириной:", r.Width)
    }

    // Type switch
    switch v := s.(type) {
    case Rectangle:
        fmt.Println("Rectangle:", v.Width, "x", v.Height)
    case Circle:
        fmt.Println("Circle с радиусом:", v.Radius)
    default:
        fmt.Println("Неизвестная фигура")
    }

    // Пустой интерфейс
    PrintAnything(42)
    PrintAnything("hello")
    PrintAnything(rect)
}

Встраивание (композиция)

package main

import "fmt"

// Базовая структура
type Entity struct {
    ID   int
    X, Y float64
}

func (e *Entity) Move(dx, dy float64) {
    e.X += dx
    e.Y += dy
}

func (e Entity) Position() (float64, float64) {
    return e.X, e.Y
}

// Player встраивает Entity
type Player struct {
    Entity              // Встраивание (анонимное поле)
    Username string
    Health   int
}

// Enemy тоже встраивает Entity
type Enemy struct {
    Entity
    Type   string
    Damage int
}

func (e Enemy) Attack() int {
    return e.Damage
}

func main() {
    player := Player{
        Entity:   Entity{ID: 1, X: 0, Y: 0},
        Username: "Герой",
        Health:   100,
    }

    enemy := Enemy{
        Entity: Entity{ID: 2, X: 10, Y: 10},
        Type:   "Гоблин",
        Damage: 15,
    }

    // Методы Entity доступны напрямую
    player.Move(5, 3)
    x, y := player.Position()
    fmt.Printf("%s на позиции (%.1f, %.1f)\n", player.Username, x, y)

    // Поля Entity тоже доступны напрямую
    fmt.Println("ID игрока:", player.ID)

    // Или через имя встроенного типа
    fmt.Println("ID через Entity:", player.Entity.ID)

    enemy.Move(-2, 0)
    fmt.Printf("%s атакует на %d урона!\n", enemy.Type, enemy.Attack())
}

Глава 7: Горутины и каналы

Конкурентность — главная сила Go! Горутины — это лёгкие потоки, управляемые рантаймом Go.

Горутины

package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Println("Привет,", name, "- итерация", i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // Запуск горутины — просто добавляем go
    go sayHello("Алексей")
    go sayHello("Мария")

    // Анонимная горутина
    go func() {
        fmt.Println("Анонимная горутина!")
    }()

    // Главная горутина должна ждать, иначе программа завершится
    time.Sleep(500 * time.Millisecond)
    fmt.Println("Готово!")
}
Не используйте time.Sleep для синхронизации в реальном коде! Это только для демонстрации. Используйте каналы или sync.WaitGroup.

Каналы (Channels)

package main

import "fmt"

func main() {
    // Создание канала
    ch := make(chan string)

    // Отправляем данные в горутине
    go func() {
        ch <- "Привет из горутины!"  // Отправка
    }()

    // Получаем данные (блокирующая операция)
    message := <-ch  // Получение
    fmt.Println(message)

    // Буферизованный канал
    buffered := make(chan int, 3)  // Буфер на 3 элемента

    buffered <- 1
    buffered <- 2
    buffered <- 3
    // buffered <- 4  // Заблокируется — буфер полон

    fmt.Println(<-buffered)  // 1
    fmt.Println(<-buffered)  // 2
    fmt.Println(<-buffered)  // 3
}

Паттерн Worker Pool

package main

import (
    "fmt"
    "time"
)

// Worker обрабатывает задачи из канала jobs
func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d начал задачу %d\n", id, job)
        time.Sleep(100 * time.Millisecond)  // Имитация работы
        results <- job * 2
        fmt.Printf("Worker %d завершил задачу %d\n", id, job)
    }
}

func main() {
    numJobs := 5
    numWorkers := 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // Запускаем воркеров
    for w := 1; w <= numWorkers; w++ {
        go worker(w, jobs, results)
    }

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

    // Собираем результаты
    for r := 1; r <= numJobs; r++ {
        result := <-results
        fmt.Println("Результат:", result)
    }
}

Select — мультиплексирование каналов

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "сообщение 1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "сообщение 2"
    }()

    // select ждёт любой из каналов
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Получено из ch1:", msg1)
        case msg2 := <-ch2:
            fmt.Println("Получено из ch2:", msg2)
        case <-time.After(500 * time.Millisecond):
            fmt.Println("Таймаут!")
        }
    }

    // Non-blocking select с default
    ch := make(chan int, 1)

    select {
    case v := <-ch:
        fmt.Println("Получено:", v)
    default:
        fmt.Println("Канал пуст")
    }
}

sync.WaitGroup

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // Уменьшает счётчик при выходе

    fmt.Printf("Worker %d начал работу\n", id)
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("Worker %d завершил работу\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)  // Увеличиваем счётчик
        go worker(i, &wg)
    }

    wg.Wait()  // Ждём, пока счётчик не станет 0
    fmt.Println("Все воркеры завершили работу!")
}

sync.Mutex — защита данных

package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

func main() {
    counter := &SafeCounter{}
    var wg sync.WaitGroup

    // 1000 горутин одновременно увеличивают счётчик
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }

    wg.Wait()
    fmt.Println("Финальное значение:", counter.Value())  // 1000
}

Глава 8: HTTP-сервер

Простой веб-сервер

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

// Обработчик как функция
func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Добро пожаловать на главную!")
}

// Обработчик с HTML
func htmlHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    html := `
        <!DOCTYPE html>
        <html>
        <head><title>Go сервер</title></head>
        <body>
            <h1>Привет от Go!</h1>
            <p>Это HTML страница</p>
        </body>
        </html>
    `
    fmt.Fprint(w, html)
}

// JSON API
type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
}

func apiUsersHandler(w http.ResponseWriter, r *http.Request) {
    users := []User{
        {ID: 1, Username: "alice", Email: "alice@example.com"},
        {ID: 2, Username: "bob", Email: "bob@example.com"},
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

// Обработчик с параметрами запроса
func searchHandler(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query().Get("q")
    if query == "" {
        http.Error(w, "Параметр q обязателен", http.StatusBadRequest)
        return
    }
    fmt.Fprintf(w, "Поиск: %s", query)
}

// Обработчик POST запроса
func createUserHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Метод не разрешён", http.StatusMethodNotAllowed)
        return
    }

    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Неверный JSON", http.StatusBadRequest)
        return
    }

    user.ID = 123  // Присваиваем ID

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    // Регистрация обработчиков
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/html", htmlHandler)
    http.HandleFunc("/api/users", apiUsersHandler)
    http.HandleFunc("/search", searchHandler)
    http.HandleFunc("/api/users/create", createUserHandler)

    // Статические файлы
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    fmt.Println("Сервер запущен на http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Middleware

package main

import (
    "log"
    "net/http"
    "time"
)

// Middleware для логирования
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // Вызываем следующий обработчик
        next(w, r)

        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    }
}

// Middleware для CORS
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }

        next(w, r)
    }
}

// Цепочка middleware
func chain(handler http.HandlerFunc, middlewares ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc {
    for _, m := range middlewares {
        handler = m(handler)
    }
    return handler
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello!"))
}

func main() {
    // Применяем middleware
    http.HandleFunc("/", chain(helloHandler, loggingMiddleware, corsMiddleware))

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Глава 9: WebSocket

WebSocket нужен для real-time коммуникации в играх. Используем популярную библиотеку gorilla/websocket.

Установка

go get github.com/gorilla/websocket

WebSocket сервер

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    // Разрешаем подключения с любого origin (для разработки)
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
    // Обновляем HTTP соединение до WebSocket
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("Ошибка upgrade:", err)
        return
    }
    defer conn.Close()

    log.Println("Новое WebSocket подключение")

    for {
        // Читаем сообщение
        messageType, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("Ошибка чтения:", err)
            break
        }

        log.Printf("Получено: %s", message)

        // Отправляем эхо-ответ
        response := []byte("Echo: " + string(message))
        if err := conn.WriteMessage(messageType, response); err != nil {
            log.Println("Ошибка записи:", err)
            break
        }
    }
}

func main() {
    http.HandleFunc("/ws", wsHandler)

    log.Println("WebSocket сервер на :8080/ws")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

HTML клиент для тестирования

<!-- test.html -->
<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Test</title>
</head>
<body>
    <h1>WebSocket Test</h1>
    <input type="text" id="message" placeholder="Введите сообщение">
    <button onclick="sendMessage()">Отправить</button>
    <div id="output"></div>

    <script>
        const ws = new WebSocket('ws://localhost:8080/ws');
        const output = document.getElementById('output');

        ws.onopen = () => {
            output.innerHTML += '<p style="color:green">Подключено!</p>';
        };

        ws.onmessage = (event) => {
            output.innerHTML += '<p>Сервер: ' + event.data + '</p>';
        };

        ws.onclose = () => {
            output.innerHTML += '<p style="color:red">Отключено</p>';
        };

        function sendMessage() {
            const msg = document.getElementById('message').value;
            ws.send(msg);
            output.innerHTML += '<p>Вы: ' + msg + '</p>';
        }
    </script>
</body>
</html>

Глава 10: Игровой сервер

Теперь создадим полноценный сервер для браузерной многопользовательской игры!

Структура проекта

game-server/ ├── go.mod ├── go.sum ├── main.go ├── game/ │ ├── player.go │ ├── world.go │ └── messages.go ├── server/ │ ├── hub.go │ └── client.go └── static/ ├── index.html ├── game.js └── style.css

Инициализация проекта

mkdir game-server
cd game-server
go mod init game-server
go get github.com/gorilla/websocket

game/messages.go — Сообщения

package game

// Типы сообщений
const (
    MsgTypeJoin       = "join"
    MsgTypeLeave      = "leave"
    MsgTypeMove       = "move"
    MsgTypeChat       = "chat"
    MsgTypeState      = "state"
    MsgTypePlayerList = "player_list"
    MsgTypeAttack     = "attack"
    MsgTypeDamage     = "damage"
    MsgTypeDeath      = "death"
    MsgTypeRespawn    = "respawn"
)

// Message — базовое сообщение
type Message struct {
    Type    string      `json:"type"`
    Payload interface{} `json:"payload"`
}

// JoinPayload — данные при входе
type JoinPayload struct {
    Username string `json:"username"`
}

// MovePayload — данные движения
type MovePayload struct {
    X float64 `json:"x"`
    Y float64 `json:"y"`
}

// ChatPayload — сообщение чата
type ChatPayload struct {
    PlayerID string `json:"player_id"`
    Username string `json:"username"`
    Text     string `json:"text"`
}

// AttackPayload — данные атаки
type AttackPayload struct {
    TargetID string `json:"target_id"`
}

// DamagePayload — информация об уроне
type DamagePayload struct {
    PlayerID string `json:"player_id"`
    Damage   int    `json:"damage"`
    Health   int    `json:"health"`
}

// StatePayload — состояние игры
type StatePayload struct {
    YourID  string             `json:"your_id"`
    Players map[string]*Player `json:"players"`
}

// PlayerListPayload — список игроков
type PlayerListPayload struct {
    Players map[string]*Player `json:"players"`
}

game/player.go — Игрок

package game

import (
    "math"
    "math/rand"
    "time"
)

const (
    MaxHealth   = 100
    AttackRange = 50.0
    BaseDamage  = 10
    MoveSpeed   = 5.0
)

// Player представляет игрока
type Player struct {
    ID        string  `json:"id"`
    Username  string  `json:"username"`
    X         float64 `json:"x"`
    Y         float64 `json:"y"`
    Health    int     `json:"health"`
    MaxHealth int     `json:"max_health"`
    Score     int     `json:"score"`
    Color     string  `json:"color"`
    IsAlive   bool    `json:"is_alive"`
}

// NewPlayer создаёт нового игрока
func NewPlayer(id, username string) *Player {
    rand.Seed(time.Now().UnixNano())

    return &Player{
        ID:        id,
        Username:  username,
        X:         float64(rand.Intn(700) + 50),
        Y:         float64(rand.Intn(500) + 50),
        Health:    MaxHealth,
        MaxHealth: MaxHealth,
        Score:     0,
        Color:     randomColor(),
        IsAlive:   true,
    }
}

// randomColor генерирует случайный цвет
func randomColor() string {
    colors := []string{
        "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4",
        "#FFEAA7", "#DDA0DD", "#98D8C8", "#F7DC6F",
        "#BB8FCE", "#85C1E9", "#F8B500", "#00CED1",
    }
    return colors[rand.Intn(len(colors))]
}

// MoveTo перемещает игрока
func (p *Player) MoveTo(x, y float64, worldWidth, worldHeight float64) {
    if !p.IsAlive {
        return
    }

    // Ограничиваем координаты в пределах мира
    p.X = math.Max(20, math.Min(worldWidth-20, x))
    p.Y = math.Max(20, math.Min(worldHeight-20, y))
}

// DistanceTo вычисляет расстояние до другого игрока
func (p *Player) DistanceTo(other *Player) float64 {
    dx := p.X - other.X
    dy := p.Y - other.Y
    return math.Sqrt(dx*dx + dy*dy)
}

// CanAttack проверяет, может ли атаковать цель
func (p *Player) CanAttack(target *Player) bool {
    if !p.IsAlive || !target.IsAlive {
        return false
    }
    return p.DistanceTo(target) <= AttackRange
}

// TakeDamage наносит урон игроку
func (p *Player) TakeDamage(damage int) bool {
    if !p.IsAlive {
        return false
    }

    p.Health -= damage
    if p.Health <= 0 {
        p.Health = 0
        p.IsAlive = false
        return true // Игрок умер
    }
    return false
}

// Respawn возрождает игрока
func (p *Player) Respawn(worldWidth, worldHeight float64) {
    p.Health = MaxHealth
    p.IsAlive = true
    p.X = float64(rand.Intn(int(worldWidth)-100)) + 50
    p.Y = float64(rand.Intn(int(worldHeight)-100)) + 50
}

// AddScore добавляет очки
func (p *Player) AddScore(points int) {
    p.Score += points
}

game/world.go — Игровой мир

package game

import (
    "sync"
)

// World представляет игровой мир
type World struct {
    Width   float64
    Height  float64
    Players map[string]*Player
    mu      sync.RWMutex
}

// NewWorld создаёт новый игровой мир
func NewWorld(width, height float64) *World {
    return &World{
        Width:   width,
        Height:  height,
        Players: make(map[string]*Player),
    }
}

// AddPlayer добавляет игрока в мир
func (w *World) AddPlayer(player *Player) {
    w.mu.Lock()
    defer w.mu.Unlock()
    w.Players[player.ID] = player
}

// RemovePlayer удаляет игрока из мира
func (w *World) RemovePlayer(playerID string) {
    w.mu.Lock()
    defer w.mu.Unlock()
    delete(w.Players, playerID)
}

// GetPlayer возвращает игрока по ID
func (w *World) GetPlayer(playerID string) (*Player, bool) {
    w.mu.RLock()
    defer w.mu.RUnlock()
    player, exists := w.Players[playerID]
    return player, exists
}

// GetAllPlayers возвращает копию карты игроков
func (w *World) GetAllPlayers() map[string]*Player {
    w.mu.RLock()
    defer w.mu.RUnlock()

    players := make(map[string]*Player)
    for id, p := range w.Players {
        players[id] = p
    }
    return players
}

// MovePlayer перемещает игрока
func (w *World) MovePlayer(playerID string, x, y float64) bool {
    w.mu.Lock()
    defer w.mu.Unlock()

    player, exists := w.Players[playerID]
    if !exists {
        return false
    }

    player.MoveTo(x, y, w.Width, w.Height)
    return true
}

// ProcessAttack обрабатывает атаку
func (w *World) ProcessAttack(attackerID, targetID string) (damage int, killed bool, err error) {
    w.mu.Lock()
    defer w.mu.Unlock()

    attacker, exists := w.Players[attackerID]
    if !exists {
        return 0, false, nil
    }

    target, exists := w.Players[targetID]
    if !exists {
        return 0, false, nil
    }

    if !attacker.CanAttack(target) {
        return 0, false, nil
    }

    damage = BaseDamage
    killed = target.TakeDamage(damage)

    if killed {
        attacker.AddScore(100)
    }

    return damage, killed, nil
}

// RespawnPlayer возрождает игрока
func (w *World) RespawnPlayer(playerID string) bool {
    w.mu.Lock()
    defer w.mu.Unlock()

    player, exists := w.Players[playerID]
    if !exists {
        return false
    }

    player.Respawn(w.Width, w.Height)
    return true
}

// PlayerCount возвращает количество игроков
func (w *World) PlayerCount() int {
    w.mu.RLock()
    defer w.mu.RUnlock()
    return len(w.Players)
}

server/client.go — WebSocket клиент

package server

import (
    "encoding/json"
    "log"
    "time"

    "game-server/game"

    "github.com/gorilla/websocket"
)

const (
    writeWait      = 10 * time.Second
    pongWait       = 60 * time.Second
    pingPeriod     = (pongWait * 9) / 10
    maxMessageSize = 512
)

// Client представляет WebSocket клиента
type Client struct {
    ID       string
    Hub      *Hub
    Conn     *websocket.Conn
    Send     chan []byte
    Player   *game.Player
}

// NewClient создаёт нового клиента
func NewClient(id string, hub *Hub, conn *websocket.Conn) *Client {
    return &Client{
        ID:   id,
        Hub:  hub,
        Conn: conn,
        Send: make(chan []byte, 256),
    }
}

// ReadPump читает сообщения от клиента
func (c *Client) ReadPump() {
    defer func() {
        c.Hub.Unregister <- c
        c.Conn.Close()
    }()

    c.Conn.SetReadLimit(maxMessageSize)
    c.Conn.SetReadDeadline(time.Now().Add(pongWait))
    c.Conn.SetPongHandler(func(string) error {
        c.Conn.SetReadDeadline(time.Now().Add(pongWait))
        return nil
    })

    for {
        _, message, err := c.Conn.ReadMessage()
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                log.Printf("Ошибка: %v", err)
            }
            break
        }

        c.Hub.ProcessMessage(c, message)
    }
}

// WritePump отправляет сообщения клиенту
func (c *Client) WritePump() {
    ticker := time.NewTicker(pingPeriod)
    defer func() {
        ticker.Stop()
        c.Conn.Close()
    }()

    for {
        select {
        case message, ok := <-c.Send:
            c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
            if !ok {
                c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
                return
            }

            w, err := c.Conn.NextWriter(websocket.TextMessage)
            if err != nil {
                return
            }
            w.Write(message)

            // Добавляем очередь сообщений
            n := len(c.Send)
            for i := 0; i < n; i++ {
                w.Write([]byte{'\n'})
                w.Write(<-c.Send)
            }

            if err := w.Close(); err != nil {
                return
            }

        case <-ticker.C:
            c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
            if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                return
            }
        }
    }
}

// SendJSON отправляет JSON сообщение
func (c *Client) SendJSON(msg interface{}) {
    data, err := json.Marshal(msg)
    if err != nil {
        log.Printf("Ошибка сериализации: %v", err)
        return
    }

    select {
    case c.Send <- data:
    default:
        close(c.Send)
    }
}

server/hub.go — Центр управления

package server

import (
    "encoding/json"
    "log"
    "sync"
    "time"

    "game-server/game"
)

// Hub управляет всеми клиентами и игровым миром
type Hub struct {
    Clients    map[string]*Client
    Register   chan *Client
    Unregister chan *Client
    Broadcast  chan []byte
    World      *game.World
    mu         sync.RWMutex
}

// NewHub создаёт новый Hub
func NewHub() *Hub {
    return &Hub{
        Clients:    make(map[string]*Client),
        Register:   make(chan *Client),
        Unregister: make(chan *Client),
        Broadcast:  make(chan []byte),
        World:      game.NewWorld(800, 600),
    }
}

// Run запускает главный цикл Hub
func (h *Hub) Run() {
    // Тикер для отправки состояния игры
    ticker := time.NewTicker(50 * time.Millisecond) // 20 FPS
    defer ticker.Stop()

    for {
        select {
        case client := <-h.Register:
            h.registerClient(client)

        case client := <-h.Unregister:
            h.unregisterClient(client)

        case message := <-h.Broadcast:
            h.broadcastMessage(message)

        case <-ticker.C:
            h.broadcastGameState()
        }
    }
}

// registerClient регистрирует нового клиента
func (h *Hub) registerClient(client *Client) {
    h.mu.Lock()
    h.Clients[client.ID] = client
    h.mu.Unlock()

    log.Printf("Клиент подключился: %s", client.ID)
}

// unregisterClient удаляет клиента
func (h *Hub) unregisterClient(client *Client) {
    h.mu.Lock()
    if _, ok := h.Clients[client.ID]; ok {
        delete(h.Clients, client.ID)
        close(client.Send)
    }
    h.mu.Unlock()

    // Удаляем игрока из мира
    if client.Player != nil {
        h.World.RemovePlayer(client.ID)

        // Уведомляем остальных
        h.broadcastToAll(game.Message{
            Type: game.MsgTypeLeave,
            Payload: map[string]string{
                "player_id": client.ID,
                "username":  client.Player.Username,
            },
        })
    }

    log.Printf("Клиент отключился: %s", client.ID)
}

// broadcastMessage отправляет сообщение всем клиентам
func (h *Hub) broadcastMessage(message []byte) {
    h.mu.RLock()
    defer h.mu.RUnlock()

    for _, client := range h.Clients {
        select {
        case client.Send <- message:
        default:
            close(client.Send)
            delete(h.Clients, client.ID)
        }
    }
}

// broadcastToAll отправляет JSON всем клиентам
func (h *Hub) broadcastToAll(msg game.Message) {
    data, err := json.Marshal(msg)
    if err != nil {
        log.Printf("Ошибка сериализации: %v", err)
        return
    }
    h.Broadcast <- data
}

// broadcastGameState отправляет состояние игры
func (h *Hub) broadcastGameState() {
    players := h.World.GetAllPlayers()
    if len(players) == 0 {
        return
    }

    msg := game.Message{
        Type: game.MsgTypePlayerList,
        Payload: game.PlayerListPayload{
            Players: players,
        },
    }

    h.broadcastToAll(msg)
}

// ProcessMessage обрабатывает входящее сообщение
func (h *Hub) ProcessMessage(client *Client, data []byte) {
    var msg game.Message
    if err := json.Unmarshal(data, &msg); err != nil {
        log.Printf("Ошибка парсинга сообщения: %v", err)
        return
    }

    switch msg.Type {
    case game.MsgTypeJoin:
        h.handleJoin(client, msg.Payload)

    case game.MsgTypeMove:
        h.handleMove(client, msg.Payload)

    case game.MsgTypeChat:
        h.handleChat(client, msg.Payload)

    case game.MsgTypeAttack:
        h.handleAttack(client, msg.Payload)

    case game.MsgTypeRespawn:
        h.handleRespawn(client)
    }
}

// handleJoin обрабатывает вход игрока
func (h *Hub) handleJoin(client *Client, payload interface{}) {
    data, _ := json.Marshal(payload)
    var joinData game.JoinPayload
    json.Unmarshal(data, &joinData)

    // Создаём игрока
    player := game.NewPlayer(client.ID, joinData.Username)
    client.Player = player
    h.World.AddPlayer(player)

    log.Printf("Игрок присоединился: %s (%s)", player.Username, player.ID)

    // Отправляем клиенту его данные
    client.SendJSON(game.Message{
        Type: game.MsgTypeState,
        Payload: game.StatePayload{
            YourID:  client.ID,
            Players: h.World.GetAllPlayers(),
        },
    })

    // Уведомляем остальных
    h.broadcastToAll(game.Message{
        Type: game.MsgTypeJoin,
        Payload: map[string]interface{}{
            "player": player,
        },
    })
}

// handleMove обрабатывает движение
func (h *Hub) handleMove(client *Client, payload interface{}) {
    if client.Player == nil {
        return
    }

    data, _ := json.Marshal(payload)
    var moveData game.MovePayload
    json.Unmarshal(data, &moveData)

    h.World.MovePlayer(client.ID, moveData.X, moveData.Y)
}

// handleChat обрабатывает сообщение чата
func (h *Hub) handleChat(client *Client, payload interface{}) {
    if client.Player == nil {
        return
    }

    data, _ := json.Marshal(payload)
    var chatData game.ChatPayload
    json.Unmarshal(data, &chatData)

    // Добавляем информацию об отправителе
    chatData.PlayerID = client.ID
    chatData.Username = client.Player.Username

    h.broadcastToAll(game.Message{
        Type:    game.MsgTypeChat,
        Payload: chatData,
    })
}

// handleAttack обрабатывает атаку
func (h *Hub) handleAttack(client *Client, payload interface{}) {
    if client.Player == nil || !client.Player.IsAlive {
        return
    }

    data, _ := json.Marshal(payload)
    var attackData game.AttackPayload
    json.Unmarshal(data, &attackData)

    damage, killed, _ := h.World.ProcessAttack(client.ID, attackData.TargetID)

    if damage > 0 {
        target, _ := h.World.GetPlayer(attackData.TargetID)

        // Уведомляем об уроне
        h.broadcastToAll(game.Message{
            Type: game.MsgTypeDamage,
            Payload: game.DamagePayload{
                PlayerID: attackData.TargetID,
                Damage:   damage,
                Health:   target.Health,
            },
        })

        if killed {
            // Уведомляем о смерти
            h.broadcastToAll(game.Message{
                Type: game.MsgTypeDeath,
                Payload: map[string]string{
                    "player_id": attackData.TargetID,
                    "killer_id": client.ID,
                },
            })
        }
    }
}

// handleRespawn обрабатывает возрождение
func (h *Hub) handleRespawn(client *Client) {
    if client.Player == nil {
        return
    }

    h.World.RespawnPlayer(client.ID)

    h.broadcastToAll(game.Message{
        Type: game.MsgTypeRespawn,
        Payload: map[string]interface{}{
            "player": client.Player,
        },
    })
}

main.go — Точка входа

package main

import (
    "log"
    "net/http"

    "game-server/server"

    "github.com/google/uuid"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

var hub *server.Hub

func wsHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println("Ошибка WebSocket:", err)
        return
    }

    // Генерируем уникальный ID
    clientID := uuid.New().String()[:8]

    client := server.NewClient(clientID, hub, conn)
    hub.Register <- client

    // Запускаем горутины для чтения и записи
    go client.WritePump()
    go client.ReadPump()
}

func main() {
    // Создаём Hub
    hub = server.NewHub()
    go hub.Run()

    // Статические файлы
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/", fs)

    // WebSocket endpoint
    http.HandleFunc("/ws", wsHandler)

    log.Println("🎮 Игровой сервер запущен на http://localhost:8080")
    log.Println("📡 WebSocket: ws://localhost:8080/ws")

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal("Ошибка сервера:", err)
    }
}

Установка зависимостей

go get github.com/gorilla/websocket
go get github.com/google/uuid

static/index.html — Клиент игры

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Браузерная игра на Go</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="login-screen">
        <div class="login-box">
            <h1>🎮 Go Arena</h1>
            <input type="text" id="username" placeholder="Введите имя" maxlength="15">
            <button id="join-btn">Войти в игру</button>
        </div>
    </div>

    <div id="game-screen" style="display: none;">
        <div id="game-container">
            <canvas id="game-canvas" width="800" height="600"></canvas>
            <div id="ui-overlay">
                <div id="player-info">
                    <span id="player-name"></span>
                    <div id="health-bar">
                        <div id="health-fill"></div>
                    </div>
                    <span id="score">Очки: 0</span>
                </div>
                <div id="players-online">Игроков: 0</div>
            </div>
        </div>

        <div id="chat-container">
            <div id="chat-messages"></div>
            <div id="chat-input-container">
                <input type="text" id="chat-input" placeholder="Сообщение...">
                <button id="chat-send">→</button>
            </div>
        </div>

        <div id="respawn-screen" style="display: none;">
            <div class="respawn-box">
                <h2>💀 Вы погибли!</h2>
                <button id="respawn-btn">Возродиться</button>
            </div>
        </div>
    </div>

    <div id="controls-help">
        <p>🖱️ Клик — движение | ⌨️ Пробел — атака | 💬 Enter — чат</p>
    </div>

    <script src="game.js"></script>
</body>
</html>

static/style.css — Стили

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
    min-height: 100vh;
    color: white;
    overflow: hidden;
}

/* Login Screen */
#login-screen {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.login-box {
    background: rgba(255, 255, 255, 0.1);
    padding: 40px;
    border-radius: 20px;
    text-align: center;
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.2);
}

.login-box h1 {
    font-size: 2.5em;
    margin-bottom: 30px;
    background: linear-gradient(90deg, #667eea, #764ba2);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

.login-box input {
    width: 100%;
    padding: 15px;
    border: none;
    border-radius: 10px;
    font-size: 16px;
    margin-bottom: 20px;
    background: rgba(255, 255, 255, 0.9);
    color: #333;
}

.login-box button {
    width: 100%;
    padding: 15px 30px;
    border: none;
    border-radius: 10px;
    font-size: 18px;
    cursor: pointer;
    background: linear-gradient(90deg, #667eea, #764ba2);
    color: white;
    transition: transform 0.3s, box-shadow 0.3s;
}

.login-box button:hover {
    transform: translateY(-2px);
    box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}

/* Game Screen */
#game-screen {
    display: flex;
    height: 100vh;
    padding: 20px;
    gap: 20px;
}

#game-container {
    position: relative;
    flex: 1;
}

#game-canvas {
    background: linear-gradient(180deg, #0f0f23 0%, #1a1a3e 100%);
    border-radius: 15px;
    border: 3px solid #667eea;
    box-shadow: 0 0 30px rgba(102, 126, 234, 0.3);
}

#ui-overlay {
    position: absolute;
    top: 10px;
    left: 10px;
    right: 10px;
    display: flex;
    justify-content: space-between;
    pointer-events: none;
}

#player-info {
    background: rgba(0, 0, 0, 0.7);
    padding: 10px 20px;
    border-radius: 10px;
}

#player-name {
    font-weight: bold;
    font-size: 1.1em;
}

#health-bar {
    width: 150px;
    height: 15px;
    background: #333;
    border-radius: 10px;
    margin: 5px 0;
    overflow: hidden;
}

#health-fill {
    height: 100%;
    background: linear-gradient(90deg, #ff6b6b, #ee5a5a);
    border-radius: 10px;
    transition: width 0.3s;
    width: 100%;
}

#score {
    font-size: 0.9em;
    color: #ffd700;
}

#players-online {
    background: rgba(0, 0, 0, 0.7);
    padding: 10px 20px;
    border-radius: 10px;
}

/* Chat */
#chat-container {
    width: 300px;
    background: rgba(0, 0, 0, 0.5);
    border-radius: 15px;
    display: flex;
    flex-direction: column;
    backdrop-filter: blur(10px);
}

#chat-messages {
    flex: 1;
    padding: 15px;
    overflow-y: auto;
    max-height: calc(100vh - 120px);
}

.chat-message {
    margin-bottom: 10px;
    padding: 8px 12px;
    background: rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    word-wrap: break-word;
}

.chat-message .username {
    font-weight: bold;
    color: #667eea;
}

.chat-message.system {
    color: #888;
    font-style: italic;
    background: transparent;
}

#chat-input-container {
    display: flex;
    padding: 15px;
    gap: 10px;
}

#chat-input {
    flex: 1;
    padding: 10px;
    border: none;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.9);
    color: #333;
}

#chat-send {
    padding: 10px 20px;
    border: none;
    border-radius: 8px;
    background: #667eea;
    color: white;
    cursor: pointer;
    font-size: 18px;
}

/* Respawn Screen */
#respawn-screen {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.8);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 1000;
}

.respawn-box {
    text-align: center;
    background: rgba(255, 255, 255, 0.1);
    padding: 40px;
    border-radius: 20px;
    backdrop-filter: blur(10px);
}

.respawn-box h2 {
    font-size: 2em;
    margin-bottom: 20px;
    color: #ff6b6b;
}

#respawn-btn {
    padding: 15px 40px;
    border: none;
    border-radius: 10px;
    font-size: 18px;
    cursor: pointer;
    background: linear-gradient(90deg, #4ecdc4, #45b7d1);
    color: white;
}

/* Controls Help */
#controls-help {
    position: fixed;
    bottom: 10px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(0, 0, 0, 0.7);
    padding: 10px 20px;
    border-radius: 20px;
    font-size: 14px;
}

/* Scrollbar */
::-webkit-scrollbar {
    width: 8px;
}

::-webkit-scrollbar-track {
    background: rgba(0, 0, 0, 0.3);
    border-radius: 4px;
}

::-webkit-scrollbar-thumb {
    background: #667eea;
    border-radius: 4px;
}

static/game.js — Игровая логика клиента

// Игровой клиент
class GameClient {
    constructor() {
        this.ws = null;
        this.playerId = null;
        this.players = {};
        this.canvas = document.getElementById('game-canvas');
        this.ctx = this.canvas.getContext('2d');
        this.isConnected = false;

        this.init();
    }

    init() {
        // Элементы UI
        this.loginScreen = document.getElementById('login-screen');
        this.gameScreen = document.getElementById('game-screen');
        this.respawnScreen = document.getElementById('respawn-screen');
        this.usernameInput = document.getElementById('username');
        this.joinBtn = document.getElementById('join-btn');
        this.chatInput = document.getElementById('chat-input');
        this.chatSend = document.getElementById('chat-send');
        this.chatMessages = document.getElementById('chat-messages');
        this.respawnBtn = document.getElementById('respawn-btn');

        // События
        this.joinBtn.addEventListener('click', () => this.join());
        this.usernameInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') this.join();
        });

        this.chatSend.addEventListener('click', () => this.sendChat());
        this.chatInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') this.sendChat();
        });

        this.canvas.addEventListener('click', (e) => this.handleClick(e));
        document.addEventListener('keydown', (e) => this.handleKeydown(e));

        this.respawnBtn.addEventListener('click', () => this.respawn());
    }

    join() {
        const username = this.usernameInput.value.trim();
        if (!username) {
            alert('Введите имя!');
            return;
        }

        this.username = username;
        this.connect();
    }

    connect() {
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        this.ws = new WebSocket(`${protocol}//${window.location.host}/ws`);

        this.ws.onopen = () => {
            console.log('Подключено к серверу');
            this.isConnected = true;

            // Отправляем запрос на вход
            this.send({
                type: 'join',
                payload: { username: this.username }
            });

            // Показываем игровой экран
            this.loginScreen.style.display = 'none';
            this.gameScreen.style.display = 'flex';

            // Запускаем игровой цикл
            this.gameLoop();
        };

        this.ws.onmessage = (event) => {
            const messages = event.data.split('\n');
            messages.forEach(msg => {
                if (msg.trim()) {
                    try {
                        const data = JSON.parse(msg);
                        this.handleMessage(data);
                    } catch (e) {
                        console.error('Ошибка парсинга:', e);
                    }
                }
            });
        };

        this.ws.onclose = () => {
            console.log('Отключено от сервера');
            this.isConnected = false;
            this.addSystemMessage('Соединение потеряно');
        };

        this.ws.onerror = (error) => {
            console.error('Ошибка WebSocket:', error);
        };
    }

    send(data) {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify(data));
        }
    }

    handleMessage(data) {
        switch (data.type) {
            case 'state':
                this.playerId = data.payload.your_id;
                this.players = data.payload.players;
                this.updateUI();
                break;

            case 'player_list':
                this.players = data.payload.players;
                this.updateUI();
                break;

            case 'join':
                const newPlayer = data.payload.player;
                this.players[newPlayer.id] = newPlayer;
                this.addSystemMessage(`${newPlayer.username} присоединился к игре`);
                break;

            case 'leave':
                const leftId = data.payload.player_id;
                const leftName = data.payload.username;
                delete this.players[leftId];
                this.addSystemMessage(`${leftName} покинул игру`);
                break;

            case 'chat':
                this.addChatMessage(data.payload.username, data.payload.text);
                break;

            case 'damage':
                // Визуальный эффект урона можно добавить здесь
                break;

            case 'death':
                if (data.payload.player_id === this.playerId) {
                    this.respawnScreen.style.display = 'flex';
                }
                this.addSystemMessage(`${this.getPlayerName(data.payload.player_id)} убит игроком ${this.getPlayerName(data.payload.killer_id)}`);
                break;

            case 'respawn':
                if (data.payload.player.id === this.playerId) {
                    this.respawnScreen.style.display = 'none';
                }
                break;
        }
    }

    getPlayerName(id) {
        return this.players[id]?.username || 'Неизвестный';
    }

    handleClick(e) {
        if (!this.isConnected || !this.playerId) return;

        const rect = this.canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        this.send({
            type: 'move',
            payload: { x, y }
        });
    }

    handleKeydown(e) {
        if (!this.isConnected) return;

        // Пробел — атака
        if (e.code === 'Space' && document.activeElement !== this.chatInput) {
            e.preventDefault();
            this.attack();
        }

        // Enter — фокус на чат
        if (e.code === 'Enter' && document.activeElement !== this.chatInput) {
            this.chatInput.focus();
        }
    }

    attack() {
        const myPlayer = this.players[this.playerId];
        if (!myPlayer || !myPlayer.is_alive) return;

        // Находим ближайшего игрока
        let closestId = null;
        let closestDist = Infinity;

        for (const [id, player] of Object.entries(this.players)) {
            if (id === this.playerId || !player.is_alive) continue;

            const dist = Math.hypot(myPlayer.x - player.x,myPlayer.y - player.y);
            if (dist < closestDist && dist <= 50) {
                closestDist = dist;
                closestId = id;
            }
        }

        if (closestId) {
            this.send({
                type: 'attack',
                payload: { target_id: closestId }
            });
        }
    }

    respawn() {
        this.send({ type: 'respawn', payload: {} });
    }

    sendChat() {
        const text = this.chatInput.value.trim();
        if (!text) return;

        this.send({
            type: 'chat',
            payload: { text }
        });

        this.chatInput.value = '';
    }

    addChatMessage(username, text) {
        const div = document.createElement('div');
        div.className = 'chat-message';
        div.innerHTML = `${this.escapeHtml(username)}: ${this.escapeHtml(text)}`;
        this.chatMessages.appendChild(div);
        this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
    }

    addSystemMessage(text) {
        const div = document.createElement('div');
        div.className = 'chat-message system';
        div.textContent = text;
        this.chatMessages.appendChild(div);
        this.chatMessages.scrollTop = this.chatMessages.scrollHeight;
    }

    escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    updateUI() {
        const myPlayer = this.players[this.playerId];
        if (!myPlayer) return;

        // Имя игрока
        document.getElementById('player-name').textContent = myPlayer.username;

        // Здоровье
        const healthPercent = (myPlayer.health / myPlayer.max_health) * 100;
        document.getElementById('health-fill').style.width = `${healthPercent}%`;

        // Очки
        document.getElementById('score').textContent = `Очки: ${myPlayer.score}`;

        // Количество игроков
        document.getElementById('players-online').textContent = `Игроков: ${Object.keys(this.players).length}`;
    }

    gameLoop() {
        this.render();
        requestAnimationFrame(() => this.gameLoop());
    }

    render() {
        const ctx = this.ctx;
        const canvas = this.canvas;

        // Очищаем canvas
        ctx.fillStyle = '#0f0f23';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Рисуем сетку
        this.drawGrid();

        // Рисуем всех игроков
        for (const [id, player] of Object.entries(this.players)) {
            this.drawPlayer(player, id === this.playerId);
        }
    }

    drawGrid() {
        const ctx = this.ctx;
        ctx.strokeStyle = 'rgba(102, 126, 234, 0.1)';
        ctx.lineWidth = 1;

        const gridSize = 50;

        for (let x = 0; x <= this.canvas.width; x += gridSize) {
            ctx.beginPath();
            ctx.moveTo(x, 0);
            ctx.lineTo(x, this.canvas.height);
            ctx.stroke();
        }

        for (let y = 0; y <= this.canvas.height; y += gridSize) {
            ctx.beginPath();
            ctx.moveTo(0, y);
            ctx.lineTo(this.canvas.width, y);
            ctx.stroke();
        }
    }

    drawPlayer(player, isMe) {
        const ctx = this.ctx;
        const { x, y, username, health, max_health, color, is_alive } = player;

        if (!is_alive) {
            // Рисуем мёртвого игрока (полупрозрачный)
            ctx.globalAlpha = 0.3;
        }

        // Радиус атаки (только для текущего игрока)
        if (isMe && is_alive) {
            ctx.beginPath();
            ctx.arc(x, y, 50, 0, Math.PI * 2);
            ctx.strokeStyle = 'rgba(255, 107, 107, 0.2)';
            ctx.lineWidth = 2;
            ctx.stroke();
        }

        // Тело игрока
        ctx.beginPath();
        ctx.arc(x, y, 20, 0, Math.PI * 2);
        ctx.fillStyle = color;
        ctx.fill();

        // Обводка для текущего игрока
        if (isMe) {
            ctx.strokeStyle = '#fff';
            ctx.lineWidth = 3;
            ctx.stroke();
        }

        // Полоска здоровья
        const healthBarWidth = 40;
        const healthBarHeight = 6;
        const healthPercent = health / max_health;

        ctx.fillStyle = '#333';
        ctx.fillRect(x - healthBarWidth / 2, y - 35, healthBarWidth, healthBarHeight);

        ctx.fillStyle = healthPercent > 0.5 ? '#4ecdc4' : healthPercent > 0.25 ? '#ffd700' : '#ff6b6b';
        ctx.fillRect(x - healthBarWidth / 2, y - 35, healthBarWidth * healthPercent, healthBarHeight);

        // Имя игрока
        ctx.fillStyle = '#fff';
        ctx.font = 'bold 12px Arial';
        ctx.textAlign = 'center';
        ctx.fillText(username, x, y - 42);

        ctx.globalAlpha = 1;
    }
}

// Запуск игры
const game = new GameClient();

Запуск игрового сервера

# Переходим в папку проекта
cd game-server

# Устанавливаем зависимости
go mod tidy

# Запускаем сервер
go run main.go

# Вывод:
# 🎮 Игровой сервер запущен на http://localhost:8080
# 📡 WebSocket: ws://localhost:8080/ws
Откройте http://localhost:8080 в нескольких вкладках браузера, чтобы проверить многопользовательский режим!

Глава 11: Тестирование

Go имеет встроенную поддержку тестирования. Файлы тестов заканчиваются на _test.go.

Базовые тесты

// game/player_test.go
package game

import "testing"

func TestNewPlayer(t *testing.T) {
    player := NewPlayer("test-id", "TestUser")

    if player.ID != "test-id" {
        t.Errorf("Ожидался ID 'test-id', получен '%s'", player.ID)
    }

    if player.Username != "TestUser" {
        t.Errorf("Ожидалось имя 'TestUser', получено '%s'", player.Username)
    }

    if player.Health != MaxHealth {
        t.Errorf("Ожидалось здоровье %d, получено %d", MaxHealth, player.Health)
    }

    if !player.IsAlive {
        t.Error("Новый игрок должен быть живым")
    }
}

func TestTakeDamage(t *testing.T) {
    player := NewPlayer("test", "Test")

    // Обычный урон
    killed := player.TakeDamage(30)
    if killed {
        t.Error("Игрок не должен умереть от 30 урона")
    }
    if player.Health != 70 {
        t.Errorf("Ожидалось 70 HP, получено %d", player.Health)
    }

    // Смертельный урон
    killed = player.TakeDamage(100)
    if !killed {
        t.Error("Игрок должен умереть от 100 урона")
    }
    if player.IsAlive {
        t.Error("Игрок должен быть мёртв")
    }
    if player.Health != 0 {
        t.Errorf("HP должно быть 0, получено %d", player.Health)
    }
}

func TestDistanceTo(t *testing.T) {
    p1 := &Player{X: 0, Y: 0}
    p2 := &Player{X: 3, Y: 4DistanceTo(p2)
    expected := 5.0

    if distance != expected {
        t.Errorf("Ожидалось расстояние %.2f, получено %.2f", expected, distance)
    }
}

func TestCanAttack(t *testing.T) {
    attacker := NewPlayer("1", "Attacker")
    attacker.X = 0
    attacker.Y = 0

    // В пределах досягаемости
    target1 := NewPlayer("2", "Target1")
    target1.X = 30
    target1.Y = 0

    if !attacker.CanAttack(target1) {
        t.Error("Должен иметь возможность атаковать цель на расстоянии 30")
    }

    // Вне досягаемости
    target2 := NewPlayer("3", "Target2")
    target2.X = 100
    target2.Y = 0

    if attacker.CanAttack(target2) {
        t.Error("Не должен атаковать цель на расстоянии 100")
    }

    // Мёртвая цель
    target1.IsAlive = false
    if attacker.CanAttack(target1) {
        t.Error("Не должен атаковать мёртвую цель")
    }
}

func TestRespawn(t *testing.T) {
    player := NewPlayer("test", "Test")
    player.TakeDamage(200) // Убиваем

    if player.IsAlive {
        t.Error("Игрок должен быть мёртв")
    }

    player.Respawn(800, 600)

    if !player.IsAlive {
        t.Error("Игрок должен быть жив после возрождения")
    }
    if player.Health != MaxHealth {
        t.Errorf("HP должно быть %d после возрождения", MaxHealth)
    }
}

Тесты для World

// game/world_test.go
package game

import (
    "sync"
    "testing"
)

func TestWorldAddRemovePlayer(t *testing.T) {
    world := NewWorld(800, 600)
    player := NewPlayer("test-id", "TestUser")

    world.AddPlayer(player)

    if world.PlayerCount() != 1 {
        t.Errorf("Ожидался 1 игрок, получено %d", world.PlayerCount())
    }

    retrieved, exists := world.GetPlayer("test-id")
    if !exists {
        t.Error("Игрок должен существовать")
    }
    if retrieved.Username != "TestUser" {
        t.Error("Неверное имя игрока")
    }

    world.RemovePlayer("test-id")

    if world.PlayerCount() != 0 {
        t.Error("Игрок должен быть удалён")
    }
}

func TestWorldMovePlayer(t *testing.T) {
    world := NewWorld(800, 600)
    player := NewPlayer("test", "Test")
    world.AddPlayer(player)

    world.MovePlayer("test", 100, 200)

    p, _ := world.GetPlayer("test")
    if p.X != 100 || p.Y != 200 {
        t.Errorf("Ожидалась позиция (100, 200), получена (%.0f, %.0f)", p.X, p.Y)
    }

    // Проверка границ
    world.MovePlayer("test", -100, -100)
    p, _ = world.GetPlayer("test")
    if p.X < 20 || p.Y < 20 {
        t.Error("Игрок вышел за границы мира")
    }
}

func TestWorldProcessAttack(t *testing.T) {
    world := NewWorld(800, 600)

    attacker := NewPlayer("attacker", "Attacker")
    attacker.X = 0
    attacker.Y = 0

    target := NewPlayer("target", "Target")
    target.X = 30
    target.Y = 0

    world.AddPlayer(attacker)
    world.AddPlayer(target)

    damage, killed, err := world.ProcessAttack("attacker", "target")

    if err != nil {
        t.Errorf("Неожиданная ошибка: %v", err)
    }
    if damage != BaseDamage {
        t.Errorf("Ожидался урон %d, получен %d", BaseDamage, damage)
    }
    if killed {
        t.Error("Цель не должна умереть от одной атаки")
    }

    // Проверяем HP цели
    t2, _ := world.GetPlayer("target")
    expectedHP := MaxHealth - BaseDamage
    if t2.Health != expectedHP {
        t.Errorf("Ожидалось HP %d, получено %d", expectedHP, t2.Health)
    }
}

// Тест конкурентного доступа
func TestWorldConcurrency(t *testing.T) {
    world := NewWorld(800, 600)
    var wg sync.WaitGroup

    // Одновременно добавляем 100 игроков
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            player := NewPlayer(
                fmt.Sprintf("player-%d", id),
                fmt.Sprintf("User%d", id),
            )
            world.AddPlayer(player)
        }(i)
    }

    wg.Wait()

    if world.PlayerCount() != 100 {
        t.Errorf("Ожидалось 100 игроков, получено %d", world.PlayerCount())
    }
}

// Нужен импорт fmt для TestWorldConcurrency
import "fmt"

Табличные тесты

// game/player_table_test.go
package game

import "testing"

func TestTakeDamageTable(t *testing.T) {
    tests := []struct {
        name           string
        initialHealth  int
        damage         int
        expectedHealth int
        shouldDie      bool
    }{
        {
            name:           "обычный урон",
            initialHealth:  100,
            damage:         30,
            expectedHealth: 70,
            shouldDie:      false,
        },
        {
            name:           "смертельный урон",
            initialHealth:  100,
            damage:         100,
            expectedHealth: 0,
            shouldDie:      true,
        },
        {
            name:           "overkill",
            initialHealth:  50,
            damage:         200,
            expectedHealth: 0,
            shouldDie:      true,
        },
        {
            name:           "нулевой урон",
            initialHealth:  100,
            damage:         0,
            expectedHealth: 100,
            shouldDie:      false,
        },
        {
            name:           "ровно смертельный",
            initialHealth:  50,
            damage:         50,
            expectedHealth: 0,
            shouldDie:      true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            player := NewPlayer("test", "Test")
            player.Health = tt.initialHealth

            died := player.TakeDamage(tt.damage)

            if died != tt.shouldDie {
                t.Errorf("shouldDie: ожидалось %v, получено %v", tt.shouldDie, died)
            }

            if player.Health != tt.expectedHealth {
                t.Errorf("Health: ожидалось %d, получено %d", tt.expectedHealth, player.Health)
            }
        })
    }
}

Бенчмарки

// game/benchmark_test.go
package game

import (
    "fmt"
    "testing"
)

func BenchmarkNewPlayer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        NewPlayer("test-id", "TestUser")
    }
}

func BenchmarkDistanceTo(b *testing.B) {
    p1 := &Player{X: 0, Y: 0}
    p2 := &Player{X: 100, Y: 100}

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        p1.DistanceTo(p2)
    }
}

func BenchmarkWorldAddPlayer(b *testing.B) {
    world := NewWorld(800, 600)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        player := NewPlayer(fmt.Sprintf("id-%d", i), "User")
        world.AddPlayer(player)
    }
}

func BenchmarkWorldGetAllPlayers(b *testing.B) {
    world := NewWorld(800, 600)

    // Добавляем 100 игроков
    for i := 0; i < 100; i++ {
        player := NewPlayer(fmt.Sprintf("id-%d", i), "User")
        world.AddPlayer(player)
    }

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        world.GetAllPlayers()
    }
}

Запуск тестов

# Запуск всех тестов
go test ./...

# С подробным выводом
go test -v ./...

# Тесты конкретного пакета
go test -v ./game

# Конкретный тест
go test -v -run TestNewPlayer ./game

# С покрытием кода
go test -cover ./...

# Детальный отчёт о покрытии
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# Запуск бенчмарков
go test -bench=. ./game

# Бенчмарки с информацией о памяти
go test -bench=. -benchmem ./game

Пример вывода тестов

=== RUN   TestNewPlayer
--- PASS: TestNewPlayer (0.00s)
=== RUN   TestTakeDamage
--- PASS: TestTakeDamage (0.00s)
=== RUN   TestDistanceTo
--- PASS: TestDistanceTo (0.00s)
=== RUN   TestCanAttack
--- PASS: TestCanAttack (0.00s)
=== RUN   TestTakeDamageTable
=== RUN   TestTakeDamageTable/обычный_урон
=== RUN   TestTakeDamageTable/смертельный_урон
=== RUN   TestTakeDamageTable/overkill
=== RUN   TestTakeDamageTable/нулевой_урон
=== RUN   TestTakeDamageTable/ровно_смертельный
--- PASS: TestTakeDamageTable (0.00s)
PASS
coverage: 85.7% of statements
ok      game-server/game    0.003s

HTTP тесты

// server/http_test.go
package server

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHealthEndpoint(t *testing.T) {
    // Создаём тестовый обработчик
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"status":"ok"}`))
    })

    // Создаём тестовый запрос
    req := httptest.NewRequest("GET", "/health", nil)

    // Создаём recorder для записи ответа
    rr := httptest.NewRecorder()

    // Вызываем обработчик
    handler.ServeHTTP(rr, req)

    // Проверяем статус
    if rr.Code != http.StatusOK {
        t.Errorf("Ожидался статус %d, получен %d", http.StatusOK, rr.Code)
    }

    // Проверяем тело ответа
    expected := `{"status":"ok"}`
    if rr.Body.String() != expected {
        t.Errorf("Ожидалось '%s', получено '%s'", expected, rr.Body.String())
    }
}
Используйте go test -race ./... для обнаружения гонок данных (race conditions).

🎉 Заключение

Поздравляю! Вы прошли путь от установки Go до создания полноценного игрового сервера!

Что мы изучили:

  • ✅ Установка и настройка Go
  • ✅ Основы синтаксиса: переменные, условия, циклы
  • ✅ Типы данных: массивы, слайсы, карты, указатели
  • ✅ Функции и методы
  • ✅ Структуры и интерфейсы
  • ✅ Горутины и каналы
  • ✅ HTTP-сервер
  • ✅ WebSocket для real-time коммуникации
  • ✅ Создание игрового сервера
  • ✅ Тестирование и бенчмарки

Идеи для развития проекта:

  • 🎯 Добавить разные типы оружия
  • 🗺️ Создать систему комнат/уровней
  • 💾 Подключить базу данных для сохранения прогресса
  • 🔐 Добавить аутентификацию
  • 📊 Реализовать таблицу лидеров
  • 🎨 Улучшить графику (спрайты, анимации)
  • 🤖 Добавить ботов
  • ☁️ Развернуть на сервере (Docker, Kubernetes)

Полезные ресурсы:

Весь код из этого руководства доступен для копирования и экспериментов. Лучший способ учиться — писать код и разбираться в ошибках!

📋 Шпаргалка по командам Go

Команда Описание
go run main.go Запуск программы
go build Компиляция
go mod init <name> Инициализация модуля
go mod tidy Синхронизация зависимостей
go get <package> Установка пакета
go test ./... Запуск тестов
go test -bench=. Бенчмарки
go test -cover Покрытие кода
go fmt ./... Форматирование кода
go vet ./... Статический анализ
go doc <package> Документация
go env Переменные окружения