Функции и несколько возвращаемых значений

Функции и несколько возвращаемых значений

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

Основы функций

Простая функция

func greet(name string) string {
    return "Привет, " + name + "!"
}

// Использование
message := greet("Мир")
fmt.Println(message) // Привет, Мир!

Несколько параметров

func add(x int, y int) int {
    return x + y
}

// Или короче, если типы одинаковые
func multiply(x, y int) int {
    return x * y
}

Множественный возврат

Это killer-feature Go - функция может возвращать несколько значений:

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("деление на ноль")
    }
    return a / b, nil
}

// Использование
result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result) // 5
Последним возвращаемым значением часто является error. Это стандартный паттерн обработки ошибок в Go.

Игнорирование значений

// Нужен только результат
result, _ := divide(10, 2)

// Нужна только ошибка
_, err := divide(10, 0)

Именованные возвращаемые значения

func getCoordinates() (x, y int) {
    x = 10
    y = 20
    return // naked return
}

// Более читаемый вариант
func calculateStats(numbers []int) (min, max, avg int) {
    if len(numbers) == 0 {
        return // возвращает нулевые значения
    }

    min, max = numbers[0], numbers[0]
    sum := 0

    for _, n := range numbers {
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
        sum += n
    }

    avg = sum / len(numbers)
    return min, max, avg
}

Вариативные функции

Функции с переменным числом аргументов:

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// Использование
fmt.Println(sum())           // 0
fmt.Println(sum(1, 2, 3))    // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15

// Передача слайса
numbers := []int{1, 2, 3}
fmt.Println(sum(numbers...)) // 6

Defer - отложенное выполнение

Defer выполняется перед выходом из функции, даже если произошла паника. Отложенные вызовы выполняются в обратном порядке (LIFO).

Базовое использование

func readFile(filename string) ([]byte, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer file.Close() // Выполнится при выходе из функции

    // Читаем файл
    return io.ReadAll(file)
}

Порядок выполнения defer

func deferOrder() {
    defer fmt.Println("первый")
    defer fmt.Println("второй")
    defer fmt.Println("третий")
}
// Вывод:
// третий
// второй
// первый

defer хранится в стеке LIFO: последний пушится первым, выходит первым

Defer и аргументы

func deferArgs() {
    i := 0
    defer fmt.Println("defer:", i) // i захватывается сразу
    i++
    // Выведет: defer: 0
}

Функции как значения

В Go функции - это first-class citizens:

// Функция как переменная
var operation func(int, int) int

operation = add
fmt.Println(operation(5, 3)) // 8

operation = multiply
fmt.Println(operation(5, 3)) // 15

// Функция как параметр
func apply(fn func(int, int) int, a, b int) int {
    return fn(a, b)
}

result := apply(add, 10, 5) // 15

Анонимные функции и замыкания

Анонимные функции

// Немедленный вызов
func() {
    fmt.Println("Анонимная функция")
}()

// Присваивание переменной
double := func(x int) int {
    return x * 2
}
fmt.Println(double(5)) // 10

Замыкания (closures)

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

// Использование
c1 := counter()
c2 := counter()

fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c2()) // 1
fmt.Println(c1()) // 3

Практические примеры

HTTP handler с замыканием

func makeHandler(prefix string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "%s: %s", prefix, r.URL.Path)
    }
}

// Использование
http.HandleFunc("/api", makeHandler("API"))
http.HandleFunc("/web", makeHandler("WEB"))

Retry с экспоненциальной задержкой

func retryWithBackoff(fn func() error, maxRetries int) error {
    var err error

    for i := 0; i < maxRetries; i++ {
        if err = fn(); err == nil {
            return nil
        }

        if i < maxRetries-1 {
            delay := time.Duration(math.Pow(2, float64(i))) * time.Second
            time.Sleep(delay)
            fmt.Printf("Попытка %d не удалась, жду %v\n", i+1, delay)
        }
    }

    return fmt.Errorf("не удалось после %d попыток: %w", maxRetries, err)
}

Middleware паттерн

type Middleware func(http.HandlerFunc) http.HandlerFunc

func withLogging(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("Запрос: %s %s", r.Method, r.URL.Path)
        next(w, r)
    }
}

func withAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if r.Header.Get("Authorization") == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next(w, r)
    }
}

// Композиция
handler := withLogging(withAuth(myHandler))

Методы (методы структур)

Хотя это не совсем функции, методы тесно связаны с ними:

type Rectangle struct {
    width, height float64
}

// Метод с value receiver
func (r Rectangle) Area() float64 {
    return r.width * r.height
}

// Метод с pointer receiver
func (r *Rectangle) Scale(factor float64) {
    r.width *= factor
    r.height *= factor
}

// Использование
rect := Rectangle{10, 5}
fmt.Println(rect.Area()) // 50

rect.Scale(2)
fmt.Println(rect.Area()) // 200

Escape analysis: стек или куча

Компилятор сам решает, где разместить локальную переменную. Если значение не переживёт кадр функции - оно живёт на стеке (бесплатное освобождение при return). Если уезжает наружу (возврат указателя, замыкание, отправка в канал) - попадает в кучу, и потом подметает GC.

Локальная переменная на стеке против указателя, который уезжает в кучу

Посмотреть решения компилятора: go build -gcflags="-m".

Лучшие практики

<ComparisonTable data={{ headers: ["Практика", "Плохо", "Хорошо"], rows: [ ["Обработка ошибок", "result := must(divide(10, 0))", "result, err := divide(10, 0)\nif err != nil { ... }"], ["Именование", "func calc(x, y int) int", "func calculateSum(x, y int) int"], ["Defer для ресурсов", "file.Close() // в конце функции", "defer file.Close() // сразу после Open"], ["Возврат ошибок", "return errors.New("error")", "return fmt.Errorf("не удалось обработать %s: %w", file, err)"], ["Параметры", "func process(s1, s2, s3 string)", "func process(config ProcessConfig)"] ] }} />

Полезные паттерны

Functional options

type Server struct {
    port    int
    timeout time.Duration
}

type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func NewServer(opts ...Option) *Server {
    s := &Server{
        port:    8080,           // значения по умолчанию
        timeout: 30 * time.Second,
    }

    for _, opt := range opts {
        opt(s)
    }

    return s
}

// Использование
server := NewServer(
    WithPort(9000),
    WithTimeout(60 * time.Second),
)

Итоги

  • Функции могут возвращать несколько значений
  • Defer для гарантированного выполнения кода
  • Функции - первоклассные объекты
  • Замыкания захватывают внешние переменные
  • Методы расширяют типы

В следующем уроке изучим слайсы - динамические массивы Go!

Типичная ошибка

Смешивать «что делаем» и «как печатаем»: пусть функция возвращает значение, а вывод - отдельно.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.