Почему 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, рано или поздно встает вопрос: "А сколько горутин можно