Перший погляд на Go generics
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.
Привіт! Дженерики — це найсуперечливіша тема в українській спільноті гоферів 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.Company
go 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 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів