🚀 Go с нуля
Полное руководство: от установки до создания игрового сервера
📑 Оглавление
Глава 1: Введение в Go
Что такое Go?
Go (или Golang) — это компилируемый язык программирования, созданный в Google в 2009 году. Его разработали Роб Пайк, Кен Томпсон и Роберт Гризмер — легенды программирования, создавшие UNIX и UTF-8.
Почему Go?
- Простота — синтаксис минималистичен, легко учить
- Скорость — компилируется в машинный код
- Конкурентность — горутины делают параллельное программирование простым
- Надёжность — строгая типизация, сборка мусора
- Кроссплатформенность — один код для Windows, Linux, macOS
Где используется Go?
| Компания | Применение |
|---|---|
| Kubernetes, YouTube | |
| Uber | Геолокация, обработка данных |
| Twitch | Система чатов |
| Docker | Весь проект написан на Go |
| Cloudflare | Прокси-серверы, DNS |
Глава 2: Установка Go
Windows
- Перейдите на go.dev/dl
- Скачайте установщик
go1.XX.X.windows-amd64.msi - Запустите установщик и следуйте инструкциям
- Перезапустите терминал
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:
- Установите VS Code
- Откройте Extensions (Ctrl+Shift+X)
- Найдите "Go" от Go Team at Google
- Нажмите Install
- VS Code предложит установить Go tools — согласитесь
Глава 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)
}
Условные операторы
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
}
}
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)
}
Глава 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)
}
Интерфейсы
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("Готово!")
}
Каналы (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: Игровой сервер
Теперь создадим полноценный сервер для браузерной многопользовательской игры!
Структура проекта
Инициализация проекта
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
Глава 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
- A Tour of Go — интерактивный курс
- Go by Example
- Go Wiki
- pkg.go.dev — документация пакетов
📋 Шпаргалка по командам 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 |
Переменные окружения |