четверг, 28 октября 2021 г.

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

Объектно-ориентированное программирование

  • В Go нет классов с конструкторами. Вместо методов экземпляра, иерархии наследования классов и поиска динамических методов Go предоставляет структуры и интерфейсы.
  • Go позволяет использовать методы любого типа; упаковка не требуется. Получатель метода, который соответствует this в Java, может быть прямым значением или указателем.
  • Go предоставляет два уровня доступа, аналогичные public и package-private в Java. Объявления верхнего уровня являются общедоступными, если их имена начинаются с заглавной буквы, в противном случае они являются частными для пакета.

Функциональное программирование

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

Указатели и ссылки

  • Go предлагает указатели на значения всех типов, а не только на объекты и массивы. Для любого типа T существует соответствующий тип указателя *T, обозначающий указатели на значения типа T.
  • Go использует nil для недопустимых указателей, где Java использует null.
  • Массивы в Go - это значения. Когда массив используется в качестве параметра функции, функция получает копию массива, а не указатель на нее. Однако на практике функции часто используют срезы для параметров; срезы - это ссылки на базовые массивы.
  • Определенные типы (карты, срезы и каналы) передаются по ссылке, а не по значению. То есть передача карты функции не копирует карту; если функция изменяет карту, вызывающий абонент увидит это изменение. В терминах Java это можно рассматривать как ссылку на карту.

Встроенные типы

  • Строки предоставляются языком; строка ведет себя как срез байтов, но является неизменной.
  • Хэш-таблицы предоставляются языком. Их называют картами.

Обработка ошибок

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

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

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

Отсутствующие функции

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

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


среда, 27 октября 2021 г.

Go для Java разработчиков: Hello world пример

В постах Go для Java разработчиков будут представлены основные отличия Go для быстрого освоения Java разработчиками.

Начнем с небольшого, но полного примера. Он показывает, как реализовать и использовать простой абстрактный тип данных в Go. В данном случае это стек.

// Пакет collection реализует стек строк.
package collection

// Нулевое значение для Stack - это пустой стек, 
// готовый к использованию.
type Stack struct {
    data []string
}

// Push добавляет x на вершину стека.
func (s *Stack) Push(x string) {
    s.data = append(s.data, x)
}

// Pop удаляет и возвращает верхний элемент стека.
// Вызов Pop для пустого стека - ошибка времени выполнения.
func (s *Stack) Pop() string {
    n := len(s.data) - 1
    res := s.data[n]
    s.data[n] = "" // чтобы избежать утечки памяти
    s.data = s.data[:n]
    return res
}

// Size возвращает количество элементов в стеке.
func (s *Stack) Size() int {
    return len(s.data)
}

  • Комментарии, которые появляются непосредственно перед объявлениями верхнего уровня, являются комментариями документации. Они написаны обычным текстом.
  • Для объявлений вы пишете имя, а затем тип.
  • Структура (struct) соответствует классу в Java, но членами структуры не могут быть методы, только переменные.
  • Фрагмент кода (s *Stack) объявляет приемник метода s, соответствующий Java this.
  • Оператор := объявляет и инициализирует переменную. Его тип выводится из выражения инициализации.

Вот программа Hello world, которая показывает, как использовать абстрактный тип данных collection.Stack.

package collection_test

import (
    "fmt"
    "go-for-java-programmers/collection"
)

func ExampleStack() {
    var s collection.Stack
    s.Push("world!")
    s.Push("Hello, ")
    for s.Size() > 0 {
        fmt.Print(s.Pop())
    }
    fmt.Println()
    // Вывод: Hello, world!
}

  • Тестовый пакет collection_test находится в том же каталоге, что и пакет collection.
  • Первое объявление импорта содержит путь "fmt" к стандартному пакету; второй указывает, что мы будем использовать пакет из каталога "go-for-java-programmers/collection".
  • Доступ к пакетам в исходном файле осуществляется по коротким именам fmt и collection соответственно.

Примечание: идиоматический способ реализовать стек в Go - это использовать срез напрямую. Смотрите этот пост, о том как реализовать стек (LIFO).


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


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

Golang tips&tricks: таймауты и крайние сроки

Чтобы отказаться от синхронных вызовов, которые выполняются слишком долго, используйте оператор select с time.After:

import "time"

c := make(chan error, 1)
go func() { c <- client.Call("Service.Method", args, &reply) } ()
select {
  case err := <-c:
    // использование err и reply
  case <-time.After(timeoutNanoseconds):
    // истечение времени вызова
}

Обратите внимание, что канал c имеет размер буфера 1. Если бы это был небуферизованный канал и метод client.Call занимал больше времени timeoutNanoseconds, передача канала заблокировалась бы навсегда, и горутина никогда не была бы уничтожена.


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


четверг, 7 октября 2021 г.

Пустой интерфейс в Golang

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

Пустой интерфейс

Вот хорошее определение пустого интерфейса:

Интерфейс - это два компонента: это набор методов и тип.

Тип interface{} - это интерфейс, не имеющий методов. Поскольку в Go нет ключевого слова implements (реализует), все типы реализуют по крайней мере ноль методов, и удовлетворение интерфейса выполняется автоматически, все типы удовлетворяют пустой интерфейс. Следовательно, метод с пустым интерфейсом в качестве аргумента может принимать любой тип. Go перейдет к преобразованию в тип интерфейса, который будет выполнять эту функцию.

Расс Кокс, один из ведущих разработчиков Go, написал статью о внутреннем представлении интерфейсов и объяснил, что интерфейс состоит из двух слов:

  • указатель на информацию о сохраненном типе
  • указатель на связанные данные

Эта было в 2009 году, когда среда выполнения была написана на C.

Среда выполнения теперь написана на Go, но представление осталось прежним. Мы можем убедиться в этом, распечатав пустой интерфейс:

package main

func main() {
    var myVar int32 = 1
    printInterface(myVar)
}

//go:noinline
func printInterface(val interface{}) {
    println(val)
}

Вывод:

(0x459dc0,0xc00003476c)

Оба адреса представляют собой два указателя на тип информации и значение.

Базовая структура

Базовое представление пустого интерфейса задокументировано в пакете reflect:

type emptyInterface struct {
   typ  *rtype            // слово 1 с описанием типа
   word unsafe.Pointer    // слово 2 со значением
}

Как объяснялось ранее, мы ясно видим, что пустой интерфейс имеет слово описания типа, за которым следует слово, содержащее данные.

Структура rtype содержит основу описания типа:

type rtype struct {
	size       uintptr
	ptrdata    uintptr // количество байтов в типе, которое может содержать указатели
	hash       uint32  // хэш типа; избегает вычислений в хэш-таблицах
	tflag      tflag   // флаги дополнительной информации типа
	align      uint8   // выравнивание переменной с этим типом
	fieldAlign uint8   // выравнивание поля структуры с этим типом
	kind       uint8   // перечисление для C
	// функция для сравнения объектов этого типа
	// (ptr to object A, ptr to object B) -> ==?
	equal     func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata    *byte   // данные сборки мусора
	str       nameOff // строковая форма
	ptrToThis typeOff //  тип для указателя на этот тип, может быть нулевым
}

Среди этих полей некоторые довольно простые и хорошо известные:

  • size - это размер в байтах
  • kind содержит тип: int8, int16, bool и т. д.
  • align - это выравнивание переменной с этим типом

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

// structType представляет struct тип.
type structType struct {
	rtype
	pkgPath name
	fields  []structField
}

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

Давайте теперь посмотрим, какие виды конверсии действительно возможны из пустого интерфейса.

Конверсии

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

package main

func main() {
    var myVar int32 = 1
    printInterface(myVar)
}

//go:noinline
func printInterface(val interface{}) {
    n := val.(int64)
    println(n)
}

Хотя преобразование из int32 в int64 допустимо, программа запаникует:

panic: interface conversion: interface {} is int32, not int64

goroutine 1 [running]:
main.printInterface(0xe49f20, 0xc00002ff74)
	/home/myuser/main.go:10 +0x85
main.main()
	/home/myuser/main.go:5 +0x46

Давайте сгенерируем asm-код, чтобы увидеть, какая проверка выполняется Go:

...
0x0024 00036 (/home/myuser/main.go:10)  LEAQ    type.int64(SB), AX // Шаг 1
0x002b 00043 (/home/myuser/main.go:10)  MOVQ    "".val+48(SP), CX
0x0030 00048 (/home/myuser/main.go:10)  CMPQ    AX, CX
0x0033 00051 (/home/myuser/main.go:10)  JNE     105                // Шаг 2
0x0035 00053 (/home/myuser/main.go:10)  MOVQ    "".val+56(SP), AX
0x003a 00058 (/home/myuser/main.go:10)  MOVQ    (AX), AX
0x003d 00061 (/home/myuser/main.go:10)  MOVQ    AX, "".n+24(SP)
0x0042 00066 (/home/myuser/main.go:11)  PCDATA  $1, $1
0x0042 00066 (/home/myuser/main.go:11)  CALL    runtime.printlock(SB)
0x0047 00071 (/home/myuser/main.go:11)  MOVQ    "".n+24(SP), AX
0x004c 00076 (/home/myuser/main.go:11)  MOVQ    AX, (SP)
0x0050 00080 (/home/myuser/main.go:11)  CALL    runtime.printint(SB)
0x0055 00085 (/home/myuser/main.go:11)  CALL    runtime.printnl(SB)
0x005a 00090 (/home/myuser/main.go:11)  CALL    runtime.printunlock(SB)
0x005f 00095 (/home/myuser/main.go:12)  MOVQ    32(SP), BP
0x0064 00100 (/home/myuser/main.go:12)  ADDQ    $40, SP
0x0068 00104 (/home/myuser/main.go:12)  RET
0x0069 00105 (/home/myuser/main.go:10)  MOVQ    CX, (SP)                
0x006d 00109 (/home/myuser/main.go:10)  MOVQ    AX, 8(SP)
0x0072 00114 (/home/myuser/main.go:10)  LEAQ    type.interface {}(SB), AX
0x0079 00121 (/home/myuser/main.go:10)  MOVQ    AX, 16(SP)
0x007e 00126 (/home/myuser/main.go:10)  NOP
0x0080 00128 (/home/myuser/main.go:10)  CALL    runtime.panicdottypeE(SB) // Шаг 3
0x0085 00133 (/home/myuser/main.go:10)  XCHGL   AX, AX                    // Шаг 4
0x0086 00134 (/home/myuser/main.go:10)  NOP
0x0086 00134 (/home/myuser/main.go:9)   PCDATA  $1, $-1
0x0086 00134 (/home/myuser/main.go:9)   PCDATA  $0, $-2
0x0086 00134 (/home/myuser/main.go:9)   CALL    runtime.morestack_noctxt(SB) 
0x008b 00139 (/home/myuser/main.go:9)   PCDATA  $0, $-1
0x008b 00139 (/home/myuser/main.go:9)   JMP     0
...

Вот несколько шагов:

  • Шаг 1: сравнить (инструкция CMPQ) тип int64 (загруженный инструкцией LEAQ, Load Effective Address) с внутренним типом пустого интерфейса (инструкция MOVQ, которая считывает память со смещением 48 байтов из сегмента памяти пустого интерфейса)
  • Шаг 2: инструкция JNE "Перейти, если не равно" перейдет к сгенерированным инструкциям, которые будут обрабатывать ошибку на шаге 3.
  • Шаг 3: код запаникует и выдаст сообщение об ошибке, которое мы видели ранее
  • Шаг 4: это конец инструкций по ошибке. На эту конкретную инструкцию ссылается сообщение об ошибке, которое показывает инструкцию: main.go: 10 + 0x85

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

Производительность

Вот два теста (Golang 1.16.2). Один с копией структуры, а другой с использованием пустого интерфейса:

package maintest

import (
    "testing"
)

var instance StructWithFields

type StructWithFields struct {
    field1 int
    field2 string
    field3 float32
    field4 float64
    field5 int32
    field6 bool
    field7 uint64
    field8 *string
    field9 uint16
}

//go:noinline
func emptyInterface(i interface {}) {
    str := i.(StructWithFields)
    instance = str
}

//go:noinline
func withType(str StructWithFields) {
    instance = str
}

func BenchmarkWithType(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        withType(str)
    }
}

func BenchmarkWithEmptyInterface(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        emptyInterface(str)
    }
}

Вот результаты:

BenchmarkWithType-4             	74843296  16.75 ns/op
BenchmarkWithEmptyInterface-4   	48790801  33.27 ns/op

Для двойного преобразования типа в пустой интерфейс, а затем обратно в тип требуется на 16 наносекунд больше, чем копирование структуры.

Хорошим решением было бы использовать указатель и преобразовать обратно в тот же указатель на структуру.

package maintest

import (
    "testing"
)

var instance *StructWithFields

type StructWithFields struct {
    field1 int
    field2 string
    field3 float32
    field4 float64
    field5 int32
    field6 bool
    field7 uint64
    field8 *string
    field9 uint16
}

//go:noinline
func emptyInterface(i interface {}) {
    str := i.(*StructWithFields)
    instance = str
}

//go:noinline
func withType(str *StructWithFields) {
    instance = str
}

func BenchmarkWithType(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        withType(&str)
    }
}

func BenchmarkWithEmptyInterface(b *testing.B) {
    str := StructWithFields{field1: 1, field2: "string", field8: new(string)}
    for i := 0; i < b.N; i++ {
        emptyInterface(&str)
    }
}

Результаты теперь совсем другие:

BenchmarkWithType-4             	187950846  6.374 ns/op
BenchmarkWithEmptyInterface-4   	150776062  8.141 ns/op

Что касается базового типа, такого как int или string, производительность немного отличается:

package maintest

import (
    "testing"
)

var intVar int
var stringVar string

//go:noinline
func emptyInterfaceInt(i interface {}) {
    str := i.(int)
    intVar = str
}

//go:noinline
func withTypeInt(str int) {
    intVar = str
}

//go:noinline
func emptyInterfaceString(i interface {}) {
    str := i.(string)
    stringVar = str
}

//go:noinline
func withTypeString(str string) {
    stringVar = str
}

func BenchmarkWithTypeInt(b *testing.B) {
    str := 123
    for i := 0; i < b.N; i++ {
        withTypeInt(str)
    }
}

func BenchmarkWithEmptyInterfaceInt(b *testing.B) {
    str := 123
    for i := 0; i < b.N; i++ {
        emptyInterfaceInt(str)
    }
}

func BenchmarkWithTypeString(b *testing.B) {
    str := "123"
    for i := 0; i < b.N; i++ {
        withTypeString(str)
    }
}

func BenchmarkWithEmptyInterfaceString(b *testing.B) {
    str := "123"
    for i := 0; i < b.N; i++ {
        emptyInterfaceString(str)
    }
}

BenchmarkWithTypeInt-4                	228122656  5.319 ns/op
BenchmarkWithEmptyInterfaceInt-4      	144611329  8.028 ns/op
BenchmarkWithTypeString-4             	158939217  7.508 ns/op
BenchmarkWithEmptyInterfaceString-4   	100000000  10.10 ns/op

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


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


среда, 6 октября 2021 г.

Ограничение использования ресурсов для программы в Golang

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

const (
    AvailableMemory         = 10 << 20 // 10 MB
    AverageMemoryPerRequest = 10 << 10 // 10 KB
    MaxOutstanding          = AvailableMemory / AverageMemoryPerRequest
)

var sem = make(chan int, MaxOutstanding)

func Serve(queue chan *Request) {
    for {
        sem <- 1 // Блокировать до тех пор, 
                 // пока не появится возможность обработать запрос.
        req := <-queue
        go handle(req) // Не дожидаемся завершения handle.
    }
}

func handle(r *Request) {
    process(r) // Может занять много времени 
               // и потреблять много памяти или ЦП
    <-sem      // Готово; разрешить запуск следующего запроса.
}


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