Функции, типы, пакеты и память

Лекция 2

Максим Иванов

После лекции

2

Объявления

В Go есть четыре вида объявлений:

var   const   type   func

Порядок package-level объявлений в файле не определяет порядок их использования.

var circumference = 2 * Pi * radius
const Pi = 3.141592653589793
var radius = 10.0

Объявление связывает имя с типом, значением или функцией. Package-level declarations доступны из всех файлов того же пакета.

3

Ключевые слова

В Go 25 ключевых слов. Их нельзя использовать как имена.

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var
4

Predeclared identifiers

Эти имена объявлены заранее, но не зарезервированы:

true false iota nil
any comparable int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex64 complex128
bool byte rune string error
min max make len cap new append clear copy close delete
complex real imag panic recover print println

Их можно переопределять по обычным правилам

// just example, don't do this
// Backward compatibility
var true = false
5

var и zero value

Общая форма:

var name type = expression

Тип или expression можно опустить, но не оба сразу.

var retries int                 // 0
var ready bool                  // false
var title string                // ""
var timeout = 5 * time.Second   // time.Duration
var file, err = os.Open(name)   // *os.File, error

Каждый тип имеет zero value. Объявленная переменная всегда инициализирована.

6

Короткое объявление

i, j := 0, 1

:= работает только внутри функций.

В том же block можно переиспользовать существующее имя, если появляется хотя бы одно новое:

in, err := os.Open(inputName)
// ...
out, err := os.Create(outputName) // out новое, err переиспользован

in, err := os.Open(otherName)     // compile error: no new variables

Redeclaration относится только к тому же lexical block. Во вложенном block такое же имя создаёт новую переменную.

Вопрос: почему объявление `out, err := ...` корректно, а повторное `in, err := ...` нет?

7

Область видимости

Scope имени определяется синтаксическими блоками программы.

package main

var language = "Go" // package scope

func printTwice() {
    message := "hello" // function body scope
    for i := 0; i < 2; i++ {
        fmt.Println(i, message) // loop block scope
    }
}

Имя видно во вложенных блоках, пока его не затенит новое объявление.

8

Shadowing

package main

import "fmt"

func main() {
    word := "hello"
    for _, word := range word {
        word := word + 'A' - 'a'
        fmt.Printf("%c", word)
    }

    fmt.Println()
    fmt.Println(word)
}
$ go run ./02-language/scope
HELLO
hello
9

Shadowing: ловушка

Одинаковое написание не означает одну переменную.

var cwd string

func init() {
    cwd, err := os.Getwd() // новое локальное cwd
    if err != nil {
        log.Fatal(err)
    }
    log.Print(cwd)         // локальное значение
}

После init package-level cwd всё ещё равно "".

10

Scope initializer в if

Имя из initializer доступно во всех ветках, но не после if.

if file, err := os.Open(name); err != nil {
    return err
} else {
    return readConfig(file)
}

Если файл нужен дальше, объявление должно находиться снаружи:

file, err := os.Open(name)
if err != nil {
    return err
}
return readConfig(file)
11

Указатели

Указатель хранит адрес переменной.

x := 1
p := &x         // *int
fmt.Println(*p) // 1
*p = 2
fmt.Println(x)  // 2

Zero value указателя равен nil.

var p *int
fmt.Println(p == nil) // true
fmt.Println(*p)       // panic: nil pointer dereference
12

Функции

func name(parameterList) resultList {
    body
}

func hypot(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}

Соседние параметры одного типа можно объединять:

func move(x, y int, label string) Point

Go всегда передаёт аргументы по значению.

13

Function values

Функцию можно хранить в переменной и передавать как значение.

func increment(value int) int { return value + 1 }

var transform func(int) int
transform = increment
fmt.Println(transform(10)) // 11

Zero value function value равен nil. Вызов nil function приводит к panic.

14

Closures: что напечатает программа?

package main

import "fmt"

func counter(start int) func() int {
    value := start

    return func() int {
        value++
        return value
    }
}

func main() {
    first := counter(10)
    second := counter(100)

    fmt.Println(first())
    fmt.Println(first())
    fmt.Println(second())
}
15

Closures: вывод

$ go run ./02-language/counter
11
12
101

Function literal видит переменные окружающей функции.

Каждый вызов counter создаёт независимое состояние.

16

Range-переменные с Go 1.22

package main

import "fmt"

func main() {
    var callbacks []func()
    for _, value := range []string{"A", "B", "C"} {
        callbacks = append(callbacks, func() { fmt.Print(value) })
    }
    for _, callback := range callbacks {
        callback()
    }
    fmt.Println()
}
$ go run ./02-language/rangevars
ABC

С := каждая итерация создаёт новую value.

До Go 1.22 этот код печатал CCC.

17

Рекурсия

Рекурсивной функции нужен базовый случай.

func sum(root *Tree) int {
    if root == nil {
        return 0
    }
    return root.Value + sum(root.Left) + sum(root.Right)
}

Для recursive closure имя объявляют до присваивания:

var visit func(*Tree)
visit = func(node *Tree) { /* ... visit(node.Left) ... */ }
18

Несколько результатов

Функция часто возвращает полезное значение и ошибку:

func parsePort(text string) (uint16, error) {
    value, err := strconv.ParseUint(text, 10, 16)
    if err != nil {
        return 0, err
    }
    return uint16(value), nil
}

Результаты другой функции можно передать напрямую:

func parseDefaultPort() (uint16, error) {
    return parsePort("8080")
}
19

Именованные результаты

Имена результатов документируют короткую сигнатуру:

func split(total int) (quotient, remainder int) {
    quotient = total / 10
    remainder = total % 10
    return quotient, remainder
}

return без значений допустим, но в длинной функции ухудшает читаемость.

func split(total int) (quotient, remainder int) {
    quotient = total / 10
    remainder = total % 10
    return // работает, но явный return проще читать
}
20

Ошибки как значения

Ожидаемая неудача возвращается как error.

balance, err := wallet.NewBalance(initial)
if err != nil {
    return err
}

Текст ошибки начинается со строчной буквы: вызывающий код может добавить контекст.

21

Контекст ошибки

config, err := loadConfig(path)
if err != nil {
    return fmt.Errorf("load config %q: %w", path, err)
}

%w сохраняет исходную ошибку в цепочке.

if errors.Is(err, fs.ErrNotExist) {
    // отдельная реакция на отсутствующий файл
}

Sentinel error можно сравнивать через errors.Is:

if errors.Is(err, io.EOF) {
    break
}
22

Объявление типа

Named type создаёт новый тип:

type Celsius float64
type Fahrenheit float64

var c Celsius = 20
var f Fahrenheit = Fahrenheit(c) // явное преобразование

Alias даёт другое имя существующему типу:

type byte = uint8
type rune = int32

Named types помогают не смешивать значения с разным смыслом.

23

Константы и iota

type Weekday int
const (
    Sunday Weekday = iota // 0
    Monday                // 1
    Tuesday               // 2
)

type Flags uint
const (
    FlagUp Flags = 1 << iota // 1
    FlagBroadcast            // 2
    FlagLoopback             // 4
    FlagPointToPoint         // 8
)

Пропущенное выражение повторяет предыдущее. iota равен индексу текущей спецификации констант (ConstSpec), начиная с 0.

24

iota: что чему равно?

Первый вариант:

const (
    A = iota
    B
    _
    C
)

Второй вариант:

const (
    X = 100
    Y, Z = iota, iota + 10
    W, V
    Q = iota
    R = 2
    T
)
25

Untyped constants

const secondsPerDay = 24 * 60 * 60

Untyped constant получает тип из контекста, если значение представимо:

var seconds int64 = secondsPerDay
var duration time.Duration = secondsPerDay * time.Second

const value = 256                 // OK: untyped integer
var enough uint16 = value         // OK
var tooSmall uint8 = value        // compile error: 256 overflows uint8

Это исключение, а не неявное преобразование между named types.

var c Celsius = 20
var f Fahrenheit = c // compile error
26

Большие untyped constants

const (
    _ = 1 << (10 * iota)
    KiB
    MiB
    GiB
    TiB
    PiB
    EiB
    ZiB
    YiB
)

Реализация обязана поддерживать точные integer constants как минимум с 256 значащими битами.

Если внутреннего лимита не хватает, компилятор сообщает ошибку: integer constant не округляется и не переполняется.

Вопрос: скомпилируется ли `fmt.Println(YiB / ZiB)` и что он напечатает?

27

Структуры

Структура объединяет значения с разным смыслом.

type Account struct {
    Owner   string
    Balance int64
}

var account Account
fmt.Println(account.Owner)   // ""
fmt.Println(account.Balance) // 0

Zero value структуры состоит из zero values её полей.

28

Struct literals

type Point struct {
    X int
    Y int
}

p := Point{1, 2}
q := Point{X: 1, Y: 2}
r := Point{Y: 5} // X == 0

Вне небольших локальных типов keyed literal устойчивее к добавлению и перестановке полей.

29

Параметры передаются по значению

Функция получает копию каждого аргумента.

func resetValue(value int) {
    value = 0
}

func resetPointer(value *int) {
    *value = 0
}

Копия указателя всё ещё указывает на исходную переменную.

Вопрос: какая из функций может изменить переменную вызывающего кода?

30

Рекурсивные структуры

type Node struct {
    Value int
    Next  *Node
}

Поле `Next Node` невозможно: размер Node зависел бы от самого себя.

Размер указателя известен, поэтому `Next *Node` завершает определение типа.

third := &Node{Value: 3}
second := &Node{Value: 2, Next: third}
first := &Node{Value: 1, Next: second}
31

new

new(T) создаёт zero value типа T и возвращает указатель *T.

func newInt() *int {
    return new(int)
}

Адрес локальной переменной имеет ту же семантику:

func newInt() *int {
    value := 0
    return &value
}

new не означает «обязательно выделить память на heap».

32

Dangling pointer в C++

const PointsData* fullPointsData = nullptr;
if (fullData) {
    auto value = getPointsData();
    fullPointsData = &value;
}
auto table = getPointsByDocument(docID, fullPointsData);

Что не так с fullPointsData?

33

Время жизни переменной

Переменная живёт, пока программа может до неё добраться.

var global *int

func remember() {
    value := 42
    global = &value
}

Лексический block закончился, но значение остаётся достижимым через global.

Garbage collector может освободить память недостижимых объектов.

34

Время жизни в C++ и Go

В C++ value уничтожается при выходе из block. Указатель не равен nullptr, но объект по этому адресу больше не существует.

В Go адрес локальной переменной можно безопасно вернуть:

func loadPoints() *PointsData {
    value := getPointsData()
    return &value
}

Go сохраняет значение, пока оно достижимо. Конкретное размещение выбирает компилятор.

35

Escape analysis: что попадёт в heap?

package main

import "fmt"

func localPointer() *int {
    value := 42
    return &value
}

func localValue() int {
    value := new(int)
    *value = 42
    return *value
}

func main() {
    fmt.Println(*localPointer())
    fmt.Println(localValue())
}
36

Escape analysis: вывод программы

$ go run ./02-language/escape
42
42

Обе функции корректны: адрес локальной переменной можно вернуть, а new(int) не обещает конкретное место хранения.

37

Escape analysis: диагностика компилятора

Компилятор решает, где хранить значение. Это оптимизация, а не семантика языка.

$ go build -gcflags=-m=2 -o /dev/null ./02-language/escape
... value escapes to heap in localPointer
... moved to heap: value

Формулировки в выводе и конкретное размещение могут меняться между версиями Go.

Правильность программы не должна зависеть от stack или heap.

38

Пакеты

Пакет состоит из одного или нескольких .go файлов в одной директории.

Файлы foo_test.go могут использовать package foo или foo_test.

39

Exported identifiers: пакет

package exported

const Gravity = 9.8

var Name string

type Point struct {
    X int
    y int
}

func Distance() float64 {
    return 0
}

func helper() {}
40

Exported identifiers: клиент пакета

package main

import (
	"fmt"

	"hse-go/lectures/02-language/exported"
)

func main() {
    exported.Name = "geometry"
    point := exported.Point{X: 7}

    fmt.Printf("%s: X=%d gravity=%.1f distance=%.1f\n",
        exported.Name, point.X, exported.Gravity, exported.Distance())
}
$ go run ./02-language/exported-demo
geometry: X=7 gravity=9.8 distance=0.0

Видимость определяется первой Unicode-буквой имени.

Клиент видит Gravity, Name, Point, X и Distance, но не может обратиться к y и helper.

41

Один пакет, несколько файлов: types.go

package tempconv

type (
    Celsius    float64
    Fahrenheit float64
)

const (
    AbsoluteZeroC Celsius = -273.15
    FreezingC     Celsius = 0
    BoilingC      Celsius = 100
)
42

Один пакет, несколько файлов: conv.go

package tempconv

func CToF(c Celsius) Fahrenheit {
    return Fahrenheit(c*9/5 + 32)
}

func FToC(f Fahrenheit) Celsius {
    return Celsius((f - 32) * 5 / 9)
}
43

Использование package tempconv

package main

import (
	"fmt"

	"hse-go/lectures/02-language/tempconv"
)

func main() {
    cold := tempconv.Celsius(-40)
    hot := tempconv.Fahrenheit(212)

    fmt.Printf("%.0f °C = %.0f °F\n", cold, tempconv.CToF(cold))
    fmt.Printf("%.0f °F = %.0f °C\n", hot, tempconv.FToC(hot))
}
$ go run ./02-language/tempconv-demo
-40 °C = -40 °F
212 °F = 100 °C

Файлы компилируются вместе. Порядок файлов не влияет на доступность объявлений.

44

Package и module

Package задаёт пространство имён и единицу компиляции.

Module задаёт import path проекта и версии зависимостей.

module hse-go/lectures

import "hse-go/lectures/02-language/tempconv"

Базовые команды из корня module:

go test ./...
go mod tidy

Версии модулей разберём в отдельной лекции.

45

Инициализация пакета: что выполнится первым?

package main

import "fmt"

var (
    third  = second
    second = twice(first)
    first  = 1
)

func twice(value int) int {
    return value * 2
}

func init() {
    fmt.Println("init", first, second)
}

func main() {
    fmt.Println("main", third)
}
46

Инициализация пакета: вывод

$ go run ./02-language/initorder
init 1 2
main 2

Package-level variables вычисляются в порядке зависимостей, затем вызываются функции init.

Не прячьте обычную бизнес-логику в init.

47

Главное

48

Вопросы для самопроверки

1. Чем `var x int` отличается от `x := 0`?
2. Почему можно вернуть &local из функции?
3. Что копируется при вызове функции с аргументом *Account?
4. Чем named type отличается от alias?
5. В каком порядке инициализируются package-level variables?
6. Почему fullPointsData в C++ остаётся не nullptr, но уже недействителен?

49

Thank you

Максим Иванов

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)