Использование собственных типов в Golang | Амплеев Евгений - Agile Coach / Full stack web developer. Персональный блог.
Читаете:
Использование собственных типов в Golang
Поделиться:

Использование собственных типов в Golang

Avatar
Автор статьи: Yevgeniy Ampleev
24 марта 2025 в 19:34
Использование собственных типов в                                 Golang

Аннотация

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

Целевая аудитория

Эта статья предназначена для разработчиков, которые только начинают осваивать Go и не имеют практически никакого опыта работы с ним.


Проблематика

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

Для начала мы определим структуру Book, которую планируем классифицировать по жанру Genre:

                          package books  type Book struct {     ID    int    `json:"id"`     Name  string `json:"name"`     Genre string `json:"genre"` }                                              

Теперь, когда мы определились с книгой, давайте определим константы для жанра:

 const ( 	Adventure     = "Adventure" 	Comic         = "Comic" 	Crime         = "Crime" 	Fiction       = "Fiction" 	Fantasy       = "Fantasy" 	Historical    = "Historical" 	Horror        = "Horror" 	Magic         = "Magic" 	Mystery       = "Mystery" 	Philosophical = "Philosophical" 	Political     = "Political" 	Romance       = "Romance" 	Science       = "Science" 	Superhero     = "Superhero" 	Thriller      = "Thriller" 	Western       = "Western" )                         

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

В Go одним из способов сделать это является создание констант, основанных на типе int:

 const ( 	Adventure     = 1 	Comic         = 2 	Crime         = 3 	Fiction       = 4 	Fantasy       = 5 	Historical    = 6 	Horror        = 7 	Magic         = 8 	Mystery       = 9 	Philosophical = 10 	Political     = 11 	Romance       = 12 	Science       = 13 	Superhero     = 14 	Thriller      = 15 	Western       = 16 )                         

Нам также нужно изменить структуру Book, чтобы теперь Genre имел тип int:

  type Book struct { 	ID    int 	Name  string 	Genre int // Заменили тип string на int для увеличения производительности }                         

Хотя теперь у нас есть более эффективная модель памяти для Genre, она не так удобна для человека. Если я выведу значение Book, то теперь мы получим просто целочисленное значение. Чтобы показать это, мы напишем быстрый тест:

 package books  import "testing"  func TestGenre(t *testing.T) { 	b := Book{ 		ID:    1, 		Name:  "Всё про Golang", 		Genre: Magic, 	}  	t.Logf("%+v\n", b)  	if got, exp := b.Genre, 8; got != exp { 		t.Errorf("unexpected genre.  got %d, exp %d", got, exp) 	} }                         

И вот что он выведет после запуска:

 $go test -v ./...  === RUN   TestGenre     book_test.go:12: {ID:1 Name:Всё про Golang Genre:8} --- PASS: TestGenre (0.00s)  PASS ok      bitbucket.org/ampleevee/examples.git/internal/books      0.273s   -------- Go Version: go 1.22.0                         

Заметьте, что Genre просто показывает значение 8. Каждый раз, когда мы отлаживаем код, пишем отчет и т. д., нам нужно выяснить, что на самом деле означает 8 для человека.

Для этого мы можем написать вспомогательную функцию, которая принимает значение Genre и определяет, каким должно быть «человеческое» представление:

 func GenreToString(i int) string { 	switch i { 	case 1: 		return "Adventure" 	case 2: 		return "Comic" 	case 3: 		return "Crime" 	case 4: 		return "Fiction" 	case 5: 		return "Fantasy" 	case 6: 		return "Historical" 	case 7: 		return "Horror" 	case 8: 		return "Magic" 	case 9: 		return "Mystery" 	case 10: 		return "Philosophical" 	case 11: 		return "Political" 	case 12: 		return "Romance" 	case 13: 		return "Science" 	case 14: 		return "Superhero" 	case 15: 		return "Thriller" 	case 16: 		return "Western" 	default: 		return "" 	} }                         

Более эффективный способ

Хотя весь приведенный выше код работает отлично, в нем не хватает некоторых ключевых моментов:

  • Если в будущем значение Genre изменится, нам придется не только изменить константное значение, но и обновить функцию GenreToString. Если этого не сделать, то в коде возникнет ошибка.
  • Мы не используем систему типов, чтобы инкапсулировать это поведение для Genre. Скоро мы покажем вам, что мы имеем в виду.

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

Правильный способ сделать это - не использовать жестко закодированные значения, а использовать значения самих констант:

 func GenreToString(i int) string { switch i { 	case Adventure: 		return "Adventure" 	case Comic: 		return "Comic" 	case Crime: 		return "Crime" 	case Fiction: 		return "Fiction" 	case Fantasy: 		return "Fantasy" 	case Historical: 		return "Historical" 	case Horror: 		return "Horror" 	case Magic: 		return "Magic" 	case Mystery: 		return "Mystery" 	case Philosophical: 		return "Philosophical" 	case Political: 		return "Political" 	case Romance: 		return "Romance" 	case Science: 		return "Science" 	case Superhero: 		return "Superhero" 	case Thriller: 		return "Thriller" 	case Western: 		return "Western" 	default: 		return "" 	} }

Хорошо, это гораздо чище (и читабельнее), но мы все еще не решили проблему того, что при выводе на печать отображается значение данных (int), а не «человеческое» читаемое значение.

Собственные типы Go нам в помощь

Вместо того чтобы использовать стандартный тип int для Genre, мы можем создать свой собственный тип на основе стандартного. В данном случае мы создадим новый тип Genre на основе типа int:

 type Genre int  type Book struct { 	ID    int 	Name  string 	Genre Genre // Заменили тип int на собственный Genre без потери производительности }

Теперь мы определим наши константы как типы Genre:

 const ( 	Adventure     Genre = 1 	Comic         Genre = 2 	Crime         Genre = 3 	Fiction       Genre = 4 	Fantasy       Genre = 5 	Historical    Genre = 6 	Horror        Genre = 7 	Magic         Genre = 8 	Mystery       Genre = 9 	Philosophical Genre = 10 	Political     Genre = 11 	Romance       Genre = 12 	Science       Genre = 13 	Superhero     Genre = 14 	Thriller      Genre = 15 	Western       Genre = 16 ) 

Пока что в коде нет никаких изменений. Однако теперь, когда Genre - это собственный тип, мы можем добавить к нему методы. Это позволит инкапсулировать «человеческое» поведение, которое мы хотим получить, в тип, а не в общую функцию.

Для этого мы добавим метод String к типу Genre:

 func (g Genre) String() string { 	switch g { 	case Adventure: 		return "Adventure" 	case Comic: 		return "Comic" 	case Crime: 		return "Crime" 	case Fiction: 		return "Fiction" 	case Fantasy: 		return "Fantasy" 	case Historical: 		return "Historical" 	case Horror: 		return "Horror" 	case Magic: 		return "Magic" 	case Mystery: 		return "Mystery" 	case Philosophical: 		return "Philosophical" 	case Political: 		return "Political" 	case Romance: 		return "Romance" 	case Science: 		return "Science" 	case Superhero: 		return "Superhero" 	case Thriller: 		return "Thriller" 	case Western: 		return "Western" 	default: 		return "" 	} }

Теперь мы сможем использовать метод String, когда захотим узнать, каково «человеческое» значение у Genre (данный код уже писался в другом пакете, поэтому здесь отдельно импортируется пакет books):

 package main  import ( 	"bitbucket.org/ampleevee/examples.git/internal/books" 	"fmt" )  func main() { 	b := books.Book{ 		ID:    1, 		Name:  "Всё про Go", 		Genre: books.Magic, 	} 	fmt.Println(b.Genre.String()) }                         

Вывод:

Magic

Магическое форматирование

В Go, если вы добавите метод String к любому типу, пакет fmt будет использовать его при выводе вашего типа автоматически. Благодаря этому мы увидим, что если мы распечатаем Book в наших тестах, то получим и «человекочитаемый» Genre:

 $go test -v ./...  === RUN   TestGenre     book_test.go:12: {ID:1 Name:Всё про Golang Genre:Magic} --- PASS: TestGenre (0.00s)  PASS ok      bitbucket.org/ampleevee/examples.git/internal/books      0.273s   -------- Go Version: go 1.22.0

Теперь мы видим, что в распечатанном выводе значение для Genre - Magic, а не 8. Важно также отметить, что наш тест фактически не изменился, изменился только способ, которым мы использовали наш новый тип для Genre.

А как же iota?

Те из вас, кто уже знаком с Go, возможно, посмотрели на эту задачу и спросили: «Почему вы просто не использовали iota?». iota - это идентификатор, который вы можете использовать в Go для создания увеличивающихся числовых констант. Хотя есть несколько причин, по которым я не использовал iota в этой задаче, я посвятил этой теме целую статью. Читайте об этом в статье «Где и когда использовать iota в Golang.

Резюме

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



    Добавить комментарий
    divider graphic

    Возможно, вам будет интересно

    Image
    20 марта 2025 в 17:34
    heart interface icon347

    Эффективное управление релизами it-продуктов на базе микросервисной архитектуры

    В данной статье рассказывается о том как наладить релизный процесс для it-продукта с микросервисной архитектурой

    Image
    Yevgeniy Ampleev
    Image
    05 февраля 2020 в 15:54
    heart interface icon2106

    Как высчитывать значения в Burn Down Chart в Scrum

    В это статье рассказано о том, как высчитываются параметры в Burn DownCharts в Scrum.Персональный блог.

    Image
    Yevgeniy Ampleev
    arrow-up icon