DevGang
Авторизоваться

Дженерики в Go - как они работают и как с ними играть 

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

Примеры

LIFO Стек

Допустим, вы хотите создать стек «первым пришел - первым вышел». Без дженериков вы, вероятно, реализовали бы это так:

type Stack []interface{}

func (s Stack) Peek() interface{} {
	return s[len(s)-1]
}

func (s *Stack) Pop() {
	*s = (*s)[:len(*s)-1]
}

func (s *Stack) Push(value interface{}) {
	*s = append(*s, value)
}

Однако здесь есть проблема: всякий раз, когда вы обращаетесь к элементу Peek, вы должны использовать утверждение типа interface{}, чтобы преобразовать его в нечто полезное. Если ваш стек является стеком *MyObject, это значит много s.Peek().(*MyObject). Это не только боль в глазах, но и предлагает место для ошибки. Что если вы забудете звездочку? Или что, если вы выберете неправильный тип? s.Push(MyObject{})с удовольствием скомпилирует, и вы не сможете понять свою ошибку, пока она не повлияет на ваш сервис.

В целом использование interface{} относительно опасно. Всегда безопаснее использовать более ограниченные типы, чтобы проблемы могли быть обнаружены во время компиляции, а не во время выполнения.

Обобщения решают эту проблему, позволяя типам иметь параметры типа:

type Stack(type T) []T

func (s Stack(T)) Peek() T {
	return s[len(s)-1]
}

func (s *Stack(T)) Pop() {
	*s = (*s)[:len(*s)-1]
}

func (s *Stack(T)) Push(value T) {
	*s = append(*s, value)
}

Это добавляет параметр типа к Stack, устраняя необходимость в interface{}. Теперь, когда вы вызываете Peek(), возвращаемое значение уже является исходным типом, и нет никакой возможности ввести неправильный тип значения. Эта реализация намного безопаснее и проще в использовании.

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

type MyObject struct {
    X int
}

var sink MyObject

func BenchmarkGo1(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var s Stack
		s.Push(MyObject{})
		s.Push(MyObject{})
		s.Pop()
		sink = s.Peek().(MyObject)
	}
}

func BenchmarkGo2(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var s Stack(MyObject)
		s.Push(MyObject{})
		s.Push(MyObject{})
		s.Pop()
		sink = s.Peek()
	}
}

BenchmarkGo1
BenchmarkGo1-16    	12837528	        87.0 ns/op	      48 B/op	       2 allocs/op
BenchmarkGo2
BenchmarkGo2-16    	28406479	        41.9 ns/op	      24 B/op	       2 allocs/op

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

Контракты

Приведенный выше пример стека работает для любого типа. Однако во многих случаях вам нужно написать код, который работает только для типов с определенными характеристиками. Например, вы можете захотеть, чтобы ваш стек требовал, чтобы типы реализовывали функцию String(). Вот где вступают «контракты»:

contract stringer(T) {
	T String() string
}

type Stack(type T stringer) []T

// Now we can use the String method of T:
func (s Stack(T)) String() string {
	ret := ""
	for _, v := range s {
		if ret != "" {
			ret += ", "
		}
		ret += v.String()
	}
	return ret
}

Больше примеров

Приведенные выше примеры охватывают только основы. Вы также можете добавить параметры типа в функции и добавить определенные типы в контракты.

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

Эскизный проект

Эскизный проект содержит более подробное описание, а также еще несколько примеров:

https://go.googlesource.com/proposal/+/4a54a00950b56dd0096482d0edae46969d7432a6/design/go2draft-contracts.md

Прототип CL

Прототип CL также имеет несколько примеров. Найдите файлы, заканчивающиеся на «.go2»:

https://go-review.googlesource.com/c/go/+/187317

Как вы можете попробовать дженерики сегодня

Использование playground площадки WebAssembly

Безусловно, самый быстрый и простой способ опробовать дженерики - через playground площадку WebAssembly. За кулисами он использует WASM-сборку прототипа транслятора исходного кода для запуска кода Go прямо в браузере. Это действительно имеет некоторые ограничения (см. Github.com/ccbrown/wasm-go-playground).

Компиляция CL

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

CL расширяет существующие пакеты go/* для включения поддержки обобщений и добавляет новый пакет go/go2go, который можно использовать для переписывания общего кода Go 2 в код Go 1.

Он также добавляет команду «go2go», которую можно использовать для перевода кода из вашего CLI.

Вы можете скомпилировать CL, следуя инструкциям Installing Go from Source. Когда вы доберетесь до необязательного шага «Переключиться на основную ветку», проверьте CL вместо этого:

git fetch "https://go.googlesource.com/go" refs/changes/17/187317/14 && git checkout FETCH_HEAD

Обратите внимание, что это извлечет набор 14 патчей, который является самым последним на момент написания. Перейдите в CL и найдите кнопку «Скачать», чтобы получить команду проверки для последнего набора патчей.

После компиляции CL вы можете использовать пакеты go/* для написания пользовательских инструментов для работы с универсальными шаблонами или просто использовать инструмент командной строки go2go:

go tool go2go translate mygenericcode.go2

Источник:

#Golang
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу