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

зачем нужны дженерики?

Обобщения могут помочь избежать написания шаблонного кода для одной и той же функциональности для разных типов.

Естественно, я был взволнован, когда было объявлено, что дженерики будут представлены в версии 1.18.

Хорошим примером варианта использования является копирование фрагмента.

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

func CopySlice[T any](slice []T) []T {
   newSlice := make([]T, len(slice))

   for i := 0; i < len(slice); i++ {
      newSlice[i] = slice[i]
   }

   return newSlice
}

И затем, когда мы вызываем функцию для любого типа среза, мы можем указать тип для компилятора.

newsSlice := CopySlice[string]([]string{"a", "b"})

или позвольте компилятору вывести тип и просто вызвать

newsSlice := CopySlice([]string{"a", "b"})

Как это работает?

Компилятор ищет синтаксис `[]` и во время компиляции определяет тип, который использует вызывающая сторона.

Хорошо, давайте реализуем что-то более сложное.

Вариант:

что такое опция?

Для тех из вас, кто знаком с Rust или Scala, я имею в виду тот же тип, что и в этих языках.

Опция — это объект, который может либо иметь значение, либо не иметь значения.

Таким образом, тип Option может быть Some(value) или None, и мы можем использовать для него несколько функций, например, при наличии необязательного значения в json мы можем использовать Option вместо указателя и не проверять, если значение == nil.

Итак, как дженерики помогают нам это реализовать?

Вместо реализации OptionString/OptionInt и так далее…

мы можем реализовать Option[T any] для любого типа.

(любой — это ключевое слово для любого типа любого=интерфейса{})

Первый способ, который приходит на ум, — реализовать Option как таковой:

type Option[T any] interface {
   Get() T
   GetOrElse(other T) 
    ...
}

и иметь типы None и Some, реализующие этот интерфейс, ноэта реализация будет проблематичной для Unmarshaling json, поскольку нам нужно будет знать конкретный тип для реализации маршалированного интерфейса (подробнее я могу рассказать в комментарии при необходимости).

Моя реализация:

type Option[T any] struct {
   value  T
   isFull bool
}

это в основном все, что нам нужно знать о типе:

значение и если оно заполнено.

Теперь давайте реализуем конструкторы, которые могут создавать Some или None:

func Some[T any](value T) Option[T] {
   return Option[T]{value: value, isFull: true}
}

func None[T any]() Option[T] {
   return Option[T]{isFull : false}
}

Теперь давайте реализуем некоторые базовые функции (можно и нужно добавить гораздо больше)

func (s *Option[T]) Get() T {
   if s.isFull {
      return s.value
   } else {
      panic("cannot get from None type")

   }
}

func (s *Option[T]) GetOrElse(other T) T {
   if s.isFull {
      return s.value
   } else {
      return other
   }
}

func (s *Option[_]) IsEmpty() bool {
   return !s.isFull
}

Следующим шагом будет его вывод на стандартный вывод так, как мы этого хотим, поэтому для этого мы реализуем пользовательскую функцию String(). Таким образом, при вызове функций пакета fmt для этого типа будет использоваться наша пользовательская функция String.

func (s Option[T]) String() string {
   if s.isFull {
      return fmt.Sprintf("Some(%v)", s.value)
   } else {
      return "None"
   }
}

последнее, но не менее важное: мы должны иметь возможность правильно писать и читать из json.

Для этого мы реализуем наши пользовательские функции UnmarshalJSON и MarshalJSON. поэтому нулевой тип json будет None, а если он не найден, также будет none (поскольку при создании экземпляра нового параметра isFull будет ложным -> при необходимости дополнительная информация в комментариях).

Мы будем использовать подчеркивание, когда нам не нужно знать тип в реализации функции.

func (s Option[_]) MarshalJSON() ([]byte, error) {
   if s.isFull {
      return json.Marshal(s.value)
   } else {
      return []byte("null"), nil
   }
}

func (s *Option[_]) UnmarshalJSON(data []byte) error {

   if string(data) == "null" {
      s.isFull = false

      return nil
   }

   err := json.Unmarshal(data, &s.value)

   if err != nil {
      return err
   }

   s.isFull = true

   return nil
}

Отлично, как мы его используем?

Допустим, у нас есть Пользователь и фамилия, а также возраст не являются обязательными.

Мы определяем пользователя как такового:

type User struct {
   firstName string         
   lastName  Option[string] 
   Age       Option[int]    
}

Давайте создадим нового пользователя и распечатаем его:

user := User{
   FirstName: "aryeh",
   LastName:  Some("lev"),
}

fmt.Printf("%v", user)

будет напечатано:

{aryeh Some(lev) None}

Теперь, если мы создадим json из этого объекта:

b, err := json.Marshal(user)

fmt.Println(string(b))

будет напечатано:

{"first_name":"aryeh","last_name":"lev",",age":null}

Теперь давайте прочитаем объект json, где возраст не указан:

var user1 User

err := json.Unmarshal([]byte(`{"first_name" : "aryeh", "last_name" : "klein"}`), &user1)
if err != nil {
   return
}

fmt.Printf("%v", user)

будет напечатано:

{aryeh Some(lev) None}

Выводы:

Дженерики могут быть отличным инструментом и имеют множество вариантов использования.

Мы рассмотрели множество тем в одной статье, таких как настраиваемые json marshallers и т. д., чтобы попытаться смоделировать реальный вариант использования.

Исходный код можно найти здесь:

https://github.com/aryehlev/option

И открыт для взносов.