Использование собственных типов в 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
    12 марта 2025 в 18:32
    heart interface icon134

    Протокол HTTP/2 и его использование в Golang

    Данная статья является переводом c оригинала - https://victoriametrics.com/blog/go-http2/. В ней подробно рассказывается о принципах функционирования протокола HTTP/2.

    Image
    Yevgeniy Ampleev
    Image
    04 февраля 2020 в 12:54
    heart interface icon2409

    Специфика работы Agile-команды в SAFe относительно Scrum на практике

    В данной статье я постараюсь описать основные и, наиболее яркие отличия фреймворка SAFe относительно SCRUM, которые я заметил за год работы в SAFe.Персональный блог.

    Image
    Yevgeniy Ampleev
    arrow-up icon