MidТеория8 min

Время и криптография

Пакеты time, crypto/sha256, crypto/rand, math/rand/v2 и шифрование

Пакет time -- один из самых используемых в Go: от измерения производительности до планирования задач. Криптографические пакеты обеспечивают хеширование, HMAC, шифрование и генерацию случайных чисел.

time.Time -- моменты времени

import "time"

// Current time
now := time.Now()
fmt.Println(now) // 2025-06-15 10:30:45.123456789 +0300 MSK

// Components
year := now.Year()       // 2025
month := now.Month()     // June (time.Month type)
day := now.Day()         // 15
hour := now.Hour()       // 10
minute := now.Minute()   // 30
second := now.Second()   // 45
nano := now.Nanosecond() // 123456789
weekday := now.Weekday() // Sunday (time.Weekday type)

// Comparison
t1 := time.Now()
t2 := t1.Add(time.Hour)
t1.Before(t2)  // true
t2.After(t1)   // true
t1.Equal(t1)   // true

// Zero value check
var t time.Time
t.IsZero()     // true

time.Since() и time.Until()

// Measure elapsed time
start := time.Now()
doWork()
elapsed := time.Since(start) // Shortcut for time.Now().Sub(start)
fmt.Printf("Took %v\n", elapsed)

// Time until deadline
deadline := time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC)
remaining := time.Until(deadline) // Shortcut for deadline.Sub(time.Now())
fmt.Printf("Days remaining: %.0f\n", remaining.Hours()/24)

time.Duration -- длительности

// Duration constants
time.Nanosecond   // 1
time.Microsecond  // 1000 nanoseconds
time.Millisecond  // 1000 microseconds
time.Second       // 1000 milliseconds
time.Minute       // 60 seconds
time.Hour         // 60 minutes

// Creating durations
d := 5 * time.Second          // 5s
d = 2*time.Hour + 30*time.Minute // 2h30m0s

// Parse duration from string
d, err := time.ParseDuration("1h30m45s")   // 1h30m45s
d, err = time.ParseDuration("500ms")        // 500ms
d, err = time.ParseDuration("2.5s")         // 2.5s

// Duration methods
d = 90 * time.Second
d.Hours()       // 0.025
d.Minutes()     // 1.5
d.Seconds()     // 90
d.Milliseconds() // 90000
d.String()      // "1m30s"

// Arithmetic
t := time.Now()
later := t.Add(2 * time.Hour)      // Add duration
diff := later.Sub(t)               // Get duration between times

time.Parse и time.Format -- эталонное время

Go использует уникальный подход к форматированию: эталонное время вместо символов-заполнителей.

Эталонное время: Mon Jan 2 15:04:05 MST 2006

Это время выбрано специально: 01/02 03:04:05PM '06 -0700

// Parsing
t, err := time.Parse("2006-01-02", "2025-06-15")
t, err = time.Parse("2006-01-02 15:04:05", "2025-06-15 10:30:00")
t, err = time.Parse(time.RFC3339, "2025-06-15T10:30:00Z")

// Formatting
now := time.Now()
s := now.Format("2006-01-02")                // "2025-06-15"
s = now.Format("02.01.2006 15:04")           // "15.06.2025 10:30"
s = now.Format("Monday, January 2, 2006")    // "Sunday, June 15, 2025"
s = now.Format(time.RFC3339)                  // "2025-06-15T10:30:00+03:00"
s = now.Format(time.RFC822)                   // "15 Jun 25 10:30 MSK"

// Predefined layouts
time.RFC3339     // "2006-01-02T15:04:05Z07:00"
time.RFC3339Nano // "2006-01-02T15:04:05.999999999Z07:00"
time.DateTime    // "2006-01-02 15:04:05" (Go 1.20+)
time.DateOnly    // "2006-01-02" (Go 1.20+)
time.TimeOnly    // "15:04:05" (Go 1.20+)

Распространённые форматы

Формат Шаблон Go Пример
ISO 8601 "2006-01-02T15:04:05Z07:00" 2025-06-15T10:30:00+03:00
Дата "2006-01-02" 2025-06-15
Время "15:04:05" 10:30:00
Русский "02.01.2006" 15.06.2025
Лог "2006/01/02 15:04:05" 2025/06/15 10:30:00
12h "3:04 PM" 10:30 AM

Таймеры и тикеры

time.After и time.Sleep

// Sleep -- block for duration
time.Sleep(2 * time.Second)

// After -- channel that fires once after duration
select {
case <-time.After(5 * time.Second):
    fmt.Println("Timeout!")
case result := <-resultCh:
    fmt.Println("Got result:", result)
}

time.NewTimer -- однократный таймер

timer := time.NewTimer(5 * time.Second)
defer timer.Stop() // Always stop when done!

select {
case <-timer.C:
    fmt.Println("Timer fired")
case <-ctx.Done():
    fmt.Println("Cancelled")
    if !timer.Stop() {
        <-timer.C // Drain the channel
    }
}

// Reset timer
timer.Reset(10 * time.Second)

time.NewTicker -- периодический тикер

ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // ALWAYS stop tickers to avoid goroutine leak!

for {
    select {
    case t := <-ticker.C:
        fmt.Println("Tick at", t)
    case <-ctx.Done():
        fmt.Println("Done")
        return
    }
}

time.AfterFunc -- отложенный вызов

// Execute function after delay in a new goroutine
timer := time.AfterFunc(5*time.Second, func() {
    fmt.Println("Executed after 5 seconds")
})

// Cancel if needed
timer.Stop()

Часовые пояса

// UTC
utcTime := time.Now().UTC()

// Local timezone
localTime := time.Now() // Uses system timezone

// Load specific timezone
loc, err := time.LoadLocation("Europe/Moscow")
if err != nil {
    log.Fatal(err)
}
moscowTime := time.Now().In(loc)

// Fixed offset timezone
msk := time.FixedZone("MSK", 3*60*60) // UTC+3
t := time.Now().In(msk)

// Convert between timezones
nyLoc, _ := time.LoadLocation("America/New_York")
nyTime := moscowTime.In(nyLoc)

// Parse with timezone
t, err = time.ParseInLocation("2006-01-02 15:04", "2025-06-15 10:30", loc)

Правило: Храните время в UTC, конвертируйте в локальное только для отображения.

Монотонные часы

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

start := time.Now() // Contains BOTH wall clock AND monotonic clock

// Even if system clock changes (NTP sync, DST, etc.),
// Sub/Since use monotonic component
time.Sleep(time.Second)
elapsed := time.Since(start) // Uses monotonic clock -- always accurate

// Strip monotonic reading
wallOnly := start.Round(0) // Now only wall clock

Монотонные часы гарантируют корректные измерения времени даже при синхронизации NTP или смене часового пояса.

crypto/sha256 и crypto/sha512

import (
    "crypto/sha256"
    "crypto/sha512"
    "encoding/hex"
)

// SHA-256
data := []byte("Hello, World!")
hash := sha256.Sum256(data)
fmt.Println(hex.EncodeToString(hash[:])) // 64-char hex string

// SHA-256 with streaming (for large data)
h := sha256.New()
h.Write([]byte("Hello, "))
h.Write([]byte("World!"))
hash2 := h.Sum(nil)
fmt.Println(hex.EncodeToString(hash2))

// SHA-512
hash512 := sha512.Sum512(data)
fmt.Println(hex.EncodeToString(hash512[:])) // 128-char hex string

// Hash a file
func hashFile(path string) (string, error) {
    f, err := os.Open(path)
    if err != nil {
        return "", fmt.Errorf("opening file: %w", err)
    }
    defer f.Close()

    h := sha256.New()
    if _, err := io.Copy(h, f); err != nil {
        return "", fmt.Errorf("hashing: %w", err)
    }

    return hex.EncodeToString(h.Sum(nil)), nil
}

crypto/hmac -- подпись сообщений

HMAC (Hash-based Message Authentication Code) проверяет целостность и аутентичность данных:

import "crypto/hmac"

// Create HMAC
func signMessage(message, secret []byte) []byte {
    mac := hmac.New(sha256.New, secret)
    mac.Write(message)
    return mac.Sum(nil)
}

// Verify HMAC (constant-time comparison!)
func verifyMessage(message, secret, signature []byte) bool {
    expected := signMessage(message, secret)
    return hmac.Equal(signature, expected) // Constant-time comparison
}

// Example: webhook verification
func verifyWebhook(body []byte, signature string, secret string) bool {
    sig, err := hex.DecodeString(signature)
    if err != nil {
        return false
    }
    return verifyMessage(body, []byte(secret), sig)
}

Критично: Всегда используйте hmac.Equal() вместо bytes.Equal() для сравнения подписей. hmac.Equal работает за постоянное время, предотвращая timing-атаки.

crypto/rand -- криптографически безопасные числа

import "crypto/rand"

// Random bytes
buf := make([]byte, 32)
_, err := rand.Read(buf) // Fills buf with random bytes
if err != nil {
    log.Fatal(err) // Should never happen on modern systems
}

// Random UUID (v4)
func generateUUID() string {
    uuid := make([]byte, 16)
    rand.Read(uuid)
    uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
    uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant 10
    return fmt.Sprintf("%x-%x-%x-%x-%x",
        uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:])
}

// Random token (URL-safe)
func generateToken(length int) (string, error) {
    bytes := make([]byte, length)
    if _, err := rand.Read(bytes); err != nil {
        return "", err
    }
    return base64.URLEncoding.EncodeToString(bytes), nil
}

math/rand/v2 -- псевдослучайные числа (Go 1.22+)

math/rand/v2 -- обновлённый пакет для некриптографических случайных чисел:

import "math/rand/v2"

// Top-level functions (auto-seeded since Go 1.20)
n := rand.IntN(100)          // [0, 100) -- random int
f := rand.Float64()          // [0.0, 1.0) -- random float
n = rand.N(100)              // Same as IntN but generic (Go 1.22+)

// Shuffle
items := []string{"a", "b", "c", "d", "e"}
rand.Shuffle(len(items), func(i, j int) {
    items[i], items[j] = items[j], items[i]
})

// Custom source for reproducible results
src := rand.NewPCG(42, 0) // PCG source with seed 42
rng := rand.New(src)
rng.IntN(100)       // Always same sequence for same seed

// ChaCha8 source (cryptographic quality but faster than crypto/rand)
src2 := rand.NewChaCha8([32]byte{1, 2, 3}) // Seed
rng2 := rand.New(src2)

Правило: crypto/rand для токенов, ключей, пароли, сессии. math/rand/v2 для тестов, шаффлы, случайные задержки, игры.

crypto/tls -- TLS-соединения

import "crypto/tls"

// HTTPS server
cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
if err != nil {
    log.Fatal(err)
}

tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{cert},
    MinVersion:   tls.VersionTLS12,
}

server := &http.Server{
    Addr:      ":443",
    TLSConfig: tlsConfig,
}

log.Fatal(server.ListenAndServeTLS("", ""))

// TLS client
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            MinVersion: tls.VersionTLS12,
        },
    },
}

crypto/aes и cipher.AEAD -- симметричное шифрование

AES-GCM -- рекомендуемый алгоритм для симметричного шифрования:

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
)

// Encrypt with AES-GCM
func encrypt(key, plaintext []byte) ([]byte, error) {
    block, err := aes.NewCipher(key) // key must be 16, 24, or 32 bytes
    if err != nil {
        return nil, fmt.Errorf("creating cipher: %w", err)
    }

    aead, err := cipher.NewGCM(block)
    if err != nil {
        return nil, fmt.Errorf("creating GCM: %w", err)
    }

    // Generate random nonce
    nonce := make([]byte, aead.NonceSize())
    if _, err := rand.Read(nonce); err != nil {
        return nil, fmt.Errorf("generating nonce: %w", err)
    }

    // Encrypt: nonce is prepended to ciphertext
    return aead.Seal(nonce, nonce, plaintext, nil), nil
}

// Decrypt with AES-GCM
func decrypt(key, ciphertext []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, fmt.Errorf("creating cipher: %w", err)
    }

    aead, err := cipher.NewGCM(block)
    if err != nil {
        return nil, fmt.Errorf("creating GCM: %w", err)
    }

    nonceSize := aead.NonceSize()
    if len(ciphertext) < nonceSize {
        return nil, errors.New("ciphertext too short")
    }

    nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
    return aead.Open(nil, nonce, ciphertext, nil)
}

// Usage
func main() {
    key := make([]byte, 32) // AES-256
    rand.Read(key)

    encrypted, err := encrypt(key, []byte("Secret message"))
    if err != nil {
        log.Fatal(err)
    }

    decrypted, err := decrypt(key, encrypted)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(decrypted)) // "Secret message"
}

Практический пример: подписанные токены

type Token struct {
    UserID    string    `json:"user_id"`
    ExpiresAt time.Time `json:"expires_at"`
}

func createToken(userID string, secret []byte, ttl time.Duration) (string, error) {
    token := Token{
        UserID:    userID,
        ExpiresAt: time.Now().Add(ttl),
    }

    payload, err := json.Marshal(token)
    if err != nil {
        return "", fmt.Errorf("marshaling token: %w", err)
    }

    // Sign with HMAC
    mac := hmac.New(sha256.New, secret)
    mac.Write(payload)
    signature := mac.Sum(nil)

    // Combine: base64(payload).base64(signature)
    return base64.URLEncoding.EncodeToString(payload) + "." +
        base64.URLEncoding.EncodeToString(signature), nil
}

func verifyToken(tokenStr string, secret []byte) (*Token, error) {
    parts := strings.SplitN(tokenStr, ".", 2)
    if len(parts) != 2 {
        return nil, errors.New("invalid token format")
    }

    payload, err := base64.URLEncoding.DecodeString(parts[0])
    if err != nil {
        return nil, fmt.Errorf("decoding payload: %w", err)
    }

    signature, err := base64.URLEncoding.DecodeString(parts[1])
    if err != nil {
        return nil, fmt.Errorf("decoding signature: %w", err)
    }

    // Verify signature
    mac := hmac.New(sha256.New, secret)
    mac.Write(payload)
    expected := mac.Sum(nil)

    if !hmac.Equal(signature, expected) {
        return nil, errors.New("invalid signature")
    }

    // Parse token
    var token Token
    if err := json.Unmarshal(payload, &token); err != nil {
        return nil, fmt.Errorf("unmarshaling token: %w", err)
    }

    // Check expiration
    if time.Now().After(token.ExpiresAt) {
        return nil, errors.New("token expired")
    }

    return &token, nil
}

Проверь себя

Какое эталонное время используется в Go для форматирования?

Почему для сравнения HMAC-подписей нужно использовать hmac.Equal() вместо bytes.Equal()?

В чём разница между crypto/rand и math/rand/v2?

Что такое монотонные часы и зачем они нужны?