Go 1.22 (Февраль 2024)
Go 1.22 принёс два долгожданных изменения: значительно улучшенный стандартный HTTP-роутер и исправление одного из самых распространённых багов Go -- захвата переменных цикла.
Enhanced ServeMux -- полноценный HTTP-роутер в стандартной библиотеке
До Go 1.22 стандартный http.ServeMux был примитивным: он поддерживал только точные пути и префиксы, без методов и параметров. Это заставляло использовать сторонние роутеры (gorilla/mux, chi, gin).
Go 1.22 полностью переработал ServeMux, добавив:
Паттерны с HTTP-методами
mux := http.NewServeMux()
// Method-specific patterns
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// Without method: matches any method
mux.HandleFunc("/health", healthCheck)
Path parameters
func getUser(w http.ResponseWriter, r *http.Request) {
// Extract path parameter
id := r.PathValue("id")
user, err := db.FindUser(r.Context(), id)
if err != nil {
http.Error(w, "user not found", http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(user)
}
Wildcard (catch-all)
// {path...} matches everything including slashes
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
filePath := r.PathValue("path")
// filePath = "docs/2024/report.pdf" for /files/docs/2024/report.pdf
http.ServeFile(w, r, filepath.Join(staticDir, filePath))
})
Правила приоритетов
mux.HandleFunc("GET /posts/{id}", getPost) // Specific parameter
mux.HandleFunc("GET /posts/latest", getLatest) // Exact match wins!
mux.HandleFunc("GET /posts/{id}/comments", getComments)
mux.HandleFunc("GET /{path...}", catchAll) // Lowest priority
// Request: GET /posts/latest -> getLatest (exact > parameter)
// Request: GET /posts/42 -> getPost
// Request: GET /posts/42/comments -> getComments
// Request: GET /anything/else -> catchAll
Правила приоритетов:
- Exact match побеждает wildcard
- Более длинный путь побеждает короткий
- Более специфичный паттерн побеждает менее специфичный
- Если два паттерна конфликтуют -- паника при регистрации
Host-based routing
mux.HandleFunc("GET api.example.com/v1/users", apiListUsers)
mux.HandleFunc("GET www.example.com/", webIndex)
Полный пример: REST API на стандартном роутере
package main
import (
"encoding/json"
"log/slog"
"net/http"
"sync"
"time"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type UserStore struct {
mu sync.RWMutex
users map[string]*User
}
func NewUserStore() *UserStore {
return &UserStore{users: make(map[string]*User)}
}
// Middleware: logging
func withLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"duration", time.Since(start),
)
})
}
// Middleware: JSON content type
func withJSON(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next(w, r)
}
}
func main() {
store := NewUserStore()
mux := http.NewServeMux()
// RESTful routes with the new enhanced patterns
mux.HandleFunc("GET /api/v1/users", withJSON(store.List))
mux.HandleFunc("POST /api/v1/users", withJSON(store.Create))
mux.HandleFunc("GET /api/v1/users/{id}", withJSON(store.Get))
mux.HandleFunc("PUT /api/v1/users/{id}", withJSON(store.Update))
mux.HandleFunc("DELETE /api/v1/users/{id}", withJSON(store.Delete))
// Health check
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// Apply global middleware
handler := withLogging(mux)
slog.Info("server starting", "addr", ":8080")
if err := http.ListenAndServe(":8080", handler); err != nil {
slog.Error("server error", "error", err)
}
}
func (s *UserStore) List(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]*User, 0, len(s.users))
for _, u := range s.users {
users = append(users, u)
}
json.NewEncoder(w).Encode(users)
}
func (s *UserStore) Get(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
s.mu.RLock()
user, ok := s.users[id]
s.mu.RUnlock()
if !ok {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(user)
}
func (s *UserStore) Create(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
return
}
user.ID = generateID()
user.CreatedAt = time.Now()
s.mu.Lock()
s.users[user.ID] = &user
s.mu.Unlock()
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func (s *UserStore) Update(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
s.mu.Lock()
defer s.mu.Unlock()
existing, ok := s.users[id]
if !ok {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
var update User
if err := json.NewDecoder(r.Body).Decode(&update); err != nil {
http.Error(w, `{"error":"invalid JSON"}`, http.StatusBadRequest)
return
}
existing.Name = update.Name
existing.Email = update.Email
json.NewEncoder(w).Encode(existing)
}
func (s *UserStore) Delete(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[id]; !ok {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
delete(s.users, id)
w.WriteHeader(http.StatusNoContent)
}
Сравнение: стандартный роутер vs сторонние
| Возможность | ServeMux (< 1.22) | ServeMux (1.22+) | chi / gorilla/mux |
|---|---|---|---|
| Методы | Нет | Да | Да |
| Параметры | Нет | {id} |
{id} |
| Wildcard | Только / |
{path...} |
* |
| Middleware | Нет | Обёртки | Встроенные |
| Группы | Нет | Нет | Да |
| Regex в путях | Нет | Нет | Да (gorilla) |
| Зависимости | 0 | 0 | 1+ |
Вывод: Для большинства API стандартный роутер Go 1.22+ достаточен. Сторонние роутеры нужны для middleware chaining, route groups и regex в путях.
Range over integer
// БЫЛО
for i := 0; i < 10; i++ {
fmt.Println(i)
}
// СТАЛО (Go 1.22+)
for i := range 10 {
fmt.Println(i) // 0, 1, 2, ..., 9
}
// Practical: repeat N times without using the variable
for range 5 {
fmt.Println("retry...")
}
// Works with any integer expression
n := getRetryCount()
for i := range n {
if err := tryOperation(); err == nil {
break
}
time.Sleep(time.Duration(i) * time.Second)
}
Исправление захвата переменных цикла
Это исправление -- одно из самых важных за всю историю Go. Оно устраняет класс багов, который мучил программистов более 10 лет.
Проблема (до Go 1.22)
// BUG: all goroutines see the same value!
func processItems(items []string) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func() {
defer wg.Done()
process(item) // BUG: captures loop variable by reference
}()
}
wg.Wait()
// All goroutines process the LAST item!
}
Обходные пути (до Go 1.22)
// Workaround 1: shadow variable
for _, item := range items {
item := item // Create new variable for each iteration
go func() {
process(item)
}()
}
// Workaround 2: pass as argument
for _, item := range items {
go func(it string) {
process(it)
}(item)
}
Решение (Go 1.22+)
// JUST WORKS in Go 1.22+
// Each iteration creates a new variable
for _, item := range items {
go func() {
process(item) // Safe! Each goroutine has its own copy
}()
}
// Same fix for index variable
for i := range items {
go func() {
fmt.Println(i) // Each goroutine sees a different i
}()
}
// Also fixed for 3-clause loops
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i) // Was broken before 1.22, now correct
}()
}
Влияние на существующий код
// Old code with "item := item" still works fine
for _, item := range items {
item := item // Unnecessary in Go 1.22+, but harmless
go func() {
process(item)
}()
}
// go vet will suggest removing the redundant reassignment
math/rand/v2
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
// No need to seed! Auto-seeded in v2
fmt.Println(rand.IntN(100)) // Random int in [0, 100)
fmt.Println(rand.Float64()) // Random float in [0.0, 1.0)
// Generic N function
fmt.Println(rand.N[int64](1000)) // Random int64 in [0, 1000)
fmt.Println(rand.N[uint32](256)) // Random uint32 in [0, 256)
// New distribution-specific functions
fmt.Println(rand.NormFloat64()) // Standard normal distribution
fmt.Println(rand.ExpFloat64()) // Exponential distribution
// Shuffle
s := []string{"a", "b", "c", "d", "e"}
rand.Shuffle(len(s), func(i, j int) {
s[i], s[j] = s[j], s[i]
})
}
| math/rand (v1) | math/rand/v2 | Изменение |
|---|---|---|
rand.Seed(time.Now().UnixNano()) |
Не нужно | Автоматический seed |
rand.Intn(n) |
rand.IntN(n) |
Переименовано |
rand.Int63n(n) |
rand.N[int64](n) |
Дженерик |
rand.Read(b) не crypto-safe |
Убран | Используйте crypto/rand |
| Глобальный мьютекс | Без мьютекса (per-goroutine) | Быстрее |
slices.Concat
a := []int{1, 2, 3}
b := []int{4, 5}
c := []int{6, 7, 8, 9}
result := slices.Concat(a, b, c)
fmt.Println(result) // [1 2 3 4 5 6 7 8 9]
Go 1.23 (Август 2024)
Go 1.23 стабилизировал одну из самых мощных фич -- range-over-function (итераторы) и добавил пакет unique для интернирования значений.
Range-over-function (Итераторы) -- стабильная версия
Итераторы позволяют использовать for range с пользовательскими функциями, создавая единый паттерн обхода любых коллекций.
Типы итераторов
import "iter"
// Single value iterator
type Seq[V any] func(yield func(V) bool)
// Key-value iterator
type Seq2[K, V any] func(yield func(K, V) bool)
Создание итераторов
package main
import (
"fmt"
"iter"
)
// Simple iterator: generate Fibonacci numbers
func Fibonacci() iter.Seq[int] {
return func(yield func(int) bool) {
a, b := 0, 1
for {
if !yield(a) {
return // Consumer stopped iteration
}
a, b = b, a+b
}
}
}
func main() {
for n := range Fibonacci() {
if n > 100 {
break // Triggers yield returning false
}
fmt.Println(n) // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
}
}
Итератор для пользовательской коллекции
// Binary search tree with iteration
type BST[T cmp.Ordered] struct {
root *node[T]
}
type node[T cmp.Ordered] struct {
value T
left, right *node[T]
}
// InOrder returns an iterator that yields values in sorted order
func (t *BST[T]) InOrder() iter.Seq[T] {
return func(yield func(T) bool) {
t.inorder(t.root, yield)
}
}
func (t *BST[T]) inorder(n *node[T], yield func(T) bool) bool {
if n == nil {
return true
}
return t.inorder(n.left, yield) &&
yield(n.value) &&
t.inorder(n.right, yield)
}
// Usage: natural iteration syntax
func main() {
tree := NewBST[int]()
tree.Insert(5, 3, 7, 1, 4, 6, 8)
for val := range tree.InOrder() {
fmt.Println(val) // 1, 3, 4, 5, 6, 7, 8
}
}
Seq2: итератор с ключом и значением
// Iterate over map in sorted key order
func SortedMap[K cmp.Ordered, V any](m map[K]V) iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
keys := make([]K, 0, len(m))
for k := range m {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
if !yield(k, m[k]) {
return
}
}
}
}
func main() {
scores := map[string]int{
"Alice": 95,
"Bob": 87,
"Carol": 92,
}
for name, score := range SortedMap(scores) {
fmt.Printf("%s: %d\n", name, score)
}
// Alice: 95
// Bob: 87
// Carol: 92
}
Composable iterators (цепочки итераторов)
// Filter: yield only elements matching predicate
func Filter[V any](seq iter.Seq[V], pred func(V) bool) iter.Seq[V] {
return func(yield func(V) bool) {
for v := range seq {
if pred(v) {
if !yield(v) {
return
}
}
}
}
}
// Map: transform each element
func Map[V, U any](seq iter.Seq[V], f func(V) U) iter.Seq[U] {
return func(yield func(U) bool) {
for v := range seq {
if !yield(f(v)) {
return
}
}
}
}
// Take: yield at most n elements
func Take[V any](seq iter.Seq[V], n int) iter.Seq[V] {
return func(yield func(V) bool) {
count := 0
for v := range seq {
if count >= n {
return
}
if !yield(v) {
return
}
count++
}
}
}
// Usage: pipeline composition
func main() {
// First 5 even Fibonacci numbers
evens := Filter(Fibonacci(), func(n int) bool { return n%2 == 0 })
first5 := Take(evens, 5)
for n := range first5 {
fmt.Println(n) // 0, 2, 8, 34, 144
}
}
Pull iterators: iter.Pull, iter.Pull2
Pull-итераторы преобразуют push-based итератор в pull-based (вызов next()):
func main() {
// Convert push iterator to pull iterator
next, stop := iter.Pull(Fibonacci())
defer stop() // IMPORTANT: always stop to prevent goroutine leak
// Pull values one at a time
for range 10 {
val, ok := next()
if !ok {
break
}
fmt.Println(val)
}
}
Итераторы в стандартной библиотеке
Go 1.23 добавил итераторы во многие пакеты:
import (
"maps"
"slices"
"strings"
)
// slices.All: iterate with index
for i, v := range slices.All([]string{"a", "b", "c"}) {
fmt.Printf("%d: %s\n", i, v)
}
// slices.Values: iterate values only
for v := range slices.Values([]int{1, 2, 3}) {
fmt.Println(v)
}
// slices.Backward: reverse iteration
for i, v := range slices.Backward([]int{1, 2, 3}) {
fmt.Printf("%d: %d\n", i, v) // 2:3, 1:2, 0:1
}
// maps.Keys, maps.Values return iterators
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k := range maps.Keys(m) {
fmt.Println(k)
}
// Collect iterator into slice
keys := slices.Sorted(maps.Keys(m))
fmt.Println(keys) // [a b c]
// strings.Lines: iterate over lines
text := "line1\nline2\nline3\n"
for line := range strings.Lines(text) {
fmt.Print(line)
}
Пакет unique -- интернирование значений
unique позволяет создавать канонические значения, чтобы одинаковые значения занимали одну ячейку памяти.
package main
import (
"fmt"
"unique"
)
func main() {
// Make creates a canonical handle for a value
h1 := unique.Make("hello")
h2 := unique.Make("hello")
h3 := unique.Make("world")
// Handles with equal values are identical (pointer comparison)
fmt.Println(h1 == h2) // true (same canonical value)
fmt.Println(h1 == h3) // false
// Get the value back
fmt.Println(h1.Value()) // "hello"
// Use case: efficient deduplication and comparison
type Request struct {
Method unique.Handle[string]
Host unique.Handle[string]
}
get := unique.Make("GET")
post := unique.Make("POST")
req := Request{Method: get, Host: unique.Make("example.com")}
// Comparing handles is pointer comparison -- O(1), not O(n)
if req.Method == get {
fmt.Println("GET request")
}
}
Когда использовать unique
| Сценарий | Выгода |
|---|---|
| Много одинаковых строк (HTTP headers, status codes) | Экономия памяти |
| Частые сравнения | O(1) вместо O(n) |
| Map ключи с длинными строками | Меньше потребление памяти |
| Кеширование | Автоматическая дедупликация |
Изменения Timer/Ticker
В Go 1.23 неиспользуемые Timer и Ticker собираются сборщиком мусора. Раньше для предотвращения утечки нужно было вызывать Stop().
// БЫЛО (до Go 1.23): утечка если не вызвать Stop
timer := time.NewTimer(time.Second)
// Forgot timer.Stop() -> goroutine leak!
// СТАЛО (Go 1.23+): GC collected automatically
timer := time.NewTimer(time.Second)
// If timer goes out of scope, it's garbage collected
// No leak even without Stop()
// But Stop() is still recommended for deterministic behavior
os.CopyFS
import (
"embed"
"os"
)
//go:embed templates
var templateFS embed.FS
func extractTemplates(destDir string) error {
// Copy entire embedded filesystem to disk
return os.CopyFS(destDir, templateFS)
}
Сводная таблица Go 1.22 -- 1.23
| Фича | Версия | Влияние | Действие |
|---|---|---|---|
| Enhanced ServeMux | 1.22 | Критическое | Рассмотреть отказ от chi/gorilla для новых проектов |
| Range-over-int | 1.22 | Среднее | Упростить циклы for i := 0; i < n; i++ |
| Loop variable fix | 1.22 | Критическое | Убрать v := v из циклов |
| math/rand/v2 | 1.22 | Среднее | Перейти на v2, убрать Seed() |
| Range-over-func | 1.23 | Высокое | Реализовать итераторы для коллекций |
| iter.Pull | 1.23 | Среднее | Для pull-based потребления |
| unique package | 1.23 | Среднее | Интернирование частых строк |
| Timer/Ticker GC | 1.23 | Низкое | Меньше забот об утечках таймеров |
| os.CopyFS | 1.23 | Низкое | Извлечение embed.FS на диск |