Автоматическая вставка точки с запятой в Go

Формальная грамматика определяет, что составляет синтаксически допустимую программу на Go (или другом языке программирования):

Block = "{" StatementList "}" .
StatementList = { Statement ";" } .

Приведенные выше определения взяты из спецификации Go. Они используют расширенную форму Бэкуса-Наура (EBNF). Все это означает, что блок кода - это одно или несколько операторов, разделенных точкой с запятой. Вызов функции является примером оператора. Зная, что мы можем создать простой блок:

{
    fmt.Println(1);
    fmt.Println(2);
}

Опытные суслики должны заметить точки с запятой, которые не используются в конце каждой строки в идиоматическом коде. Его можно упростить до:

{
    fmt.Println(1)
    fmt.Println(2)
}

Такой код работает так же, как и первый. Но что делает это возможным, если грамматика требует точки с запятой?

Корни

Почему разработчики языков даже начали работать над избавлением от токенов, таких как точки с запятой? Ответ довольно прост. Все дело в удобочитаемости. Чем меньше в коде артефактов, тем проще с ним работать. Это важно, поскольку однажды написанный фрагмент кода, вероятно, будет много раз прочитан разными людьми.

Грамматика использует точки с запятой как терминаторы продукции. Поскольку цель состоит в том, чтобы освободить программиста от ввода этих точек с запятой, должен быть способ их автоматического ввода. Это то, что делает лексер Go. Точка с запятой добавляется, когда последний токен строки является одним из:

Приведем пример:

func g() int {
    return 1
}
func f() func(int) {
    return func(n int) {
        fmt.Println("Inner func called")
    }
}

Имея такие определения, мы можем проанализировать два сценария:

f()
(g())

а также:

f()(g())

Первый фрагмент ничего не печатает, а второй даетInner func called. Это из-за 4-го вышеупомянутого правила - точки с запятой были добавлены после обеих строк, поскольку последние токены закрывают круглые скобки:

f();
(g());

Под капотом

Добавление точек с запятой в Golang происходит при лексическом анализе (сканировании). Это в самом начале обработки файла .go, когда символы преобразуются в токены, такие как идентификаторы, числа, ключевые слова и т. Д. Сканер реализован в самом Go, поэтому мы можем легко его использовать:

package main
import (
    "fmt"
    "go/scanner"
    "go/token"
)
func main() {
    scanner := scanner.Scanner{}
    source := []byte("n := 1\nfmt.Println(n)")
    errorHandler := func(_ token.Position, msg string) {
        fmt.Printf("error handler called: %s\n", msg)
    }
    fset := token.NewFileSet()
    file := fset.AddFile("", fset.Base(), len(source))
    scanner.Init(file, source, errorHandler, 0)
    for {
        position, tok, literal := scanner.Scan()
        fmt.Printf("%d: %s", position, tok)
        if literal != ""{
            fmt.Printf(" %q", literal)
        }
        fmt.Println()
        if tok == token.EOF {
            break
        }
    }
}

Выход:

1: IDENT "n"
3: :=
6: INT "1"
7: ; "\n"
8: IDENT "fmt"
11: .
12: IDENT "Println"
19: (
20: IDENT "n"
21: )
22: ; "\n"
22: EOF

Строки печати ; "\n" - это места, где сканер (лексер) добавляет точки с запятой для программы:

n := 1
fmt.Println(n)

Golangspec насчитывает более 300 последователей. Это не было самоцелью, но все же очень воодушевляет знать, что все больше и больше людей видят эту публикацию полезной.

Нажмите ❤ ниже, чтобы помочь другим узнать эту историю. Если вы хотите получать обновления о новых сообщениях, подписывайтесь на меня.

Ресурсы