Перейти до змісту

Функції

Функція в Go — це іменований (або анонімний) блок коду з типізованими параметрами та нулем чи більше типізованих значень повернення. Ключове слово — func. Ви вже бачили func main() як точку входу програми; ця стаття охоплює всі інші форми, яких може набувати функція.

Базова форма

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

fmt.Println(add(2, 3))     // 5

Три речі, на які варто звернути увагу:

  1. Оголошення починається з ключового слова func.
  2. Кожен параметр записується як ім'я типтип іде після імені, що є протилежним до C/Java.
  3. Тип повернення йде після списку параметрів, без : чи стрілки ->.

Коли кілька параметрів мають однаковий тип, можна вказати його один раз наприкінці:

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

Без значення повернення

Тип повернення можна не вказувати зовсім. Функція завершується, коли виконання доходить до кінця тіла (або зустрічає ранній return).

func greet(name string) {
    fmt.Println("Hello,", name)
}

Множинні значення повернення

Функція може повертати більше одного значення. Типи повернення беруться в дужки.

func divmod(a, b int) (int, int) {
    return a / b, a % b
}

q, r := divmod(7, 3)
fmt.Println(q, r)               // 2 1

Це основа угоди про обробку помилок у Go — канонічний підпис для операції, що може зазнати невдачі, має вигляд (результат, error):

func parseAge(s string) (int, error) {
    n, err := strconv.Atoi(s)
    if err != nil {
        return 0, err
    }
    return n, nil
}

Тип error розглядається в окремій статті (помилки); поки що сприймайте його як «стандартний спосіб Go повідомити про помилку».

З досвіду Python: Python підтримує множинне повернення через розпакування кортежів (return a, b). Множинне повернення в Go є повноцінною частиною системи типів — підпис функції оголошує, що вона повертає два значення, компілятор перевіряє кожного виклику, і друге значення не можна втратити випадково.

Іменовані значення повернення

Можна надати значенням повернення імена прямо в підписі. Вони поводяться як заздалегідь оголошені змінні, ініціалізовані нульовими значеннями, а голий return (без виразу) повертає те, що в них зберігається наразі.

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return                      // returns x, y
}

fmt.Println(split(17))          // 7 10

Використовуйте іменовані значення повернення обережно. Вони найбільш корисні, коли:

  • Підпис інакше містить два значення одного типу, і імена роз'яснюють призначення ((width, height int)).
  • Тіло функції достатньо коротке, щоб неявне присвоєння було очевидним.

Для довших функцій надавайте перевагу явному return x, y — читачу не доведеться переглядати все тіло, щоб зрозуміти, що повертається.

Варіадичні параметри

Параметр типу ...T приймає нуль або більше аргументів і зв'язує їх як зріз []T всередині функції. Лише останній параметр може бути варіадичним.

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

xs := []int{1, 2, 3, 4}
fmt.Println(sum(xs...))         // 10 — spread an existing slice with ...

З досвіду Python: *args та **kwargs в одному — але з типами. Go не має еквівалента іменованих аргументів; якщо потрібні іменовані параметри, використовуйте структуру або патерн «функціональних опцій».

Функції є значеннями

Типи функцій є повноправними. Функцію можна присвоїти змінній, передати як аргумент або повернути з іншої функції.

func apply(f func(int) int, x int) int {
    return f(x)
}

double := func(n int) int { return n * 2 }
fmt.Println(apply(double, 5))   // 10

Тип func(int) int описує «функцію, яка приймає один int і повертає один int». Будь-яка функція з таким підписом може зайняти цей слот.

Анонімні функції та замикання

Функціональний літерал (без імені) можна визначити прямо на місці. Він також може замикатися над змінними з навколишнього контексту — захоплюючи посилання на них, а не копії.

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

c := makeCounter()
fmt.Println(c(), c(), c())      // 1 2 3

count живе стільки, скільки повернуте замикання на нього посилається, навіть після того, як сама makeCounter завершила виконання. Це ті самі семантики замикань, що й у вкладених функціях Python.

Негайно викликаний функціональний вираз

Поширений патерн із defer (розглядається в defer): визначити функціональний літерал і одразу викликати його через ().

func() {
    fmt.Println("runs once, right now")
}()
// output: runs once, right now

Функції не мають синтаксичного цукру для необов'язкових параметрів

Go не підтримує іменовані аргументи, необов'язкові позиційні аргументи або значення за замовчуванням. Якщо вам потрібна така зручність, є три ідіоматичні патерни — кожен показано нижче.

1. Кілька конструкторів з описовими іменами. Коли варіантів лише кілька, пишіть функцію на кожен варіант. Ім'я документує різницю.

type Reader struct {
    data    string
    bufSize int
}

func NewReader(data string) *Reader            { return &Reader{data: data, bufSize: 4096} }
func NewReaderSize(data string, n int) *Reader { return &Reader{data: data, bufSize: n} }

fmt.Println(NewReader("hi").bufSize)         // output: 4096
fmt.Println(NewReaderSize("hi", 64).bufSize) // output: 64

2. Структура опцій. Передавайте структуру налаштувань. Нульове значення кожного поля — це його значення за замовчуванням, а літерал структури з ключами читається майже як іменовані аргументи — ви задаєте лише те, що потрібно, у будь-якому порядку.

type Options struct {
    Timeout int
    Retries int
    Debug   bool
}

func Connect(addr string, opts Options) string {
    if opts.Timeout == 0 {
        opts.Timeout = 30 // значення за замовчуванням, коли викликач його пропускає
    }
    return fmt.Sprintf("%s timeout=%d retries=%d debug=%v",
        addr, opts.Timeout, opts.Retries, opts.Debug)
}

fmt.Println(Connect("db:5432", Options{Retries: 3, Debug: true}))
// output: db:5432 timeout=30 retries=3 debug=true

3. Патерн функціональних опцій. Кожна опція — це функція, що змінює значення, яке будується. Конструктор приймає варіативний список таких функцій, застосовуючи кожну поверх розумних значень за замовчуванням. Саме це використовують великі бібліотеки, коли необов'язкових налаштувань багато.

type Server struct {
    host string
    port int
}

type Option func(*Server)

func WithHost(h string) Option { return func(s *Server) { s.host = h } }
func WithPort(p int) Option    { return func(s *Server) { s.port = p } }

func NewServer(opts ...Option) *Server {
    s := &Server{host: "localhost", port: 8080} // значення за замовчуванням
    for _, opt := range opts {
        opt(s)
    }
    return s
}

s := NewServer(WithPort(9090))
fmt.Println(s.host, s.port)   // output: localhost 9090

Емпіричне правило: кілька варіантів → окремі конструктори; фіксований набір налаштувань → структура опцій; відкритий, зростаючий набір → функціональні опції.

З досвіду Python: немає def f(x=42, *, debug=False). Ідіоматичний Go надає перевагу додатковому конструктору або структурі опцій замість іменованих аргументів. Спочатку це здається многослівним; швидко перестаєш помічати.

Рекурсія

Рекурсія дозволена і нічим не відрізняється від очікуваного. Go не виконує оптимізацію хвостових викликів — глибоко рекурсивна функція може вичерпати стек. Для більшості практичних глибин це несуттєво; стеки горутин починаються малими (~8 КБ) і ростуть за потреби.

func factorial(n int) int {
    if n <= 1 {
        return 1
    }
    return n * factorial(n-1)
}

fmt.Println(factorial(10))      // 3628800

Короткий довідник

Форма Виглядає як
Одне значення повернення func f(x int) int { ... }
Множинні значення повернення func f() (int, error) { ... }
Список параметрів одного типу func f(x, y, z int) { ... }
Іменовані значення + голий return func f() (n int, err error) { ... return }
Варіадична func f(xs ...int) { ... }; виклик: f(1, 2, 3) або f(slice...)
Функція як параметр func apply(f func(int) int, x int) int
Функціональний літерал func(x int) int { return x*x }
Замикання вкладений func, що захоплює зовнішню змінну

Джерела