Краткий пересказ Effective Go на русском языке

Author: Евгений Охотников
Contact: eao197 at intervale dot ru; eao197 at yahoo dot com
Date: 2009.11.19

Оглавление

Введение и предупреждение от eao197

Я не знаю, зачем я написал этот пересказ. Сначала я хотел написать краткий обзор языка Go на русском языке. Взяв за основу Effective Go и Go for C++ Programmers. Довольно быстро я понял, что лучше, чем это уже сделано на английском, у меня не получится. Поэтому я просто пересказал то, о чем читал. Просто не хотелось бросать незаконченное дело. Теперь текст написан, впечатление о языке я составил. Может быть, кому-то этот текст поможет сделать тоже самое.

Не смотря на то, что Effective Go не является учебником по языку и порядок следования разделов в нем вызывает у меня недоумение, погружение в язык он дает очень хорошее. Нужно просто внимательно читать примеры -- там сосредоточено все самое интересное (параллельное присваивание, удаление элементов из хэш-таблиц, неблокирующее чтение и запись в канал, проверка возможности приведения типов, лямбда-функции и пр.).

Хочу сразу предупредить, что я не пользовался этим языком, и не проверял приводимые в тексте примеры (они один в один взяты из упомянутой документации). Поэтому в моем рассказе могут быть неточности и ошибки.

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

Общая информация о языке

Язык Go является компилируемым в нативный код языком со сборкой мусора и встроенной многопоточностью. Основная ниша языка -- системное программирование.

Язык не поддерживает пространств имен, но поддерживает пакетную структуру.

Язык не поддерживает исключений.

Особенности синтаксиса

Язык имеет C-подобный синтаксис. Однострочные и многострочные комментарии такие же как в C. Нет каких-либо Javadoc или Doxygen-комментариев -- любой комментарий, который предшествует публичной сущности (имени пакета, имени типа, имени функции), будет использован для генерации документации.

Особенности именования сущностей

Имена пакетов

Имена пакетов задаются строчными буквами. Имя пакета соответствует имени каталога, в котором находятся исходные файлы пакета. Для использования функций из какого-то пакета, этот пакет нужно подгрузить в программу с помощью директивы import:

import "bytes" // Такое имя ищется в стандартных путях поиска пакетов.
import "./bytesops" // Такое имя ищется в определенных программистом путях.

Несколько директив import могут быть объеденены в одну:

import (
  "bytes"
  "io/sockets"
  "io/sharedmemory/fast"
)

При обращении к содержимому пакета нужно указывать имя пакета. Например:

fmt.Fprintf(...)
a := io.Open( ... )

Имена сущностей внутри пакетов

Если имя какой-то сущности в пакете начинается с заглавной буквы, то эта сущность будет экспортирована из пакета (т.е. она будет доступна пользователям пакета):

// Первый пакет.
package bytesops

type Buffer []byte

func New() Buffer { ... }

// Второй пакет.
package main

import "bytesops"

func sample() {
  var b bytesops.Buffer = bytesops.New()
  ...
}

Точки с запятой

Точки с запятой используются гораздо реже, чем в C. Они нужны только как разделители предложений (statement-ов) языка. В некоторых случаях точки с запятой можно не указывать:

func CopyInBackground(dst, src chan Item) {
    go func() { for { dst <- <-src } }()
}

или

switch {
case a < b:
    return -1
case a == b:
    return 0
case a > b:
    return 1
}

Управляющие конструкции

В языке Go нет циклов while и do-while. Есть только if, for и switch.

Вокруг условий в if, for и switch нельзя писать круглые скобки. Зато действия (даже если оно одно) должны заключаться в фигурные скобки.

If

Вот так выглядит простой if:

if x > 0 {
    return y
}

Конструкция if может принимать не только условие, но и инициализатор:

if err := file.Chmod(0664); err != nil {
    log.Stderr(err);
    return err;
}

For

Цикл for имеет три формы:

// Как for в C
for init; condition; post { }

// Как while в C
for condition { }

// Как бесконечный цикл (for(;;) или while(1)) в C
for { }

Например:

sum := 0;
for i := 0; i < 10; i++ {
    sum += i
}

При проходе по контейнеру можно использовать конструкцию range:

var m map[string]int;
sum := 0;
for _, value := range m {  // Ключ из map-а игнорируется.
    sum += value
}

В языке Go инкремент является предложением (statement), а не выражением (expression). Т.е. нельзя записать a = i++. Поэтому, если нужно в цикле инкрементировать сразу несколько переменных, то делать это нужно параллельным присваиванием (вида a,b = c,d):

// Обращение содержимого a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
  a[i], a[j] = a[j], a[i]
}

Switch

Конструкция switch в языке Go, не смотря на синтаксическое сходство со switch из C, сильно отличается. Вычисление вариантов case идет сверху вниз. Поэтому метками в case могут быть не только константы, но и условия. Поэтому switch в Go может использоваться вместо серии if-else-if-else:

func unhex(c byte) byte {
    switch {
    case '0' <= c && c <= '9':
        return c - '0'
    case 'a' <= c && c <= 'f':
        return c - 'a' + 10
    case 'A' <= c && c <= 'F':
        return c - 'A' + 10
    }
    return 0
}

В Go нет необходимости писать несколько case для одного действия (как в C серия case с "проваливанием" в последний из них), соответственно, нет необходимости писать break. Так же, в case можно перечислить несколько констант:

func shouldEscape(c byte) bool {
    switch c {
    case ' ', '?', '&', '=', '#', '+', '%':
        return true
    }
    return false
}

В Go switch может использоваться для run-time рефлексии:

switch t := interfaceValue.(type) {
default:
  fmt.Printf("unexpected type %T", type);  // %T prints type
case bool:
  fmt.Printf("boolean %t\n", t);
case int:
  fmt.Printf("integer %d\n", t);
case *bool:
  fmt.Printf("pointer to boolean %t\n", *t);
case *int:
  fmt.Printf("pointer to integer %d\n", *t);
}

Этот пример показывает, что switch, как и if, может принимать инициализатор в качестве параметра.

Функции

Множественные возвращаемые значения

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

func idiv(a, b int) (int, int) {
  d := a / b;
  r := a % b;
  return d, r
}

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

Возвращаемым значениям можно присваивать имена:

func idiv(a, b int) (d, r int) {
  d = a / b;
  r = a % b;
  return;
}

Возврат кодов ошибок

В Go нет исключений. Поэтому об ошибках в работе функции нужно сообщать с помощью кодов возврата. А возможность возврата нескольких значений из функции делает возможным совмещение возврата полезного результата и кода ошибки. Например, функция записи в файл из стандартной библиотеки Go имеет вид:

func (file *File) Write(b []byte) (n int, err Error)

Т.е. если запись была успешной, то возвращается количество записанных байт и нулевое описание ошибки. Если же запись прошла неудачно, что описание ошибки будет ненулевым. Что позволяет писать код вида:

bytesWritten, error := f.Write(data)
if error != nil {
  return error;
}
...

Данные

Напомню, что Go -- это язык со сборкой мусора. Поэтому в нем можно динамически выделять память, но освобождать ее не нужно, т.к. этим занимается сборщик мусора.

Так же в Go есть указатели, но нет адресной арифметики.

Динамическое размещение посредством new()

Конструкция new(T) в языке Go выделяет память для объекта типа T и инициализирует ее нулями. Возвращается значение типа *T, т.е. указатель на T.

Например:

type SyncedBuffer struct {
  lock        sync.Mutex;
  buffer      bytes.Buffer;
}

p := new(SyncedBuffer);

В этом примере p будет указывать на новый объект SyncedBuffer.

Если нужно создать значение SyncedBuffer без динамического выделения памяти, то это делается объявлением переменной типа SyncedBuffer:

var v SyncedBuffer;

У значения v поля lock и buffer будут нулевыми, так же, как и у объекта, на который указывает p.

Конструкторы и составные литералы

Операция new создает объект, инициализированный нулями. Не всегда это подходящий вариант. Если нужно что-то вроде конструктора, то следует создать конструирующую функцию. Например, как это делается в стандартном пакете os:

func NewFile(fd int, name string) *File {
  if fd < 0 {
    return nil
  }
  f := new(File);
  f.fd = fd;
  f.name = name;
  f.dirinfo = nil;
  f.nepipe = 0;
  return f;
}

Но можно сократить количество лишних операций за счет составных литералов:

func NewFile(fd int, name string) *File {
  if fd < 0 {
    return nil
  }
  f := File{fd, name, nil, 0};
  return &f;
}

Две последних строки этого варианта можно еще подсократить. Они эквивалентны следующей строке:

return &File{fd, name, nil, 0};

В составном литерале все атрибуты должны указываться в порядке их перечисления в исходной структуре. Но можно использовать и перечисления вида field:value -- указываются только необходимые атрибуты, а остальные обнуляются:

return &File{fd: fd, name: name}

В предельном случае запись new(File) эквивалентна записи &File{} -- все атрибуты получают нулевые значения.

Составные литералы могут использоваться для инициализации массивов, слайсов и map-ов:

// Массив, чей размер определяется автоматически.
a := [...]string   {Enone: "no error", Eio: "Eio", Einval: "invalid argument"};
// Это слайс.
s := []string      {Enone: "no error", Eio: "Eio", Einval: "invalid argument"};
// Это map (хэш-таблица).
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"};

Размещение с помощью make()

Конструкция make() используется для создания в динамической памяти специальных объектов в языке Go. К таким объектам относятся слайсы, хэш-таблицы (map-ы) и каналы.

Казалось бы, что есть new(T) и make() лишний, но различие очень существенное -- new выделяет память, инициализирует ее нулями и возвращает *T. Тогда как конструкция make(T,args) выделяет память под объект T, инициализирует его должным образом и возвращает T.

eao197. Видимо, все дело в том, что объекты вроде слайсов и каналов -- это простые структуры (дескрипторы). Эти структуры должны быть правильным образом сконструированы. Что и делает make, но не делает new. Такая противоречивость языка, имхо, является одним из самых больших его недостатков.

Например:

make([]int, 10, 100)

создает массив для 100 элементов и возвращает слайс, указывающий на десять первых элементов массива. Тогда как

new([]int)

возвращает пустой слайс.

Вот еще примеры этих различий:

var p *[]int = new([]int);       // Создает пустой слайс.
                                 // Иногда бывает полезна запись *p = nil
var v  []int = make([]int, 100); // v указывает на новый массив из 100 элементов.

// Неоправданно сложные конструкции.
var p *[]int = new([]int);
*p = make([]int, 100, 100);

// А вот так нужно поступать.
v := make([]int, 100);

eao197. В общем, простое правило: слайсы, хэш-таблицы и каналы нужно создавать через make. Все остальное -- через new.

Массивы

Массивы в Go отличаются от массивов в C:

  • Массивы являются значениями. Присваивание одного массива другому означает поэлементное копирование его содержимого.
  • Если массив передается аргументом в функцию, то функция получает копию массива.
  • Размер массива является частью его типа. Так [10]int и [20]int являются разными типами.

Для того, чтобы не терять эффективность при копировании массивов можно использовать указатели на массивы:

func Sum(a *[3]float) (sum float) {
  for _, v := range a {
    sum += v
  }
  return
}

array := [...]float{7.0, 8.5, 9.1};
x := sum(&array);  // Обратите внимание на явное взятие адреса массива.

Однако, в Go рекомендуется использовать слайсы.

Слайсы

Слайсы являются одной из ключевых особенностей языка Go, своеобразной высокоуровневой оберткой над массивами.

eao197. После знакомства с языком D понять, что такое слайсы гораздо проще. Насколько я понимаю, в D и в Go слайс это простая структура вида:

struct slice_t {
  void * ptr;
  size_t capacity;
  size_t start_offs;
  size_t len;
};

Инициализация слайса -- это помещение указателя в полe slice_t::ptr, в capacity заносится общая размерность буфера, в start_off помещается смещение начала слайса относительно ptr, а в len -- длина слайса. Т.о. изменение размера слайса -- это очень дешовая операция.

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

// Функция Read может писать в буфер buf.
func (file *File) Read(buf []byte) (n int, err os.Error)
...
// Вот такой конструкцией из буфера выделяется слайс:
var buf [255]byte;
n, err := f.Read(buf[0:32]);

У слайса есть две характеристики. Первая -- это длина. Получить размер слайса можно посредством встроенной функции len. Вторая характеристика -- это емкость (т.е. максимальный размер массива, на который ссылается слайс). Получить емкость можно посредством встроенной функции cap.

Вот так может выглядеть функция добавления нового значения в слайс:

func Append(slice, data[]byte) []byte {
  l := len(slice);
  if l + len(data) > cap(slice) {     // Недостаточно места.
    // Выделение вдвое большего буфера.
    newSlice := make([]byte, (l+len(data))*2);
    // Остается скопировать данные (можно задействовать bytes.Copy()).
    for i, c := range slice {
      newSlice[i] = c
    }
    slice = newSlice;
  }
  slice = slice[0:l+len(data)];
  for i, c := range data {
    slice[l+i] = c
  }
  return slice;
}

Следует обратит внимание на return slice. Новый слайс нужно возвращать, т.к. сам экземпляр слайса (элемент показанной мной выше структуры slice_t) передается в Append по значению. И, если он изменяется внутри Append, то снаружи Append это не видно.

Хэш-таблицы (map-ы)

Хэш-таблицы (map-ы, аналоги unordered_map из C++) являются встроенными типами в Go. Ключами хэш-таблиц могут быть любые типы, для которых определены операторы равенства (целые, вещественные, строки, указатели и даже интерфесы). Структуры, слайсы и массивы не могут быть ключами.

Как и слайсы, хэш-таблицы являются ссылочными типами. Т.е. если они передаеются в функцию, то функция может изменить содержимое хэш-таблицы.

Хэш-таблицы могут инициализироваться с помощью составных литералов:

var timeZone = map[string] int {
  "UTC":  0*60*60,
  "EST": -5*60*60,
  "CST": -6*60*60,
  "MST": -7*60*60,
  "PST": -8*60*60,
}

Для проверки наличия элемента в массиве используется оператор [], который возвращает значение и булевский признак его наличия в таблице:

func offset(tz string) int {
  // Вот эта конструкция сначала инициализирует булевский ok,
  // а затем проверяет его на равенство true.
  if seconds, ok := timeZone[tz]; ok {
    return seconds
  }
  log.Stderr("unknown time zone", tz);
  return 0;
}

Просто проверить наличие элемента в хэш-таблице можно так:

_, present := timeZone[tz];

Для удаления элемента нужно сделать вот такой фокус:

timeZone["PST"] = 0, false; // Значение false указывает на необходимость
                            // удаления элемента.

Печать

Для форматированной печати в языке Go используются функции, похожие на C-шные printf-ы, но более мощные. Эти функции находятся в пакете fmt, а их имена начинаются с большой буквы: fmt.Printf, fmt.Fprintf, fmt.Sprintf и т.д. Функции семейства Sprintf возвращают строку вместо того, чтобы выполнять печать в какой-то буфер.

Форматная строка не обязательно. Для каждой функции из числа Printf, Fprintf и Sprintf есть аналогичные функции, например Print и Println, которые не используют форматной строки. Вместо этого они отображают значения в формате по умолчанию. Функции с ln на конце добавляют пробелы между аргументами (если аргументы не являются строками) и добавояют перевод строки после вывода всех аргументов. В следующем примере все строки дают один и тот же результат:

fmt.Printf("Hello %d\n", 23);
fmt.Fprint(os.Stdout, "Hello ", 23, "\n");
fmt.Println(fmt.Sprint("Hello ", 23));

В функциях типа fmt.Fprint первым аргументом должен быть объект, который реализует интерфейс io.Writer. Таковыми могут быть os.Stdout и os.Stderr.

Далее идут различия с языком C. Во-первых, числовые форматы, такие как %d, распознают наличие знака у числа и размерность числа по фактическому типу аргумента:

var x uint64 = 1<<64 - 1;
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x));

В результате будет напечатано:

18446744073709551615 ffffffffffffffff; -1 -1

Если вы хотите, чтобы аргумент был напечатан в своем "родном" формате (скажем, int в виде дестичного целого числа), то нужно использовать формат %v (сокращение от "value"). В результате будет получено именно то представление, которое делают функции Print и Println. Более того, этот формат может печатать любые значения, даже массивы, структуры и хэш-таблиц. Вот, например, конструкция для печати таблицы временных зон из предыдущего раздела:

fmt.Printf("%v\n", timeZone);  // or just fmt.Println(timeZone);

В результате будет напечатано:

map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]

Для хэш-таблиц ключи могут быть напечатаны в произвольном порядке. Когда печатается структура модификатор %+v предписывает выводить имя поля перед значением поля. А альтернативный формат %#v печатает значение в синтаксисе Go:

type T struct {
  a int;
  b float;
  c string;
}
t := &T{ 7, -2.35, "abc\tdef" };
fmt.Printf("%v\n", t);
fmt.Printf("%+v\n", t);
fmt.Printf("%#v\n", t);
fmt.Printf("%#v\n", timeZone);

напечатано будет следующее (обратите внимание на амперсанд перед значениями):

&{7 -2.35 abc   def}
&{a:7 b:-2.35 c:abc     def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string] int{"CST":-21600, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}

Для отображения строк, заключенных в двойные кавычки можно использовать %q, который доступен для аргументов типа string или []byte. Альтернативный формат %#q будет использовать, если это возможно, обратные кавычки. Так же, формат %x работает со строками и байтовыми векторами, а так же с целыми, производя длинные строки шестнадцатиричных символов. Если же в формат добавлен пробел (% x), то между байтами расставляются пробелы.

Еще один интересный формат -- это %T, который печатает тип значения:

fmt.Printf("%T\n", timeZone);

на выходе:

map[string] int

Если вы хотите сами предоставить форматирование по умолчанию для собственного типа, то все, что вам нужно -- это определить метод String() string для типа. Скажем, для какого-то простого типа T, этот метод может выглядеть как:

func (t *T) String() string {
  return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c);
}
fmt.Printf("%v\n", t);

Что будет выдавать значения вида:

7/-2.35/"abc\tdef"

Метод String() может вызывать Sprintf потому, что функции печати реентерабильны и могут использоваться рекурсивно. Мы можем пойти даже дальше и передавать функцию печати непосредствено в другую такую же функцию аргументом. Метод Printf использует в своей сигнатуре ... для своего последнего аргумента -- это указывает, что произвольное количество аргументов может быть передано после форматной строки:

func Printf(format string, v ...) (n int, errno os.Error) {

Внутри функции Printf имя v -- это переменная, которая может быть передана, например, в другую функцию печати. Вот реализация функции log.Stderr, которую мы уже использовали ранее. Она передает свои аргументы прямо в fmt.Sprintln, а та уже выполняет все форматирование:

// Stderr -- это вспомогательная функция для облегчения логгирования
// в stderr. Она аналогична Fprint(os.Stderr).
func Stderr(v ...) {
  stderr.Output(2, fmt.Sprintln(v));  // Output принимает (int, string)
}

Инициализация

Константы

Константы получают значения во время компиляции и не могут быть изменены:

const small = 1;
const huge = 100;

Если нужно что-то типа перечисления из C/C++, то используется специальная конструкция iota:

const (
  red = iota; // red == 0
  blue;       // blue == 1
  green;      // green == 2
)

Переменные

Переменные могут изменять свои значения во время работы. Могут объявляться несколькими способами:

// Полная декларация с инициализацией.
var v int = 0;
// Полная декларация без инициализации.
var v int;
// Вывод типа при инициализации.
var v = 0;
// Тоже самое, но еще короче.
v := 0;

Вот различия между объявлением переменных в Go и C++:

/* Go */                  /* C++ */
var v1 int;               // int v1;
var v2 string;            // const std::string v2;  (приблизительно)
var v3 [10]int;           // int v3[10];
var v4 []int;             // int* v4;  (приблизительно)
var v5 struct { f int };  // struct { int f; } v5;
var v6 *int;              // int* v6;  (но в Go нет адресной арифметики)
var v7 map[string]int;    // unordered_map<string, int>* v7; (приблизительно)
var v8 func(a int) int;   // int (*v8)(int a);

Функция init()

Функция init(), если определена в пакете, во время загрузки программы для run-time инициализации. Функция init вызывается после функций init() тех пакетов, которые были импортированны данным пакетом:

var (
  HOME = os.Getenv("HOME");
  USER = os.Getenv("USER");
  GOROOT = os.Getenv("GOROOT");
)
func init() {
  if USER == "" {
    log.Exit("$USER not set")
  }
  if HOME == "" {
    HOME = "/usr/" + USER
  }
  if GOROOT == "" {
    GOROOT = HOME + "/go"
  }
  // GOROOT may be overridden by --goroot flag on command line.
  flag.StringVar(&GOROOT, "goroot", GOROOT, "Go root directory")
}

Методы

Методы выглядят как обычные функции, но имеют специальный первый параметр, называемый receiver. Этот параметр задает объект, для которого будет вызываться метод:

type myType struct { i int }
func (p *myType) get() int { return p.i }

var m myType;
i := m.get()

Методы определяются для именованных типов. Если в результате приведения типа меняется тип объекта, то для нового значения будут вызываться уже методы нового типа.

Методы можно определять даже для встроенных типов, для этого нужно дать встроенному типу новое имя. Тип с новым именем будет считаться уже другим типом

type myInteger int
func (p myInteger) get() int { return int(p) } // Требуется конвертация.
func f(i int) { }
var v myInteger
// Вызов f(v) нелегален.
// Вызов f(int(v)) легален; int(v) не имеет методов.

Указатели vs Значения

Выше уже была определена функция Append, которая позволяла дописывать значения в слайс. Ее можно сделать методом для слайса. Для этого сначала нужно определить именованный тип:

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []slice {
  // Точно такое же тело, как показано ранее.
}

eao197. Здесь не очень понятно, почему тип возвращаемого значения []slice.

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

func (p *ByteSlice) Append(data []byte) {
  slice := *p;
  // Тело такое же, как и раньше, но без return.
  *p = slice;
}

Но можно сделать еще круче: эту функцию можно оформить как стандартный метод Write:

func (p *ByteSlice) Write(data []byte) (n int, err os.Error) {
  slice := *p;
  // Тело точно такое же, как и раньше.
  *p = slice;
  return len(data), nil;
}

Таким образом тип *ByteSlice будет удовлетворять стандартному интерфейсу io.Writer, а это удобно:

var b ByteSlice;
fmt.Fprintf(&b, "This hour has %d days\n", 7);

В Fprintf передается адрес ByteSlice, поскольку только *ByteSlice удовлетворяет интерфейсу io.Writer.

Методы могут быть опеределены как для значений, так и для указателей. Но методы, которые определены для указателей, могут вызываться только для указателей. Сделано так потому, что метод для указателя может модифицировать переданный ему аргумент. И если бы такой метод вызвался для значения, то сделанные им изменения были бы потеряны.

Интерфейсы и другие типы

Интерфейсы

Интерфейсы в Go используются вместо C++ных классов, производных классов и шаблонов. Интерфейс в Go похож на C++ный чистый абстрактный класс: без атрибутов и со всеми чистыми виртуальными методами.

В Go любой тип, для которого опеределены методы, обозначенные в интерфейсе, считается реализующим этот интерфейс. Ничего больше не нужно -- просто реализация методов интерфейса.

Например, пусть есть интерфейс:

type myInterface interface {
  get() int;
  set(i int);
}

Мы можем указать, что наш тип myType реализует этот интерфейс, если определим:

type myType struct { i int }
func (p *myType) get() int { return p.i }
func (p *myType) set(i int) { p.i = i }

И теперь любая функция, которая принимает myInterface, может получать аргументы типа *myType:

func getAndSet(x myInterface) {}
func f1() {
  var p myType;
  getAndSet(&p);
}

Тип может реализовывать несколько интерфейсов. Например, коллекция может поддерживать сортировку с помощью процедур из пакета sort, если она реализует sort.Interface, а так же коллекция может иметь собственное пребразование в строку:

type Sequence []int

// Методы, которые требуются интерфейсом sort.Interface
func (s Sequence) Len() int {
  return len(s)
}
func (s Sequence) Less(i, j int) bool {
  return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
  s[i], s[j] = s[j], s[i]
}

// Метод для печати последовательности. Сначала выполняет ее сортировку.
func (s Sequence) String() string {
  sort.Sort(s);
  str := "[";
  for i, elem := range s {
    if i > 0 {
      str += " "
    }
    str += fmt.Sprint(elem);
  }
  return str + "]";
}

Преобразования

В показанном выше методе Sequence.String выполняется работа, которая уже сделана в стандартном Sprint для слайсов. Для того, чтобы не повторять ее, можно воспользоваться преобразованием в []int перед вызовом Sprint:

func (s Sequence) String() string {
  sort.Sort(s);
  return fmt.Sprint([]int(s));
}

Преобразование предписывает воспринимать s как объект типа []int. Если преобразование не выполнить, то Sprint будет рассматривать s как объект типа Sequence и будет в бесконечной рекурсии вызывать его метод String. А с преобразованием рекурсия разрушается, т.к. вызывается метод String уже для []int.

Преобразование не создает нового значения (хотя преобразования, например, из int во float делают это). Преобразование просто заставляет считать, что объект временно становится объектом другого типа.

Общей идиомой в Go является преобразование типов так, чтобы можно было задействовать разную функциональность. Так, вместо того, чтобы сортировать элементы Sequence с помощью Sort, можно использовать уже готовую сортировку массивов целых чисел за счет преобразования:

type Sequence []int

func (s Sequence) String() string {
  sort.IntArray(s).Sort();
  return fmt.Sprint([]int(s))
}

Теперь тип Sequence вместо реализации нескольких интерфейсов (для сортировки и печати), реализует только один из них.

Обобщение

Если тип только реализует некоторый интерфейс и не экспортирует методов, не входящих в этот интерфейс, то и сам тип можно не экспортировать. Это позволяет не привязываться к конкретным реализациям и менять их, не меняя код работы с интерфейсом.

В этом случае нужно предоставлять конструирующую функцию, которая будет возвращать реализацию интерфейса. Так, в стандартной библиотеке Go функции crc32.NewIEEE() и adler32.New() возвращают интерфейс hash.Hash32.

Аналогичную картину можно видеть в стандартном пакете crypto/block. Благодаря использованию интерфейсов hash.Hash, io.Reader и io.Writer появляется возможность связывать вместе разные алгоритмы шифрования и потоки данных:

type Cipher interface {
  BlockSize() int;
  Encrypt(src, dst []byte);
  Decrypt(src, dst []byte);
}

// NewECBDecrypter возвращает "читателя", который читает данные
// из r и расшифровывает их используя c в режиме
// electronic codebook (ECB).
func NewECBDecrypter(c Cipher, r io.Reader) io.Reader

// NewECBDecrypter возвращает "читателя", который читает данные
// из r и расшифровывает их используя c в режиме
// cipher block chaining (CBC) с инициализирующим вектором iv.
func NewCBCDecrypter(c Cipher, iv []byte, r io.Reader) io.Reader

Интерфейсы и методы

Почти всему можно назначать методы и почти все может соответствовать какому-то конкретному интерфейсу. В качестве примера можно посмотреть на стандарный пакет http, в котором определен интерфейс Handler. Любой объект, который реализует Handler, может обрабатывать HTTP-запросы.

type Handler interface {
  ServeHTTP(*Conn, *Request);
}

Вот тривиальная реализация HTTP-сервера, который подсчитывает количество обращений к нему:

// Simple counter server.
type Counter struct {
  n int;
}

func (ctr *Counter) ServeHTTP(c *http.Conn, req *http.Request) {
  ctr.n++;
  fmt.Fprintf(c, "counter = %d\n", ctr.n);
}

Вот так этот сервер может быть связан с конкретным URL:

import "http"
...
ctr := new(Counter);
http.Handle("/counter", ctr);

Можно обойтись и без структуры Counter. Достаточно просто целого числа для хранения количества обращений. Но метод должен быть определен для указателя на число, чтобы можно было его изменять:

// Simpler counter server.
type Counter int

func (ctr *Counter) ServeHTTP(c *http.Conn, req *http.Request) {
  *ctr++;
  fmt.Fprintf(c, "counter = %d\n", *ctr);
}

Предположим, что нужно информировать какую-то внутреннюю часть приложения о посещении Web-страницы. В этом случае можно воспользоваться каналом:

// Канал, в который посылаются уведомления при каждом визите.
// (Вероятно, канал должен быть буферизированным).
type Chan chan *http.Request

func (ch Chan) ServeHTTP(c *http.Conn, req *http.Request) {
  ch <- req;
  fmt.Fprint(c, "notification sent");
}

Допустим, нужно написать сервер, который будет печатать аргументы командной строки при обращении к нему. Печать аргументов выглядит так:

func ArgServer() {
  for i, s := range os.Args {
    fmt.Println(s);
  }
}

Для того, чтобы преобразовать функцию ArgServer() в HTTP-сервер можно сделать ее методом какого-то типа. Но можно сделать проще. Поскольку метод можно определить для любого типа, за исключением указателей (eao197. Здесь я чего-то недопонимаю. Возможно, речь о том, что если есть type T *int, то нельзя определить метод, который будет получать *T, т.к. это уже указатель на указатель на int.) и интерфейсов, то можно написать метод для функции. В пакете http есть следующий код:

// Тип HandlerFunc является адаптером, который позволяет
// использовать обычную функцию в качестве HTTP-обработчика.
// Если f -- это функция с подходящей сигнатурой, то
// HandlerFunc(f) -- это объект Handler, который вызывает f.
type HandlerFunc func(*Conn, *Request)

// ServeHTTP вызывает f(c, req).
func (f HandlerFunc) ServeHTTP(c *Conn, req *Request) {
  f(c, req);
}

Чтобы преобразовать ArgServer() в HTTP-сервер нужно сначала изменить ее сигнатуру:

func ArgServer(c *http.Conn, req *http.Request) {
  for i, s := range os.Args {
    fmt.Fprintln(c, s);
  }
}

Теперь для того, чтобы превратить ее в HTTP-обработчик нужно воспользоваться преобразованием типа:

http.Handle("/args", http.HandlerFunc(ArgServer));

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

Встраивание (композиция) интерфейсов

Язык Go не поддерживает наследование интерфейсов, принятое в объектных языках. Но позволяет комбинировать интерфейсы посредством их встраивания друг в друга.

Пусть есть два интерфеса -- io.Reader и io.Writer:

type Reader interface {
  Read(p []byte) (n int, err os.Error);
}

type Writer interface {
  Write(p []byte) (n int, err os.Error);
}

Если нужен интерфейс ReadWriter, то можно создать его путем встраивания в него интерфейсов Reader и Writer:

// Интерфейс ReadWrite группирует методы из базовых
// интерфейсов Read и Write.
type ReadWriter interface {
  Reader;
  Writer;
}

Интерфейс ReadWriter является объединением методов из Reader и Writer (которое должно строится из непересекающихся множеств методов /eao197. Я так понимаю, что во встраиваемых интерфейсов не может быть одинаковых методов./). Только интерфейсы могут встраиваться внутрь интерфейсов.

Аналогичную операцию можно сделать и для структур. Например, в стандартном пакете bufio есть структуры Reader и Writer, и есть структура bufio.ReadWriter, которая встраивает Reader и Writer в виде безымянных полей:

// ReadWriter хранит указатели на Reader и Writer.
// ReadWriter реализует интерфейс io.ReadWriter.
type ReadWriter struct {
  *Reader;
  *Writer;
}

Эту структуру можно было бы записать как:

type ReadWriter struct {
  reader *Reader;
  writer *Writer;
}

Но тогда для реализации метода Read пришлось бы обращаться к полю reader:

func (rw *ReadWriter) Read(p []byte) (n int, err os.Error) {
  return rw.reader.Read(p)
}

А если Reader и Writer встраиваются как безымянные поля, то для ReadWriter вообще не нужно реализовывать методов -- все уже имеющиеся методы для Reader и Writer будут работать и для ReadWriter. Т.о. благодаря встраиванию структура ReadWriter автоматически стала поддерживать сразу три интерфейса: io.Reader, io.Writer и io.ReadWriter.

Встраивание имеет важное отличие от наследование. Если вызывается метод для встроенной структуры (например, Read из Reader), то аргументом-receiver-ом будет не структура ReadWriter, а только структура Read, а не ReadWriter. Т.е. запись:

type ReadWriter struct {
  *Reader;
  *Writer;
}
var rw ReadWriter;
rw.Read( ... );

Эквивалентна:

type ReadWriter struct {
  reader *Reader;
  writer *Writer;
}
var rw ReadWriter;
rw.reader.Read( ... );

При встраивании в структуры можно сочетать объявление обычных полей и встраивание структур. Например:

type Job struct {
  Command     string;
  *log.Logger;
}

Тепрь тип Job имеет методы Log, Logf и остальные методы из log.Logger. И объект Job может использоваться для логирования:

job.Log("starting now...");

Logger -- это обычное поле структуры и может инициализироваться обычным образом:

func NewJob(command string, logger *log.Logger) *Job {
  return &Job{command, logger}
}

К полю Logger можно обращаться по имени Logger (при встраивании структур имя встроенного типа, но без имени пакета, будет являться именем поля):

func (job *Job) Logf(format string, args ...) {
  job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args));
}

Встраивание может порождать конфликты имен полей. Эти конфликты разрешаются с помощью простых правил.

Во-первых, имя X скрывает любое другое имя X, которое находится глубже в цепочке встраивания. Скажем, если в log.Logger есть поле Command, то оно будет скрыто полем Job.Command.

Во-вторых, если имена полей совпадают на одинаковом уровне вложенности, то это обычно свидетельствует об ошибке. Например, скорее всего ошибочно встраивать log.Logger в Job, если в Job уже есть поле Logger. Однако, если имя дубликата не используется нигде за пределами объявления типа, то ошибка не порождается. Это некоторая защита на случай, когда в тип log.Logger добавляется какое-то поле, которое уже есть в типе, в который log.Logger был встроен.

Конкурентность

Goroutines

Язык Go поддерживает goroutines -- возможность запуска функций в виде отдельной нити. При этом goroutines отображаются на потоки ОС (поэтому, если какой-то поток окажется заблокированным операцией ввода-вывода, то остальные потоки смогут продолжить свою работу). Создание goroutines должно быть быстрым и дешевым.

Для запуска goroutine необходимо указать ключевое слово go перед обращением к функции или методу. После завершения работы вызванной функции/метода goroutine завершается.

go list.Sort();  // Запустить list.Sort в другом потоке;
                 // не ждать его завершения.

При запуске goroutines могут использоваться function literals:

func Announce(message string, delay int64) {
  go func() {
    time.Sleep(delay);
    fmt.Println(message);
  }()  // Круглые скобки здесь важны -- делается вызов функции.
}

Function literals в языке Go играют роль замыканий. Реализация языка заботиться о том, чтобы все объекты, на которые ссылается function literal, оставались действительными пока function literal активен.

Приведенные выше примеры не имеют большого практического смысла, т.к. в них не сигналов завершения работы. Для этих целей предназначены каналы.

Каналы

Каналы в языке Go используют идеи из Хоаровской Communicating Sequential Processes (CSP) и могут рассматриваться как типобезопасная реализация Unix-овых каналов (пайпов).

Каналы в Go комбинируют и обмен значениями, и синхронизацию между goroutine-ами.

Каналы, как и хэш-таблицы, создаются посредством make(). Если в make() передается необязательный целочисленный параметр, то он устанавливает размер буфера для канала. По умолчанию размер буфера нулевой -- это означает небуферизированный или синхронный канал.

ci := make(chan int);            // небуферизированный канал целых чисел
cj := make(chan int, 0);         // небуферизированный канал целых чисел
cs := make(chan *os.File, 100);  // буферизированный канал указателей на файлы.

Каналы могут использоваться в реализации множества идиом. Демонстрацию которых можно начать с предыдущего примера выполнения сортировки в goroutine. Канал позволит инициатору сортировки определить момент ее завершения:

c := make(chan int);  // Создание канала.
// Сортировка запускается в отдельном потоке.
// Когда она завершается в канал записывается сигнал.
go func() {
    list.Sort();
    c <- 1;  // Отсылка сигнала. Не важно, какое это будет значение.
}();
doSomethingForAWhile();
<-c;   // Ожидание завершения сортировки. Прочитанное из канала значение
       // просто выбрасывается.

Читатель из канала блокируется всегда, если в канале нет данных для чтения. Если канал небуферизированный, то писатель в канал будет заблокирован до момента, пока читатель не прочитает данные из канала (eao197. Я так понимаю, что если писатель пишет в канал первый раз, то он не блокируется. Но если он пишет в канал, значение из которого еще не прочитано, то тогда писатель заблокируется). Если канал буферизирован, то писатель блокируется на момент копирования значения в буфер. Если же буфер заполнен, то писатель блокируется, пока какой-нибудь из читателей не прочитает значение.

Буферизированный канал может использоваться как семафор. В следующем примере входящие запросы передаются в handle, который посылает сигнал в канал, обрабатывает запрос, а затем получает значение из канала. Размер канала определяет количество запросов, которые могут обрабатываться одновременно:

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1;    // Ожидание очистки активной очереди.
    process(r);  // Может занять много времени.
    <-sem;       // Все, разрешаем выполнение следующего запроса.
}

func Serve(queue chan *Request) {
    for {
        req := <-queue;
        go handle(req);  // Не ждем завершения обработки.
    }
}

В следующем примере эта же задача решается по другому: Serve сразу стартует несколько goroutines, а каждый handle сам читает запросы из очереди. Serve() так же получает канал булевых значений, из которого он вычитывает сигнал для завершения собственной работы.

func handle(queue chan *Request) {
  for r := range queue {
    process(r);
  }
}

func Serve(clientRequests chan *clientRequests, quit chan bool) {
  // Запуск обработчиков.
  for i := 0; i < MaxOutstanding; i++ {
    go handle(clientRequests)
  }
  <-quit;     // Ожидание сигнала для завершения работы.
}

Каналы каналов

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

В примере из предыдущего раздела функция handle была "идиализированным" обработчком запросов, но не было определено, какие типы запросов она обрабатывает. В тип запроса можно включить, в том числе, и канал для отсылки ответа клиенту. Вот схематическое определение подобного типа запроса:

type Request struct {
    args  []int;
    f    func([]int) int;
    resultChan        chan int;
}

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

func sum(a []int) (s int) {
  for _, v := range a {
    s += v
  }
  return
}

request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Отсылка запроса.
clientRequests <- request;
// Ожидание ответа.
fmt.Printf("answer: %d\n", <-request.resultChan);

На стороне сервера изменяется только реализация функции handle:

func handle(queue chan *Request) {
  for req := range queue {
    req.resultChan <- req.f(req.args);
  }
}

Понятно, что это только схема реального решения. Но она может использоваться для разработки RPC-системы с ограничениями на пропускную способность, с распараллеливанием и пр., и все это без единого mutex-а.

Распараллеливание

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

Допустим, нам нужно выполнить "тяжелую" операцию над вектором элементов. И, как в идеальном примере, результаты операции над каждым элементом не зависят друг от друга.

type Vector []float64

// Выполнение операции над n элементами вектора v,
// начиная с позиции i.
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1;    // сигнализация завершения обработки элементов.
}

Мы запускаем обработку частей вектора независимо друг от друга в цикле, по одной на CPU. Обработка может завершиться в произвольном порядке, но нам это не важно. Мы всего лишь подсчитываем сигналы завершения обработки, вычитывая канал после запуска всех goroutine-ов.

const NCPU = 4        // Количество доступных CPU.

func (v Vector) DoAll(u Vector) {
    c := make(chan int, NCPU);  // Буферизация не обязательна,
                                // но может иметь смысл.
    for i := 0; i < NCPU; i++ {
        go v.DoSome(i*len(v)/NCPU, (i+1)*len(v)/NCPU, u, c);
    }
    // Вычитываем все из канала.
    for i := 0; i < NCPU; i++ {
        <-c    // Ждем, пока завершиться очередная операция.
    }
    // Все действия завершены.
}

"Текущий буфер"

Инструмент для конкурентного программирования может даже упростить реализацию не конкурентных вещей. Вот пример, который был выделен из пакета RPC. Функция client, работающая как goroutine, вычитывается откуда-то данные (например, из сети). Для того, чтобы не выделять и не освобождать буфера, она держит список свободных буферов и использует буферизированный канал для его представления. Если канал пуст, то выделяется новый буфер. Когда очередное сообщение сформировано, оно отсылается серверу через канал serverChan.

var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)

func client() {
  for {
    b, ok := <-freeList;  // Получить буфер, если есть в наличии.
    if !ok {              // Если буфера нет, то он создается.
      b = new(Buffer)
    }
    load(b);              // Очередное сообщение прочитано из сети.
    serverChan <- b;      // Отсылка на сервер.
  }
}

Внутри server в цикле считываются сообщения от клиента, обрабатываются и буфер с сообщением возвращается в список свободных буферов.

func server() {
  for {
    b := <-serverChan;    // Ожидание следующего сообщения.
    process(b);
    _ = freeList <- b;    // Возврат буфера, если есть место в списке.
  }
}

Функция client использует неблокирующее чтение из freeList. Если свободный буфер есть, то он забирается из канала. В противном случае создается новый буфер. Внутри server используется неблокирующая запись в freeList для возвращения буфера в список свободных буферов. Если же канал freeList полон, то буфер просто выбрасывается и уничтожается впоследствии сборщиком мусора. (Присваивание переменной _ в операции <- делает операцию записи в канал неблокирующей. Но результат этой операции просто игнорируется). Этот пример показывает "текущий буфер", реализованный всего несколькими строчками, за счет каналов и сборщика мусора.

Ошибки

Библиотечные функция часто должны возвращать какой-то индикатор ошибки вызывающей стороне. Как упоминалось выше, в языке Go легко возвращать детальное описание ошибки вместе с нормальным возвращаемым значением. По соглашению описания ошибок имеют тип os.Error -- это простой интерфейс:

type Error interface {
    String() string;
}

Автор библиотеки может реализовать этот интерфейс по своему. При желании можно сделать так, чтобы видеть не только описание ошибки, но и некоторый контекст, связанный с ошибкой. Например, os.Open возвращает os.PathError:

// PathError хранит описание ошибки, имя операции и
// имя файла, с которым связана ошибка.
type PathError struct {
  Op string;    // "open", "unlink", и т.д.
  Path string;  // Имя файла.
  Error Error;  // Ошибка, возвращенная системным вызовом.
}

func (e *PathError) String() string {
  return e.Op + " " + e.Path + ": " + e.Error.String();
}

Метод String из PathError генерирует сообщение вида:

open /etc/passwx: no such file or directory

Если вызывающей стороне нужно проверить конкретный тип возвращенной ошибки, то она может это сделать посредством проверки типа:

for try := 0; try < 2; try++ {
  file, err = os.Open(filename, os.O_RDONLY, 0);
  if err == nil {
    return
  }
  if e, ok := err.(*os.PathError); ok && e.Error == os.ENOSPC {
    deleteTempFiles();  // Освободить место на диске.
    continue
  }
  return
}

eao197. Конструкция e, ok := err.(*os.PathError), по-видимому, является хитрой проверкой возможности приведения типа. Если приведение типа возможно, то в e оказывается ссылка на экземпляр типа os.PathError, а в ok -- значение true. Если приведение невозможно, то в e будет nil, а в ok -- false.

Веб-сервер

В качестве примера завершенной Go программы рассмотрим web-сервер.

Google предоставляет сервис http://chart.apis.google.com, который выполняет автоматическое преобразование данных в графики. Его тяжело использовать интерактивно, т.к. данные должны помещаться в URL. Приведенная ниже программа упрощает это. Когда она получает короткий кусок текста, она вызывает этот сервис Google для формирования QR-кода.

Вот полный текст программы. Объяснения идут после нее.

package main

import (
  "flag";
  "http";
  "io";
  "log";
  "strings";
  "template";
)

var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18
var fmap = template.FormatterMap{
  "html": template.HTMLFormatter,
  "url+html": UrlHtmlFormatter,
}
var templ = template.MustParse(templateStr, fmap)

func main() {
  flag.Parse();
  http.Handle("/", http.HandlerFunc(QR));
  err := http.ListenAndServe(*addr, nil);
  if err != nil {
    log.Exit("ListenAndServe:", err);
  }
}

func QR(c *http.Conn, req *http.Request) {
  templ.Execute(req.FormValue("s"), c);
}

func UrlHtmlFormatter(w io.Writer, v interface{}, fmt string) {
  template.HTMLEscape(w, strings.Bytes(http.URLEscape(v.(string))));
}


const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{.section @}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={@|url+html}"
/>
<br>
{@|html}
<br>
<br>
{.end}
<form action="/" name=f method="GET"><input maxLength=1024 size=70
name=s value="" title="Text to QR Encode"><input type=submit
value="Show QR" name=qr>
</form>
</body>
</html>
`

Части, предшествующие main должны быть просты для понимания. Переменная addr задает параметр командной строки для указания номера HTTP порта сервера. Переменная templ содержит самое интересное. Она формирут HTML шаблон, который будет обработан сервером для отображения странички, подробнее об этом чуть позже.

Функция main разбирает аргументы командной строки и, используя описанные выше механизмы, связывает функцию QR с корневым каталогом на сервере. Затем вызвается http.ListerAndServe для запуска сервера. Она блокирует main пока сервер работает.

QR просто получает запрос, который хранит данные из формы, и выполняет обработки шаблона с данными, которые содержатся в элементе формы с именем s.

Пакет template, созданный по подобию json-template, достаточно мощен для того, чтобы программа могла просто использовать его возможности. Он просто перезаписывает фрагменты текста на лету, заменяя его части на данные, которые были переданы в templ.Execute. В данном случае -- это данные из HTTP формы. Внутри текста шаблона (templateStr) залюченные в скобки маркеры обозначают действия над шаблоном. Фрагмент между {.section @} и {.end} выполняется с использованием значения элемента данных @; это сокращение для текущего элемента, коим является элемент из HTTP-формы. (Если же @ пуст, то такой фрагмент просто выбрасывается из шаблона.)

Маркер {@|url+html} предписывает пропустить данные через преобразователь, который установлен в таблице преобразователей (fmap) под именем url+html). В данном случае это функция UrlHtmlFormatter, которая преобразует строку так, чтобы ее можно было отобразить на Web-странице.

Остальная часть шаблона -- это просто HTML, который отображается при загрузке страницы. Все это слишком короткое описание, за более подробным следует обратиться в документацию по пакету template.

Вот, собственно, и все: функциональный Web-сервер в нескольких строчках кода + некоторый событийный преобразователь HTML-текста. Язык Go достаточно мощен для того, чтобы сделать многое всего несколькими строчками.

Hosted by uCoz