Пишем сетевой сервер на Go: от TCP до HTTP за один вечер

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему Go — идеальный язык для сетевого программирования?

Если вы когда-нибудь писали сетевой сервер на Python (особенно с использованием socket или asyncio), вы знаете, как быстро код может превратиться в лабиринт коллбэков и событийных циклов. Go (Golang) решает эту проблему на архитектурном уровне: горутины и каналы делают асинхронную работу с сетью такой же простой, как написание последовательного кода.

В этой статье мы разберем сетевое программирование на Go на практике. Вы узнаете, как за 15 минут собрать полноценный TCP-чат, отправить UDP-пакет и запустить HTTP-сервер с маршрутизацией. Все примеры — рабочие, их можно скопировать и запустить.

Основы: пакет net и его суперсила

В Go вся сетевая магия живет в стандартном пакете net. Он предоставляет унифицированный интерфейс для работы с TCP, UDP, Unix-сокетами и даже SSL/TLS. Главная фишка — функции Listen, Dial и Accept работают синхронно, но не блокируют всю программу, потому что каждая горутина легковесна.

Давайте сразу перейдем к делу.

TCP-сервер: чат на 10 строк

Создадим простой эхо-сервер, который принимает подключения и отправляет обратно то, что получил. Это классика сетевого программирования на Go.

package main

import (
    "bufio"
    "fmt"
    "net"
)

func handleConnection(conn net.Conn) {
    defer conn.Close()
    scanner := bufio.NewScanner(conn)
    for scanner.Scan() {
        message := scanner.Text()
        fmt.Printf("Получено: %s\n", message)
        conn.Write([]byte("Эхо: " + message + "\n"))
    }
}

func main() {
    listener, _ := net.Listen("tcp", ":8080")
    fmt.Println("TCP-сервер запущен на :8080")
    for {
        conn, _ := listener.Accept()
        go handleConnection(conn) // каждая горутина — отдельный клиент
    }
}

Что здесь происходит?

  • net.Listen("tcp", ":8080") — создает TCP-слушатель на порту 8080.
  • Бесконечный цикл for принимает новые соединения через Accept().
  • Каждое соединение обрабатывается в отдельной горутине go handleConnection().
  • Внутри функции мы читаем строки через bufio.Scanner и отправляем ответ.

Запустите этот код, откройте терминал и подключитесь через nc localhost 8080 (или Putty). Любое сообщение вернется с префиксом "Эхо:". Удивительно, но это уже полноценный сетевой сервер, способный обслуживать тысячи клиентов без единого коллбэка.

UDP: быстрая доставка без гарантий

Если TCP похож на телефонный разговор (установили соединение — общаемся), то UDP — это как почтовые открытки: отправили и забыли. Go делает работу с UDP такой же простой.

UDP-клиент и сервер

// Сервер
package main

import (
    "fmt"
    "net"
)

func main() {
    addr, _ := net.ResolveUDPAddr("udp", ":8081")
    conn, _ := net.ListenUDP("udp", addr)
    defer conn.Close()

    buffer := make([]byte, 1024)
    for {
        n, clientAddr, _ := conn.ReadFromUDP(buffer)
        message := string(buffer[:n])
        fmt.Printf("Получено от %s: %s\n", clientAddr, message)
        conn.WriteToUDP([]byte("Принято: "+message+"\n"), clientAddr)
    }
}

// Клиент
// В отдельном файле или горутине:
conn, _ := net.Dial("udp", "localhost:8081")
defer conn.Close()
conn.Write([]byte("Привет, UDP!"))
buffer := make([]byte, 1024)
n, _ := conn.Read(buffer)
fmt.Println(string(buffer[:n]))

Ключевое отличие от TCP:

  • Нет Accept() — мы просто слушаем порт и читаем датаграммы.
  • Каждый пакет приходит со своим адресом отправителя.
  • Go сам управляет буферизацией: ReadFromUDP читает ровно один пакет.

UDP идеален для стриминга, игр и IoT, где скорость важнее надежности. Но помните: пакеты могут теряться, дублироваться или приходить не в том порядке.

HTTP-сервер: от нуля до REST API

Теперь самое интересное — веб-сервер. В Go HTTP — это буквально "из коробки". Пакет net/http настолько продуман, что многие компании (например, Docker, Kubernetes) используют его для своих API.

Простейший HTTP-сервер

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Привет, %s!", r.URL.Path[1:])
}

func main() {
    http.HandleFunc("/", helloHandler)
    fmt.Println("HTTP-сервер на :8082")
    http.ListenAndServe(":8082", nil)
}

Этот код обрабатывает любой запрос вида http://localhost:8082/Вася и отвечает "Привет, Вася!".

Но настоящая мощь раскрывается, когда мы добавляем маршрутизацию и middleware.

REST API с JSON

package main

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

type Task struct {
    ID   int    `json:"id"`
    Text string `json:"text"`
}

var tasks = []Task{{1, "Изучить Go"}, {2, "Написать статью"}}

func getTasks(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(tasks)
}

func createTask(w http.ResponseWriter, r *http.Request) {
    var newTask Task
    json.NewDecoder(r.Body).Decode(&newTask)
    newTask.ID = len(tasks) + 1
    tasks = append(tasks, newTask)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(newTask)
}

func main() {
    http.HandleFunc("/tasks", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case "GET":
            getTasks(w, r)
        case "POST":
            createTask(w, r)
        default:
            http.Error(w, "Метод не поддерживается", http.StatusMethodNotAllowed)
        }
    })
    log.Fatal(http.ListenAndServe(":8083", nil))
}

Теперь у нас есть полноценный REST API для задач. Можно тестировать через curl:

curl http://localhost:8083/tasks
curl -X POST -d '{"text":"Новая задача"}' http://localhost:8083/tasks

Продвинутые техники: горутины и пулы соединений

Когда вы пишете сетевое программирование на Go, рано или поздно встает вопрос: "А сколько горутин можно

Блоги

Рекомендаций по книгам