Easy📖Теория7 min

Строки и форматирование

Пакеты strings, strconv, fmt, regexp, bytes и unicode

Строки и форматирование

Строки в Go -- это неизменяемые (immutable) последовательности байтов, обычно содержащие текст в UTF-8. Go предоставляет богатый набор пакетов для работы со строками: strings, strconv, fmt, regexp, bytes и unicode.

Пакет strings

Поиск и проверка

import "strings"

s := "Hello, World!"

// Contains -- содержит ли подстроку
strings.Contains(s, "World")    // true
strings.Contains(s, "world")    // false (case-sensitive)
strings.ContainsAny(s, "aeiou") // true (any of these chars)
strings.ContainsRune(s, 'W')    // true

// HasPrefix / HasSuffix
strings.HasPrefix(s, "Hello")   // true
strings.HasSuffix(s, "!")       // true

// Index -- позиция первого вхождения (-1 если не найдено)
strings.Index(s, "World")       // 7
strings.LastIndex(s, "l")       // 10

// Count
strings.Count(s, "l")           // 3
strings.Count("cheese", "e")    // 3

Разделение и соединение

// Split
parts := strings.Split("a,b,c,d", ",")
// ["a", "b", "c", "d"]

// SplitN -- максимум N частей
parts = strings.SplitN("a,b,c,d", ",", 2)
// ["a", "b,c,d"]

// SplitAfter -- сохраняет разделитель
parts = strings.SplitAfter("a,b,c", ",")
// ["a,", "b,", "c"]

// Fields -- разделение по пробелам (любое кол-во)
words := strings.Fields("  hello   world  go  ")
// ["hello", "world", "go"]

// FieldsFunc -- разделение по кастомному условию
words = strings.FieldsFunc("foo1bar2baz", func(r rune) bool {
    return r >= '0' && r <= '9'
})
// ["foo", "bar", "baz"]

// Join
result := strings.Join([]string{"Go", "is", "awesome"}, " ")
// "Go is awesome"

Замена и трансформация

// Replace
s := strings.Replace("foo bar foo", "foo", "baz", 1)   // "baz bar foo"
s = strings.ReplaceAll("foo bar foo", "foo", "baz")     // "baz bar baz"

// Trim
s = strings.TrimSpace("  hello  ")       // "hello"
s = strings.Trim("***hello***", "*")      // "hello"
s = strings.TrimLeft("***hello", "*")     // "hello"
s = strings.TrimRight("hello***", "*")    // "hello"
s = strings.TrimPrefix("hello.go", "hello") // ".go"
s = strings.TrimSuffix("hello.go", ".go")   // "hello"

// Case conversion
s = strings.ToUpper("hello")   // "HELLO"
s = strings.ToLower("HELLO")   // "hello"
s = strings.Title("hello world") // Deprecated in 1.18+
// Use golang.org/x/text/cases instead

// Repeat
s = strings.Repeat("ha", 3) // "hahaha"

// Map -- apply function to each rune
s = strings.Map(func(r rune) rune {
    if r == 'o' {
        return '0'
    }
    return r
}, "foo bar")
// "f00 bar"

EqualFold -- регистро-независимое сравнение

strings.EqualFold("Go", "go")       // true
strings.EqualFold("Go", "GO")       // true
strings.EqualFold("straße", "STRASSE") // true (Unicode-aware!)

strings.Builder -- эффективная конкатенация

strings.Builder минимизирует аллокации при построении строк:

func buildQuery(fields []string, table string, conditions []string) string {
    var b strings.Builder

    // Grow pre-allocates buffer (optional but efficient)
    b.Grow(256)

    b.WriteString("SELECT ")
    for i, f := range fields {
        if i > 0 {
            b.WriteString(", ")
        }
        b.WriteString(f)
    }

    b.WriteString(" FROM ")
    b.WriteString(table)

    if len(conditions) > 0 {
        b.WriteString(" WHERE ")
        for i, c := range conditions {
            if i > 0 {
                b.WriteString(" AND ")
            }
            b.WriteString(c)
        }
    }

    return b.String()
}

Никогда не конкатенируйте строки в цикле через +. Это O(n^2) из-за создания новой строки каждый раз. strings.Builder -- O(n).

strings.NewReader -- строка как io.Reader

r := strings.NewReader("Hello, World!")

// Now you can use it wherever io.Reader is expected
data, _ := io.ReadAll(r)                     // Read all
json.NewDecoder(strings.NewReader(s)).Decode(&v) // JSON decode
http.Post(url, "text/plain", strings.NewReader(body)) // HTTP body

Пакет strconv -- конвертация типов

import "strconv"

// String <-> Int
n, err := strconv.Atoi("42")       // string -> int: 42, nil
s := strconv.Itoa(42)              // int -> string: "42"

// ParseInt with base and bit size
n64, err := strconv.ParseInt("FF", 16, 64)    // hex: 255
n64, err = strconv.ParseInt("1010", 2, 64)    // binary: 10
n64, err = strconv.ParseInt("-42", 10, 32)     // decimal int32: -42

// ParseFloat
f, err := strconv.ParseFloat("3.14", 64) // 3.14 as float64
f, err = strconv.ParseFloat("1e10", 64)  // 10000000000

// ParseBool
b, err := strconv.ParseBool("true")   // true
b, err = strconv.ParseBool("1")       // true
b, err = strconv.ParseBool("false")   // false
b, err = strconv.ParseBool("0")       // false

// FormatInt
s = strconv.FormatInt(255, 16)        // "ff"
s = strconv.FormatInt(10, 2)          // "1010"
s = strconv.FormatFloat(3.14, 'f', 2, 64)  // "3.14"
s = strconv.FormatFloat(3.14, 'e', 2, 64)  // "3.14e+00"
s = strconv.FormatFloat(3.14, 'g', -1, 64) // "3.14"

// Quote / Unquote -- для Go string литералов
s = strconv.Quote("Hello\nWorld")      // `"Hello\nWorld"`
s, err = strconv.Unquote(`"Hello\n"`)  // "Hello\n"

// AppendInt -- append to []byte without allocation
buf := make([]byte, 0, 20)
buf = strconv.AppendInt(buf, 42, 10)   // []byte("42")

Пакет fmt -- форматирование

Printf и форматирующие глаголы (verbs)

type User struct {
    Name string
    Age  int
}

u := User{"Alice", 30}

// General
fmt.Printf("%v\n", u)       // {Alice 30}          -- default format
fmt.Printf("%+v\n", u)      // {Name:Alice Age:30}  -- with field names
fmt.Printf("%#v\n", u)      // main.User{Name:"Alice", Age:30} -- Go syntax
fmt.Printf("%T\n", u)       // main.User            -- type

// Integers
fmt.Printf("%d\n", 42)      // 42          -- decimal
fmt.Printf("%b\n", 42)      // 101010      -- binary
fmt.Printf("%o\n", 42)      // 52          -- octal
fmt.Printf("%O\n", 42)      // 0o52        -- octal with prefix
fmt.Printf("%x\n", 42)      // 2a          -- hex lowercase
fmt.Printf("%X\n", 42)      // 2A          -- hex uppercase
fmt.Printf("%08d\n", 42)    // 00000042    -- zero-padded
fmt.Printf("%+d\n", 42)     // +42         -- always show sign

// Strings
fmt.Printf("%s\n", "hello") // hello       -- plain string
fmt.Printf("%q\n", "hello") // "hello"     -- quoted
fmt.Printf("%10s\n", "hi")  //         hi  -- right-aligned
fmt.Printf("%-10s\n", "hi") // hi          -- left-aligned

// Floats
fmt.Printf("%f\n", 3.14)    // 3.140000    -- decimal
fmt.Printf("%.2f\n", 3.14)  // 3.14        -- 2 decimal places
fmt.Printf("%e\n", 3.14)    // 3.140000e+00 -- scientific
fmt.Printf("%g\n", 3.14)    // 3.14        -- compact

// Pointers
fmt.Printf("%p\n", &u)      // 0xc0000b4000

// Width and precision
fmt.Printf("%10d\n", 42)    //         42  -- width 10
fmt.Printf("%-10d|\n", 42)  // 42        | -- left-aligned
fmt.Printf("%010d\n", 42)   // 0000000042  -- zero-padded

// Error formatting with %w (fmt.Errorf only)
err := fmt.Errorf("query failed: %w", sql.ErrNoRows)
// errors.Is(err, sql.ErrNoRows) == true

Sprintf, Fprintf, Errorf

// Sprintf -- format to string
msg := fmt.Sprintf("User %s has %d points", name, points)

// Fprintf -- format to io.Writer
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
fmt.Fprintf(w, "Hello, %s!", name) // w is http.ResponseWriter

// Errorf -- format error with wrapping
err := fmt.Errorf("parsing config %s: %w", filename, err)

fmt.Stringer и fmt.GoStringer

type Color struct {
    R, G, B uint8
}

// String -- controls %v and %s output
func (c Color) String() string {
    return fmt.Sprintf("#%02x%02x%02x", c.R, c.G, c.B)
}

// GoString -- controls %#v output (Go syntax)
func (c Color) GoString() string {
    return fmt.Sprintf("Color{R: %d, G: %d, B: %d}", c.R, c.G, c.B)
}

c := Color{255, 128, 0}
fmt.Println(c)           // #ff8000
fmt.Printf("%v\n", c)    // #ff8000
fmt.Printf("%#v\n", c)   // Color{R: 255, G: 128, B: 0}

Пакеты unicode и unicode/utf8

import (
    "unicode"
    "unicode/utf8"
)

// unicode -- character classification
unicode.IsLetter('A')  // true
unicode.IsDigit('5')   // true
unicode.IsSpace(' ')   // true
unicode.IsUpper('A')   // true
unicode.IsLower('a')   // true
unicode.IsPunct('!')   // true
unicode.ToUpper('a')   // 'A'
unicode.ToLower('A')   // 'a'

// utf8 -- UTF-8 encoding operations
s := "Привет, мир!"

utf8.RuneCountInString(s)   // 12 (rune count, not byte count!)
len(s)                       // 22 (byte count)

utf8.ValidString(s)          // true
utf8.Valid([]byte(s))        // true

// Decode first rune
r, size := utf8.DecodeRuneInString(s)
fmt.Printf("%c, %d bytes\n", r, size) // П, 2 bytes

// Encode rune to bytes
buf := make([]byte, 4)
n := utf8.EncodeRune(buf, 'Я')
fmt.Println(buf[:n]) // [208 175]

Помните: len(s) возвращает количество байтов, не символов. Для подсчёта символов используйте utf8.RuneCountInString(s) или for _, r := range s.

Пакет regexp -- регулярные выражения

import "regexp"

// Compile -- returns error if pattern is invalid
re, err := regexp.Compile(`\d+`)
if err != nil {
    log.Fatal(err)
}

// MustCompile -- panics on error (use for constants)
re = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

// Basic matching
re.MatchString("[email protected]") // true

// Find
re2 := regexp.MustCompile(`\d+`)
re2.FindString("abc 123 def 456")         // "123" (first match)
re2.FindAllString("abc 123 def 456", -1)  // ["123", "456"] (all matches)
re2.FindAllString("abc 123 def 456", 1)   // ["123"] (first N matches)

// Find with submatch (groups)
re3 := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`)
match := re3.FindStringSubmatch("[email protected]")
// ["[email protected]", "alice", "example", "com"]

// Named groups
re4 := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
match = re4.FindStringSubmatch("[email protected]")
names := re4.SubexpNames()
for i, name := range names {
    if i > 0 && name != "" {
        fmt.Printf("%s: %s\n", name, match[i])
    }
}
// user: alice
// domain: example.com

// Replace
result := re2.ReplaceAllString("price: 100 USD, tax: 20 USD", "XXX")
// "price: XXX USD, tax: XXX USD"

// Replace with function
result = re2.ReplaceAllStringFunc("price: 100, tax: 20", func(s string) string {
    n, _ := strconv.Atoi(s)
    return strconv.Itoa(n * 2)
})
// "price: 200, tax: 40"

// Split
parts := regexp.MustCompile(`[,;:\s]+`).Split("a, b; c: d  e", -1)
// ["a", "b", "c", "d", "e"]

Важно: Регулярные выражения Go используют синтаксис RE2 (без backtracking). Это гарантирует линейное время выполнения, но не поддерживает lookahead/lookbehind.

Предкомпиляция для производительности

// BAD: compiles regex on every call
func isEmail(s string) bool {
    matched, _ := regexp.MatchString(`^[\w.]+@[\w.]+\.\w+$`, s)
    return matched
}

// GOOD: compile once, reuse
var emailRegex = regexp.MustCompile(`^[\w.]+@[\w.]+\.\w+$`)

func isEmail(s string) bool {
    return emailRegex.MatchString(s)
}

Пакет bytes -- зеркало strings для []byte

Пакет bytes предоставляет те же функции, что strings, но для []byte:

import "bytes"

data := []byte("Hello, World!")

bytes.Contains(data, []byte("World"))     // true
bytes.HasPrefix(data, []byte("Hello"))    // true
bytes.Split(data, []byte(","))            // [[]byte("Hello"), []byte(" World!")]
bytes.Replace(data, []byte("World"), []byte("Go"), 1) // "Hello, Go!"
bytes.ToUpper(data)                       // "HELLO, WORLD!"
bytes.TrimSpace([]byte("  hello  "))      // "hello"
bytes.Equal([]byte("a"), []byte("a"))     // true

// bytes.Buffer -- mutable byte buffer
var buf bytes.Buffer
buf.WriteString("Hello")
buf.WriteByte(',')
buf.WriteString(" World!")
fmt.Println(buf.String()) // "Hello, World!"

// bytes.NewReader -- []byte as io.Reader
r := bytes.NewReader(data)

Когда использовать bytes вместо strings:

  • Работа с бинарными данными
  • Избежание конвертации string <-> []byte
  • Работа с I/O (всё в Go -- байты)

Проверь себя

🧪

Почему нельзя конкатенировать строки в цикле через оператор +?

🧪

Что возвращает len(s) для строки с кириллицей в Go?

🧪

Что делает глагол %w в fmt.Errorf?

🧪

Почему regexp в Go не поддерживает lookahead/lookbehind?