Перший погляд на Go generics
Привіт! Дженерики — це найсуперечливіша тема в українській спільноті гоферів GolangUA, дуже просто хайпонути на темі дженериків в Go. Я вирішив написати про дженерики з трьох причин.
Перша — я прочитав статтю Quick Guide: Go 1.18 Features від турка Emre Odabas (оригінал турецькою мовою) й захотів спробувати дженерики для зменшення дублікатів коду.
Друга причина — це бажання поділитись навчальними матеріалами з Go за цей рік, частину взяв з @GolangDigest.
Третя причина — це tada.training.
Стандартна задача з дублюванням коду
Для мене такою задачею є перетворення слайсу в мапу.
package main
import "fmt"
type Company struct {
ID int
Name string
Description string
}
type Vacancy struct {
ID int
CompanyID int
Title string
Description string
}
func CompaniesToMap(items []Company) map[int]Company {
var result = make(map[int]Company, len(items))
for _, item := range items {
result[item.ID] = item
}
return result
}
func VacanciesToMap(items []Vacancy) map[int]Vacancy {
var result = make(map[int]Vacancy, len(items))
for _, item := range items {
result[item.ID] = item
}
return result
}
func main() {
var companies = []Company{
{
ID: 1,
Name: "TADA!",
Description: "Job interview preparation service",
},
}
var companiesMap = CompaniesToMap(companies)
fmt.Printf("Map %v, type %T\n", companiesMap, companiesMap)
}go run ./examples/001_duplication/main.go
Map map[1:{1 TADA! Job interview preparation service}], type map[int]main.Companygo version
go version go1.18beta1 linux/amd64
package main
import "fmt"
// type Company ...
// type Vacancy ...
type Getter interface {
Company | Vacancy
}
func main() {
fmt.Println("Success!")
}go run ./examples/002_try_define_union/main.go
Success!
Далі вирішив поекспериментувати з типами, а в коментарі вставив помилки від компілятора:
package main
import "fmt"
// type Company ...
// type Vacancy ...
type Getter interface {
Company | Vacancy | *Vacancy
}
func PrintGetter[T Getter](v T) {
fmt.Printf("Value %v, type %T\n", v, v)
// ERROR: invalid operation: cannot use type assertion on type parameter value v (variable of type T constrained by Getter)
// if company, ok := v.(Company); ok {
// _ = company
// }
}
func main() {
PrintGetter(Company{
ID: 1,
Name: "TADA!",
Description: "Job interview preparation service",
})
// ERROR: *Company does not implement Getter
// PrintGetter(&Company{})
PrintGetter(Vacancy{
ID: 1,
CompanyID: 1,
Title: "Go developer",
Description: "",
})
PrintGetter(&Vacancy{})
}go run ./examples/003_try_print_union/main.go
Value {1 TADA! Job interview preparation service}, type main.Company
Value {1 1 Go developer }, type main.Vacancy
Value &{0 0 }, type *main.VacancyНастав час вже написати параметризовану функцію через просту заміну типу:
package main
import "fmt"
// type Company ...
// type Vacancy ...
type Getter interface {
Company | Vacancy
}
func ToMap[T Getter](items []T) map[int]T {
var result = make(map[int]T, len(items))
for _, item := range items {
result[item.ID] = item
}
return result
}
func main() {
var companies = []Company{
{
ID: 1,
Name: "TADA!",
Description: "Job interview preparation service",
},
}
var companiesMap = ToMap(companies)
fmt.Printf("Map %v, type %T\n", companiesMap, companiesMap)
}go run ./examples/004_try_map_by_id/main.go
examples/004_try_map_by_id/main.go:26:15: item.ID undefined (interface Getter has no method ID)
На просту заміну типу, яка б спрацювала в TypeScript, компілятор Go видає помилку.
Тоді залишається варіант через додавання методу:
package main
import "fmt"
// type Company ...
// type Vacancy ...
func (c Company) GetID() int {
return c.ID
}
func (v Vacancy) GetID() int {
return v.ID
}
type Getter interface {
GetID() int
}
func ToMap[T Getter](items []T) map[int]T {
var result = make(map[int]T, len(items))
for _, item := range items {
result[item.GetID()] = item
}
return result
}
func main() {
var companies = []Company{
{
ID: 1,
Name: "TADA!",
Description: "Job interview preparation service",
},
}
var companiesMap = ToMap(companies)
fmt.Printf("Map %v, type %T\n", companiesMap, companiesMap)
}go run ./examples/005_map_by_interace_get_id/main.go
Map map[1:{1 TADA! Job interview preparation service}], type map[int]main.CompanyВдалось використати дженерики для зменшення дублювання.
Складніша задача з обгорткою
В попередньому прикладі я мав повний доступ до коду й легко додав потрібні методи, але хочу спробувати варіант з обертанням згенерованого коду.
Отже в мене є дві однакові структури, які згенеровані sqlc:
tree ./examples/006_wrap
./examples/006_wrap ├── generated │ └── models.go └── main.go 1 directory, 2 files
cat ./examples/006_wrap/generated/models.go
package generated
type SkillsRow struct {
ID int32
Alias string
Name string
}
type DirectionsRow struct {
ID int32
Alias string
Name string
}Ці дві структури я хочу привести до однієї:
type Alias struct {
ID int32
Alias string
Name string
}Як в попередньому прикладі, спробую зробити це через дженерики.
package main
import "gitlab.com/go-yp/go-generics-map/examples/006_wrap/generated"
type (
Skill generated.SkillsRow
Direction generated.SkillsRow
)
type Alias struct {
ID int32
Alias string
Name string
}
func (s Skill) GetID() int32 {
return s.ID
}
func (s Skill) ToAlias() Alias {
return Alias{
ID: s.ID,
Alias: s.Alias,
Name: s.Name,
}
}
func (d Direction) GetID() int32 {
return d.ID
}
func (d Direction) ToAlias() Alias {
return Alias{
ID: d.ID,
Alias: d.Alias,
Name: d.Name,
}
}
type AliasGetter interface {
GetID() int32
ToAlias() Alias
}
func ToAliasMap[F any, T AliasGetter](values []F) map[int32]Alias {
var result = make(map[int32]Alias, len(values))
for _, value := range values {
var wrap = T(value)
result[wrap.GetID()] = wrap.ToAlias()
}
return result
}
func main() {
ToAliasMap[generated.SkillsRow, Skill]([]generated.SkillsRow{})
ToAliasMap[generated.DirectionsRow, Direction]([]generated.DirectionsRow{})
}Але після запуску компілятор повернув помилку:
go run ./examples/006_wrap/main.go
# command-line-arguments examples/006_wrap/main.go:49:16: cannot convert value (variable of type F constrained by any) to type T
В цьому разі код став значно складнішим, ніж написати дві функції для кожного типу, хоча рішення є:
package main
import (
"fmt"
g "gitlab.com/go-yp/go-generics-map/examples/006_wrap/generated"
)
type Alias struct {
ID int32
Alias string
Name string
}
func ToAliasMap[F any](values []F, transform func(F) Alias) map[int32]Alias {
var result = make(map[int32]Alias, len(values))
for _, value := range values {
var alias = transform(value)
result[alias.ID] = alias
}
return result
}
func main() {
var sourceSkills = []g.SkillsRow{}
var skills = ToAliasMap[g.SkillsRow](sourceSkills, func(s g.SkillsRow) Alias {
return Alias{
ID: s.ID,
Alias: s.Alias,
Name: s.Name,
}
})
fmt.Printf("Map %v, type %T\n", skills, skills)
}go run ./examples/006_wrap/main.go
Map map[], type map[int32]main.Alias
Вкладенні типи
В процесі експериментів згадав, що в Go є складнощі з виведенням вкладених типів:
package main
import "fmt"
type NameGetter interface {
Name() string
}
type User struct {
name string
}
func (u User) Name() string {
return u.name
}
func PrintName(lazy func() NameGetter) {
fmt.Println(lazy().Name())
}
func main() {
// will work
// PrintName(func() NameGetter {
// return &User{
// "TADA!",
// }
// })
// will error
PrintName(func() *User {
return &User{
"TADA!",
}
})
}go run ./examples/007_nested_before/main.go
cannot use func() *User {…} (value of type func() *User) as type func() NameGetter in argument to PrintNameЧи виправить помилку заміна на дженерик?
package main
import "fmt"
type NameGetter interface {
Name() string
}
type User struct {
name string
}
func (u User) Name() string {
return u.name
}
func PrintName[T NameGetter](lazy func() T) {
fmt.Println(lazy().Name())
}
func main() {
// will work
PrintName(func() NameGetter {
return &User{
"TADA!",
}
})
// will work too
PrintName(func() *User {
return &User{
"TADA!",
}
})
}go run ./examples/008_nested_after/main.go
TADA! TADA!
Дженерик виявився корисним в цій ситуації.
Go Türkiye
Цього року я часто зустрічав турецькі проєкти на GitHub, турецьку статтю навів на початку цієї статті, мав відрядження до Туреччини, а також отримав пропозицію в LinkedIn на турецький проєкт.
Як виявилось, в Туреччині велика спільнота гоферів kommunity.com/goturkiye (4500+ часників), є YouTube-канал Go Türkiye де є записи з GopherCon Turkey 2021 — Turkish Track та GopherCon Turkey 2021 — English Track, #GopherConTR. Й розвивають свій GitHub Go Türkiye.
А ще 12 грудня 2021 року брати Капранови опублікували відео Історія Туреччини.
Це до того, що замість суперечок в коментарях на DOU краще вкладайте час в розвиток нашої Go-спільноти GolangUA, пишіть статті, коментуйте статті на тематику Go або допомагайте колегам перекваліфіковуватись.
TADA! — сервіс для проведення тестових інтерв’ю
Ювженко Денис анонсував сервіс TADA! 30 листопада 2020 року, а зараз шукає інтерв’юера з досвідом в Go. Go активно розвивається в Україні, а ми як спільнота маємо цьому сприяти.
Звісно, інтерв’юер буде отримувати гроші за проведені інтерв’ю.
Навчальні матеріали
Цього року було багато онлайл конференцій з Go, якщо якусь пропустив, то додайте в коментарях.
- [Gopher Academy] GopherCon 2021 — Day 1
- [Gopher Academy] GopherCon 2021 — Day 2
- [Gopher Academy] GopherCon 2021 — Day 3
- GopherCon UK 2021
- GopherCon Europe 2021
Статті:
Ще відео:
YouTube-канали:
Roadmaps:
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
45 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів