From vmkteam-developer
Generates utility methods like IDs(), Index(), and custom collectors for Go struct slices using //go:generate colgen directives. Use with collection.go files and layer converters.
npx claudepluginhub vmkteam/claude-plugins --plugin vmkteam-developerThis skill uses the workspace's default tool permissions.
`colgen` (https://github.com/vmkteam/colgen) - утилита для генерации различного кода и не только:
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
colgen (https://github.com/vmkteam/colgen) - утилита для генерации различного кода и не только:
Документация: https://vmkteam.dev/colgen/
Рассмотрим более подробно на примере проекта newsportal. Сколько раз в проектах мы встречали код вот такого типа?
ids := make([]int, len(list))
for i := range list {
ids = append(ids, i.ID)
}
То есть у нас есть какой-то list, например []News, и нам нужно собрать все ID в слайс для дальнейшего использования.
Такие простые куски кода разрастаются по проекту, но не несут никакой смысловой нагрузки. Или рассмотрим другой пример – преобразование слайса в мап по ID:
r := make(map[int]News, len(list))
for i, n := range list {
r[n.ID] = n
}
И такого простого кода обычно много. Его можно упростить. Например, мы можем ввести понятие коллекций.
Создадим файл collection.go, в нем объявим новый тип и пару методов у него.
type NewsList []News
func (ll NewsList) IDs() []int {
r := make([]int, len(ll))
for i := range ll {
r[i] = ll[i].ID
}
return r
}
func (ll NewsList) Index() map[int]News {
r := make(map[int]News, len(ll))
for i := range ll {
r[ll[i].ID] = ll[i]
}
return r
}
Теперь мы можем упросить основной код и использовать новые методы:
nn := NewsList(list)
ids := nn.IDs()
idx := nn.Index()
Плюсы:
Минусы:
Давайте попробуем убрать минусы и добавим в наш файл:
//go:generate colgen
//colgen:News
Данная конструкция создаст новый файл с суффиксом _colgen.go, в котором будет базовый тип для структуры и два метода: IDs() и Index() (если есть поле ID).
Если поле ID отсутствует (например, вместо него NewsID), то можно добавить следующую конструкцию для аналогичного поведения:
//go:generate colgen
//colgen:News
//colgen:News:NewsID,Index(NewsID)
В результате мы получим базовый тип и два метода: NewsIDs() и IndexByNewsID().
Допустим, у новости есть теги TagIDs, и нам из списка надо получить уникальные теги. А у тегов есть поле alias, и нам нужно сделать индекс по нему.
type News struct {
ID int
Text string
TagIDs []int
}
type Tag struct {
ID int
Alias string
}
//go:generate colgen
//colgen:News,Tag
//colgen:News:UniqueTagIDs
//colgen:Tag:Index(Alias)
Единственное, на что надо обращать внимание: код перед вызовом генератора должен компилироваться, потому что идет парсинг go файлов через AST.
Можно сгруппировать слайс по определенному полю. //colgen:News:Group(CategoryID):
func (ll NewsList) GroupByCategoryID() map[int]NewsList {
r := make(map[int]NewsList, len(ll))
for i := range ll {
r[ll[i].CategoryID] = append(r[ll[i].CategoryID], ll[i])
}
return r
}
Допустим, наша структура News стала сложнее — появились связи с авторами и статусами:
type News struct {
ID int
Text string
StatusID int
TagIDs []int
Tags []Tag
AuthorID *int
Author *Author
}
<Field> для указателей и слайсовДля скалярного поля StatusID всё как раньше — //colgen:News:StatusID:
func (ll NewsList) StatusIDs() []int {
r := make([]int, len(ll))
for i := range ll {
r[i] = ll[i].StatusID
}
return r
}
А вот для слайса TagIDs []int вместо [][]int мы получим плоский []int — flatten. //colgen:News:TagIDs:
func (ll NewsList) TagIDs() []int {
var r []int
for i := range ll {
r = append(r, ll[i].TagIDs...)
}
return r
}
Для указателя Author *Author генератор добавит nil-check и разыменование. //colgen:News:Author:
func (ll NewsList) Authors() []Author {
var r []Author
for i := range ll {
if ll[i].Author != nil {
r = append(r, *ll[i].Author)
}
}
return r
}
Аналогично Unique<Field> теперь поддерживает *T поля: //colgen:News:UniqueAuthorID сгенерирует UniqueAuthorIDs() []int с nil-check и дедупликацией.
//colgen:News:Count(StatusID):
func (ll NewsList) CountByStatusID(v int) int {
var c int
for i := range ll {
if ll[i].StatusID == v {
c++
}
}
return c
}
Заполнение связей. Режим определяется автоматически по типу junction.
Для []int junction (many-to-many). //colgen:News:Fill(Tags,TagIDs):
func (ll NewsList) FillTags(related Tags) NewsList {
index := related.Index()
for i := range ll {
for _, id := range ll[i].TagIDs {
if v, ok := index[id]; ok {
ll[i].Tags = append(ll[i].Tags, v)
}
}
}
return ll
}
Для *int FK (one-to-one). //colgen:News:Fill(Author,AuthorID):
func (ll NewsList) FillAuthor(related Authors) NewsList {
index := related.Index()
for i := range ll {
if ll[i].AuthorID != nil {
if v, ok := index[*ll[i].AuthorID]; ok {
ll[i].Author = &v
}
}
}
return ll
}
Использование:
news = news.FillTags(tags).FillAuthor(authors)
Методы возвращают тот же список, поэтому их удобно чейнить.
Если структура реализует интерфейс через pointer receiver. //colgen:News:Cast(Searchable):
func (ll NewsList) Searchables() []Searchable {
r := make([]Searchable, len(ll))
for i := range ll {
r[i] = &ll[i]
}
return r
}
Один элемент попадёт в несколько групп. //colgen:News:Group(TagIDs):
func (ll NewsList) GroupByTagIDs() map[int]NewsList {
r := make(map[int]NewsList)
for i := range ll {
for _, v := range ll[i].TagIDs {
r[v] = append(r[v], ll[i])
}
}
return r
}
//go:generate colgen -imports=newsportal/pkg/db
//colgen:News,Tag
//colgen:News:UniqueTagIDs,Map(db)
//colgen:Tag:Index(Alias)
Добавление Map(db) сгенерирует:
func NewNewsList(in []db.News) NewsList { return Map(in, NewNews) }
NewNews принимает *db.News, то необходимо использовать MapP(db).mapp/map вместо MapP/Mapdb слое отличается, то используем полный путь: Map(db.News)//colgen:News,Tags,..., то конструктор будет возвращать []News вместо NewsList-imports=newsportal/pkg/dbMap/MapP лежат в другом пакете, то нужно добавить этот пакет в импорт через запятую и добавить название пакета через -funcpkg=<pkg>.// MapP converts slice of type T to slice of type M with given converter with pointers.
func MapP[T, M any](a []T, f func(*T) *M) []M {
n := make([]M, len(a))
for i := range a {
n[i] = *f(&a[i])
}
return n
}
// Map converts slice of type T to slice of type M with given converter.
func Map[T, M any](a []T, f func(T) M) []M {
n := make([]M, len(a))
for i := range a {
n[i] = f(a[i])
}
return n
}
Рекомендуется иметь один colgen на пакет. Директивы //go:generate colgen можно размещать в файле collection.go.
Если все сломалось, то удаляйте файл
_colgen.goи вызывайте генератор снова. Но! Если функции генератора уже используются в коде, то при удалении файла будет нерабочий код и генератор не запустится.
Шаги для максимальной генерации:
Map/MapP дженерики.Map/MapP генераторы. Если их добавить в пункте 1, то будет некомпилируемый код из-за отсутствия функций конструктора.//go:generate colgen
...
//colgen@NewNews(db)
При генерации директива //colgen@NewNews(db) будет заменена в этом файле на следующий код:
type News struct {
db.News
}
func NewNews(in *db.News) *News {
if in == nil {
return nil
}
return &News{
News: *in,
}
}
Второй пример //colgen@newNewsSummary(db.News,full,json):
type NewsSummary struct {
ID int `json:"newsSummaryId"`
Text string `json:"text"`
TagIDs []int `json:"tagIDs"`
}
func newNewsSummary(in *db.News) *NewsSummary {
if in == nil {
return nil
}
return &NewsSummary{
ID: in.ID,
Text: in.Text,
TagIDs: in.TagIDs,
}
}
Когда мы говорим о доменном слое, то нам подходит первый вариант (embed). Когда мы говорим о слое с апи, нам подходит второй вариант (full, json).
На сложных объектах вы будете получать невалидный сниппет. Но базовая идея заключается в том, чтобы получить сниппет, а потом его отредактировать как нужно.
Для работы с DeepSeek или Claude у вас должен быть API ключ. Работа с LLM идет в рамках файла, именно его содержимое (+тест, если применимо) + промт отправляется на сервер.
Три режима:
//colgen@ai:review – код ревью с идиоматичным промтом. Результат в файле с суффиксом .md.//colgen@ai:readme – генерация README.md по текущему файлу.//colgen@ai:tests – генерация тестов с нуля или дописывание _test.go.Для выбора LLM в конце добавить (deepseek) или (claude). DeepSeek по умолчанию.
//go:generate colgen
//colgen@ai:readme // makes readme using deepseek by default
//colgen@ai:tests(deepseek) // makes tests using deepseek explicitly
//colgen@ai:review(claude) // makes review using claude
*_colgen.go с заголовком // Code generated by colgen; DO NOT EDIT.collection.go (аннотации), файлы с конвертерами