понедельник, 22 ноября 2021 г.

Go для Java разработчиков: горутины и каналы

Горутины

Go позволяет запустить новый поток выполнения, горутину (goroutine), с помощью оператора go. Он запускает функцию в другой, недавно созданной горутине. Все горутины в одной программе используют одно и то же адресное пространство.

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

go list.Sort() // Выполнить list.Sort параллельно.

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

// Publish печатает текст на стандартный вывод 
// по истечении заданного времени.
func Publish(text string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(text)
    }() // Обратите внимание на круглые скобки. 
        // Мы должны вызвать функцию.
}

Переменные text и delay распределяются между окружающей функцией и литералом функции; они выживают, пока доступны.

Каналы

Канал предоставляет механизм для двух горутин для синхронизации выполнения и обмена данными, передавая значение определенного типа элемента. Оператор <- указывает направление канала, отправка или получение. Если направление не указано, канал двунаправленный.

chan Sushi     // может использоваться для отправки и получения значений типа Sushi
chan<- float64 // может использоваться только для отправки float64
<-chan int     // можно использовать только для получения целых чисел

Каналы являются ссылочным типом и назначаются с помощью make.

ic := make(chan int)       // небуферизованный канал целых чисел
wc := make(chan *Work, 10) // буферизованный канал указателей на Work

Чтобы отправить значение по каналу, используйте <- в качестве бинарного оператора. Чтобы получить значение по каналу, используйте его как унарный оператор.

ic <- 3      // Посылаем 3 по каналу.
work := <-wc // Получение указателя на Work из канала.

  • Если канал не буферизован, отправитель блокируется до тех пор, пока получатель не получит значение.
  • Если канал имеет буфер, отправитель блокируется только до тех пор, пока значение не будет скопировано в буфер; если буфер заполнен, это означает ожидание, пока какой-либо получатель не получит значение.
  • Получатели блокируются до тех пор, пока не появятся данные для приема.

Функция close записывает, что по каналу больше не будут отправляться значения:

  • После вызова close и после получения любых ранее отправленных значений операции приема вернут нулевое значение без блокировки.
  • Операция приема с несколькими значениями дополнительно возвращает указание, закрыт ли канал.

ch := make(chan string)
go func() {
    ch <- "Hello!"
    close(ch)
}()
fmt.Println(<-ch) // Печатает "Hello!".
fmt.Println(<-ch) // Распечатать нулевое значение "" без блокировки.
fmt.Println(<-ch) // Еще раз печатаем "".
v, ok := <-ch     // v - "", ok - false.

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

// Publish печатает текст на стандартный вывод по истечении заданного времени.
// Закрывает канал wait, когда текст был опубликован.
func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    go func() {
        time.Sleep(delay)
        fmt.Println(text)
        close(ch)
    }()
    return ch
}

Вот как вы можете использовать эту Publish функцию.

wait := Publish("важные новости", 2 * time.Minute)
// Проделаем еще немного работы.
<-wait // блокируется, пока текст не будет опубликован

Оператор select

Оператор select - последний инструмент в наборе инструментов конкурентности Go. Он выбирает, какая из возможных коммуникаций будет продолжена:

  • если какой-либо из case может продолжаться, один из них выбирается случайным образом и выполняются соответствующие операторы;
  • в противном случае, если нет default case, инструкция блокируется до тех пор, пока не будет завершен один из case.

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

rand := make(chan int)
for { // Посылаем случайную последовательность битов в rand.
    select {
    case rand <- 0: // примечание: нет инструкции
    case rand <- 1:
    }
}

Более реалистично, вот как можно использовать оператор select для установки ограничения по времени для операции приема.

select {
case news := <-AFP:
    fmt.Println(news)
case <-time.After(time.Minute):
    fmt.Println(Тайм-аут: нет новостей в течение одной минуты.)
}

Функция time.After является частью стандартной библиотеки; она ожидает истечения указанного времени, а затем отправляет текущее время по возвращенному каналу.


Читайте также:


Купить gopher

четверг, 18 ноября 2021 г.

Go для Java разработчиков: ошибки, паника и восстановление

Ошибки

Там, где Java обычно использует исключения, Go имеет два разных механизма:

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

Многозначный возврат Go позволяет легко вернуть подробное сообщение об ошибке вместе с обычным возвращаемым значением. По соглашению такие сообщения имеют тип error, простой встроенный интерфейс.

type error interface {
    Error() string
}

Например, функция os.Open возвращает ненулевое значение ошибки, когда не удается открыть файл.

func Open(name string) (file *File, err error)

В следующем коде для открытия файла используется os.Open. Если возникает ошибка, он вызывает log.Fatal, чтобы распечатать сообщение об ошибке и остановиться.

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}
// что-то делаем с открытым *File f

Интерфейс error требует только метода Error, но конкретные реализации ошибок часто имеют дополнительные методы, позволяющие вызывающим абонентам проверять подробности ошибки.

Паника и восстановление

Паника (panic) - это ошибка времени выполнения, которая раскручивает стек горутины, по пути запускает любые отложенные функции, а затем останавливает программу.

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

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

  • вызов recover останавливает раскрутку и возвращает аргумент, переданный панике.

Поскольку единственный код, который выполняется при раскручивании, находится внутри отложенных (defer) функций, recover полезно только внутри отложенных функций. Если горутина не паникует, recover возвращает nil.


Читайте также:


среда, 17 ноября 2021 г.

Go для Java разработчиков: методы и интерфейсы

Метод выглядит как обычное определение функции, за исключением того, что у него есть получатель. Получатель аналогичен ссылке this в методе экземпляра Java.

type MyType struct { i int }

func (p *MyType) Get() int {
    return p.i
}

var pm = new(MyType)
var n = pm.Get()

Это объявляет метод Get, связанный с MyType. Получатель назван p в теле функции.

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

Вы можете определить методы для встроенного типа, объявив новый определенный тип, производный от него. Новый тип отличается от встроенного.

type MyInt int

func (p MyInt) Get() int {
    return int(p) // Требуется преобразование.
}

func f(i int) {}
var v MyInt

v = v * v // Операторы базового типа все еще применяются.
f(int(v)) // int(v) не имеет объявленных методов.
f(v)      // INVALID

Интерфейсы

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

Предположим, что этот интерфейс определен:

type MyInterface interface {
    Get() int
    Set(i int)
}

Поскольку MyType уже имеет метод Get, мы можем заставить MyType удовлетворить интерфейс, добавив

func (p *MyType) Set(i int) {
    p.i = i
}

Теперь любая функция, которая принимает MyInterface в качестве параметра, будет принимать переменную типа *MyType.

func GetAndSet(x MyInterface) {}

func f1() {
    var p MyType
    GetAndSet(&p)
}

В терминах Java определение Set и Get для *MyType привело к тому, что *MyType автоматически реализует MyInterface. Тип может удовлетворять нескольким интерфейсам. Это форма неявной типизации (утиная типизация (англ. Duck typing)).

Когда я вижу птицу, которая ходит, как утка, плавает, как утка, и крякает, как утка, я называю эту птицу уткой. - Джеймс Уиткомб Райли

Встраивание (делегирование)

Встраивание типа как анонимного поля может использоваться для реализации формы подтипов.

type MySubType struct {
    MyType
    j int
}

func (p *MySubType) Get() int {
    p.j++
    return p.MyType.Get()
}

Это реализация MySubType как подтипа MyType.

func f2() {
    var p MySubType
    GetAndSet(&p)
}

Метод Set унаследован от MyType, поскольку методы, связанные с анонимным полем, повышаются до методов включающего типа.

В этом случае, поскольку MySubType имеет анонимное поле типа MyType, методы MyType также становятся методами MySubType. Метод Get был переопределен, а метод Set был унаследован.

Это не то же самое, что подкласс в Java, а форма делегирования. Когда вызывается метод анонимного поля, его получателем является поле, а не окружающая структура. Другими словами, методы анонимных полей не отправляются динамически. Если вам нужен эквивалент динамического поиска метода Java, используйте интерфейс.

func f3() {
    var v MyInterface

    v = new(MyType)
    v.Get() //  Вызов метода Get для *MyType.

    v = new(MySubType)
    v.Get() // Вызов метода Get для *MySubType.
}

Утверждения типа

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

type Printer interface {
    Print()
}

func f4(x MyInterface) {
    x.(Printer).Print() // утверждения типа для Printer
}

Преобразование в Printer полностью динамическое. Оно будет работать до тех пор, пока динамический тип x (фактический тип значения, хранящегося в x) определяет метод Print.


Читайте также:


четверг, 11 ноября 2021 г.

Go для Java разработчиков: срезы, создание значений

Срезы

Срез - это концептуально структура с тремя полями:

  • указатель на массив,
  • длина,
  • и емкость

Срезы поддерживают оператор [] для доступа к элементам базового массива.

  • Встроенная функция len возвращает длину среза.
  • Встроенная функция cap возвращает емкость.

Для данного массива или другого среза a новый срез создается через a[i:j].

  • Это создает новый срез, который ссылается на a, начинается с индекса i и заканчивается перед индексом j.
  • Имеет длину j - i.
  • Если i не указан, срез начинается с 0.
  • Если j опущен, срез заканчивается на len(a).

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

Емкость нового среза - это просто емкость минус i. Емкость массива - это длина массива.

var s []int
var a [10]int

s = a[:] // сокращение от s = a[0:len(a)]

Если вы создаете значение типа [100]byte (массив из 100 байтов, возможно, буфер) и хотите передать его функции, не копируя его, объявите параметр функции как тип []byte и передайте срез массива. Срезы также можно создавать с помощью функции make, как описано ниже.

Срезы в сочетании со встроенной функцией append предлагают во многом те же функции, что и ArrayList в Java.

s0 := []int{1, 2}
s1 := append(s0, 3)     // добавляем один элемент
s2 := append(s1, 4, 5)  // добавляем несколько элементов
s3 := append(s2, s0...) // добавляем срез

Синтаксис среза также можно использовать со строкой. Он возвращает новую строку, значение которой является подстрокой исходной строки.

Создание значений

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

make(map[string]int)

возвращает вновь выделенное значение типа map[string]int.

В отличие от new, make возвращает фактический объект, а не адрес. Это согласуется с тем фактом, что карты и каналы являются ссылочными типами.

Для карт make принимает подсказку о емкости в качестве второго необязательного аргумента.

Для каналов есть необязательный второй аргумент, который устанавливает емкость буферизации канала; по умолчанию - 0 (без буферизации).

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

m := make([]int, 10, 20) // То же, что и new([20]int)[:10]


Читайте также:


среда, 3 ноября 2021 г.

Go для Java разработчиков: константы, структуры, указатели

Константы

В Go константы могут быть нетипизированными. Это относится к

  • числовым литералам,
  • выражениям, использующим только нетипизированные константы,
  • и объявлениям const, в которых тип не указан, а выражение инициализатора не типизировано.

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

var a uint
f(a + 1)   // Нетипизированная числовая константа 1 
           // становится типизированной как uint.
f(a + 1e3) // 1e3 также печатается как uint.

Язык не накладывает никаких ограничений на размер нетипизированной числовой константы. Предел применяется только тогда, когда константа используется там, где требуется тип.

const huge = 1 << 100
var n int = huge >> 98

Если тип отсутствует в объявлении переменной и соответствующее выражение оценивается как нетипизированная числовая константа, константа преобразуется в тип rune, int, float64 или complex128 соответственно, в зависимости от того, является ли значение константы символом, целым числом или числом с плавающей запятой, или комплексным числом.

c := 'å'    // rune (псевдоним для int32)
n := 1 + 2  // int
x := 2.7    // float64
z := 1 + 2i // complex128

В Go нет типов перечисления (enum). Вместо этого вы можете использовать специальное имя iota в одном объявлении const, чтобы получить серию возрастающих значений. Когда выражение инициализации опускается для константы, оно повторно использует предыдущее выражение.

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

Структуры

Структура соответствует классу в Java, но членами структуры не могут быть методы, только переменные. Указатель на структуру похож на ссылочную переменную в Java. В отличие от классов Java, структуры также могут быть определены как прямые значения. В обоих случаях вы используете . (точку) для доступа к членам структуры.

type MyStruct struct {
    s string
    n int64
}

var x MyStruct     // x инициализируется MyStruct{"", 0}.
var px *MyStruct   // px инициализируется nil.
px = new(MyStruct) // px указывает на новую структуру MyStruct{"", 0}.

x.s = "Foo"
px.s = "Bar"

В Go методы могут быть связаны с любым определяемым пользователем типом, а не только со структурами.

Указатели

Если у вас есть int, структура или массив, присваивание копирует содержимое объекта. Чтобы добиться эффекта ссылочных переменных Java, Go использует указатели.

Для любого типа T существует соответствующий тип указателя *T, обозначающий указатели на значения типа T.

Чтобы выделить память для переменной-указателя, используйте встроенную функцию new, которая принимает тип и возвращает указатель на выделенное хранилище. Выделенное пространство будет инициализировано нулевым значением для типа. Например, new(int) выделяет хранилище для нового int, инициализирует его значением 0 и возвращает его адрес, который имеет тип *int.

Код Java T p = new T(), где T - класс с двумя переменными экземпляра a и b типа int, соответствует

type T struct { a, b int }
var p *T = new(T)

или, что более идиоматично

p := new(T)

Объявление var v T, в котором объявляется переменная, содержащая значение типа T, не имеет эквивалента в Java. Значения также могут быть созданы и инициализированы с помощью составного литерала. Например:

v := T{1, 2}

эквивалентно

var v T
v.a = 1
v.b = 2

Для операнда x типа T оператор адреса &x дает адрес x, значение типа *T. Например:

p := &T{1, 2} // p имеет тип *T

Для операнда x типа указателя косвенное обращение к указателю *x обозначает значение, на которое указывает x. Косвенные указатели используются редко; Go, как и Java, может автоматически принимать адрес переменной.

p := new(T)
p.a = 1 // эквивалент (*p).a = 1


Читайте также:


вторник, 2 ноября 2021 г.

Go для Java разработчиков: синтаксис

Декларации

Синтаксис объявления обратный по сравнению с Java. Вы пишете имя, а затем тип. Объявления типов можно легко читать слева направо.

Go Приблизительный эквивалент Java
var v1 int int v1 = 0;
var v2 *int Integer v2 = null;
var v3 string String v3 = "";
var v4 [10]int int[] v4 = new int[10];
(Массивы - это значения в Go.)
var v5 []int int[] v5 = null;
var v6 *struct{ a int } class C { int a; }
C v6 = null;
var v7 map[string]int HashMap v7;
v7 = null;
var v8 func(a int) int interface F {
int f(int a);
}
F v8 = null;

Объявления обычно принимают форму ключевого слова, за которым следует имя объявляемого объекта. Ключевое слово может иметь одно из следующих значений: const, type, var или func. Вы также можете использовать ключевое слово, за которым следует ряд объявлений в круглых скобках.

var (
    n int
    x float64
)

При объявлении функции вы должны либо указать имя для каждого параметра, либо не указывать имя для любого параметра; нельзя опускать одни имена и указывать другие. Вы можете сгруппировать несколько имен одного типа.

func f(i, j, k int, s, t string)

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

var v9 = *v2

Если переменная не инициализирована явно, необходимо указать тип. В этом случае он будет неявно инициализирован нулевым значением типа (0, nil, "" и т. д.). В Go нет неинициализированных переменных.

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

Внутри функции краткий синтаксис объявления доступен с :=. Утверждение

v10 := v1

то же самое что и

var v10 = v1

Типы функций

В Go функции - это объекты первого класса. Тип функции Go обозначает набор всех функций с одинаковыми параметрами и типами результатов.

type binOp func(int, int) int

var op binOp
add := func(i, j int) int { return i + j }

op = add
n = op(100, 200)  // n = 100 + 200

Множественное присвоение

Go допускает несколько назначений. Выражения справа оцениваются перед присвоением любому из операндов слева.

i, j = j, i  // Меняем местами i и j.

Функции могут иметь несколько возвращаемых значений, обозначенных списком в круглых скобках. Возвращенные значения могут быть сохранены путем присвоения списку переменных.

func f() (i int, pj *int) { ... }
v1, v2 = f()

Пустой идентификатор

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

v1, _ = f()  // Игнорировать второе значение, возвращаемое f().

Точка с запятой и форматирование

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

В коде Go на практике используется очень мало точек с запятой. Технически все операторы Go заканчиваются точкой с запятой. Однако Go неявно вставляет точку с запятой в конце непустой строки, если строка явно не является неполной. Следствием этого является то, что в некоторых случаях Go не допускает разрыв строки. Например, вы не можете писать

func g()
{            // INVALID: "{" должно быть в предыдущей строке.
}

Точка с запятой будет вставлена ​​после g(), что приведет к тому, что это будет объявление функции, а не определение функции. Точно так же вы не можете писать

if n == 0 {
}
else {       // INVALID: "else {" должно быть в предыдущей строке.
}

Точка с запятой будет вставлена ​​после символа } перед else, что вызовет синтаксическую ошибку.

Условные утверждения

Go не использует круглые скобки вокруг условия оператора if, выражений оператора for или значения оператора switch. С другой стороны, он требует фигурных скобок вокруг тела оператора if или for.

if a < b { f() }
if (a < b) { f() }          // Скобки не нужны.
if (a < b) f()              // INVALID
for i = 0; i < 10; i++ {}
for (i = 0; i < 10; i++) {} // INVALID

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

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

For утверждения

Go не имеет инструкции while и do-while. Оператор for может использоваться с одним условием, что делает его эквивалентным оператору while. Полное исключение условия приводит к бесконечному циклу.

Оператор for может содержать предложение диапазона для перебора строк, массивов, срезов, карт или каналов. Вместо того, чтобы писать

for i := 0; i < len(a); i++ { ... }

чтобы перебрать элементы a, мы также могли бы написать

for i, v := range a { ... }

Это присваивает i индексу, а v - значению последовательных элементов массива, среза или строки.

  • Для строк i - это индекс байта, а v - кодовая точка Unicode типа rune (rune - это псевдоним для int32).
  • Итерации по картам создают пары ключ-значение, тогда как каналы производят только одно значение итерации.

Break и continue

Подобно Java, Go разрешает break и continue для указания метки, но метка должна ссылаться на оператор for, switch или select.

Операторы switch

В операторе switch метки case не пропадают по умолчанию, но вы можете заставить их пропустить, завершив case оператором fallthrough.

switch n {
case 0: // пустое тело case
case 1:
    f() // f не вызывается, когда n == 0.
}

Но у case может быть несколько значений.

switch n {
case 0, 1:
    f() // f вызывается, если n == 0 || n == 1.
}

Значения в case могут быть любого типа, который поддерживает оператор сравнения равенства, например строки или указатели. Отсутствующее выражение switch эквивалентно выражению true.

switch {
case n < 0:
    f1()
case n == 0:
    f2()
default:
    f3()
}

Увеличение и уменьшение

++ и -- могут использоваться только как постфиксные операторы и только в операторах, но не в выражениях. Например, вы не можете написать n = i++.

Оператор defer

Оператор defer вызывает функцию, выполнение которой откладывается до момента возврата из окружающей функции.

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

f, err := os.Open("filename")
defer f.Close() // f будет закрыт, когда эта функция вернется.


Читайте также: