Сначала я расскажу, как представить тип суммы / объединения / варианта (-подобного) в Go.
Затем я представлю 'gosumcheck', который представляет собой статический инструмент lint. который проверяет все возможные случаи переключения типа.

Это сообщение для Адвент-календаря Hatena Engineer 2016 (японский).

Тип суммы в Go

Итак, что такое тип сумма / объединение / вариант? Из Википедии,

В информатике помеченное объединение, также называемое вариантом, вариантной записью, размеченным объединением, непересекающимся объединением или типом суммы, представляет собой структуру данных, используемую для хранения значения, которое может принимать несколько различных, но фиксированных типов.
- https://en.wikipedia.org/wiki/Tagged_union

Пример из Википедии: (двоичное дерево)

datatype tree = Leaf
              | Node of (int * tree * tree)

В этом примере «дерево» - это тип суммы, и это может быть «Лист» или «Узел». Хорошо, тогда как мы можем представить тип суммы в Go?

Взгляните на Go FAQ о типах вариантов.

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

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

- https://golang.org/doc/faq#variant_types

Да, в Go нет вариантов (сумм) типов, но вместо этого мы можем использовать интерфейс для представления типов, подобных сумме. Поскольку в FAQ в качестве примера упоминается синтаксическое дерево, давайте рассмотрим реализацию go / ast.

Тип «Узел» представляет тип узла AST, и все типы узлов должны реализовывать интерфейс узла. Интерфейс «Узел» требует «Pos () token.Pos» и «End () token.Pos», которые возвращают позицию узла. Итак, чтобы представить тип суммы как интерфейс, добавьте в интерфейс общие методы. Это полезно для пользователей и предотвращает присвоение интерфейсу неожиданного типа.

Интереснее увидеть реализацию типа «Expr» и «Stmt». Они встраивают тип «Узел», чтобы представить, что они принадлежат к типу «Узел», и у них также есть неэкспортированный «фиктивный» метод для каждого интерфейса («exprNode ()’ »и« stmtNode () ») для представления каждого типа данных. Итак, если у типа суммы нет общих полезных методов, вы можете использовать фиктивные методы.

«Внутренний» интерфейс и «публичный» интерфейс

И вот еще! Если интерфейс имеет неэкспортированные методы, внешний пакет не может создавать типы, реализующие интерфейс. Например, внешние пакеты не могут создавать свои собственные типы «Expr». Назовем этот шаблон внутренним интерфейсом. Для внутреннего интерфейса мы можем перечислить все типы, реализующие интерфейс, потому что они должны быть в одном пакете.

С другой стороны, тип «Узел» имеет только общедоступный метод, поэтому внешние пакеты могут создавать свои собственные узлы. Назовем этот шаблон «общедоступным» интерфейсом. В этом случае, если go / ast не ожидает node, который определен во внешнем пакете, и существует открытый метод, который принимает тип «Node» в качестве аргумента, такие методы должны быть небезопасными, и может произойти непредвиденное поведение.

Go Playground: https://play.golang.org/p/dPZ5UQU98S

На всякий случай может быть лучше добавить внутренний метод для суммирования типов, даже если существуют общедоступные общие методы. Кроме того, «внутренний» интерфейс полезен для проверки обработки всех возможных случаев. Я объясню это позже.

Как использовать типы сумм (или тип интерфейса) в Go

Мы используем «интерфейс» для представления типов сумм, поэтому вы можете эффективно использовать типы сумм, если знаете базовый способ работы с интерфейсом. Позвольте представить вам несколько советов.

переключатель типа

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

Go / ast / # Inspect Пример:

// Inspect the AST and print all identifiers and literals.
ast.Inspect(f, func(n ast.Node) bool {
  var s string
  switch x := n.(type) {
  case *ast.BasicLit:
   s = x.Value
  case *ast.Ident:
   s = x.Name
  }
  if s != "" {
   fmt.Printf("%s:\t%s\n", fset.Position(n.Pos()), s)
  }
  return true
})

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

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

// Ensure MyNode implements Node interface at compile time.
var _ Node = &MyNode{}
// You can also check by following line instead.
var _ Node = (*MyNode)(nil)

Перейти на игровую площадку: https://play.golang.org/p/2PKx_jLYGk

Расшифровка JSON типа Sum

Кстати, я работаю в команде Mackerel. Mackerel (https://mackerel.io/) - сервис мониторинга серверов. Я буду использовать клиент Mackerel API в Go (mackerelio / mackerel-client-go) в качестве примера использования типа суммы. Но, конечно, это всего лишь практический пример, поэтому вы можете не обращать внимания на подробности о скумбрии, чтобы понять это.

Иногда структура JSON для REST API содержит «тип» или аналогичные поля. Вы должны увидеть значение поля «тип», чтобы понять структуру. В таких случаях мы не можем легко декодировать JSON.

У Mackerel есть API для мониторинга. (Https://mackerel.io/api-docs/entry/monitors). Существует несколько типов мониторинга, таких как подключение, метрика хоста и т. Д., А поле тип представляет тип монитора. Типы мониторинга в Go определяются здесь.

type Monitor interface {
 MonitorType() string
 MonitorID() string
 MonitorName() string
 isMonitor()
}
// MonitorConnectivity represents connectivity monitor.
type MonitorConnectivity struct {
 ID                   string `json:"id,omitempty"`
 Name                 string `json:"name,omitempty"`
 Type                 string `json:"type,omitempty"`
 IsMute               bool   `json:"isMute,omitempty"`
 NotificationInterval uint64 `json:"notificationInterval,omitempty"`
 Scopes        []string `json:"scopes,omitempty"`
 ExcludeScopes []string `json:"excludeScopes,omitempty"`
}
// ...

GET / api / v0 / monitors возвращает список конфигураций монитора. Пример:

{
  "monitors": [
    {
      "id": "2cSZzK3XfmG",
      "type": "connectivity",
      "isMute": false,
      "scopes": [],
      "excludeScopes": []
    },
    {
      "id"  : "2cSZzK3XfmG",
      "type": "host",
      "isMute": false,
      "name": "disk.aa-00.writes.delta",
      "duration": 3,
      "metric": "disk.aa-00.writes.delta",
      "operator": ">",
      "warning": 20000.0,
      "critical": 400000.0,
      "scopes": [
        "SomeService"
      ],
      "excludeScopes": [
        "SomeService: db-slave-backup"
      ],
      "notificationInterval": 60
    }
  ]
}

Соответствующий метод клиента Go - FindMonitors()

func (c *Client) FindMonitors() ([]Monitor, error)

Есть 2 способа декодирования JSON и получения списка Monitor.

1) json.RawMessage

  • декодировать в список json.RawMessage
  • декодировать json.RawMessage, чтобы получить только значение типа. var typeData struct { Type String 'json:"type"'}
  • декодировать json.RawMessage для отслеживания типов с использованием значения «тип».
import "encoding/json"
func decodeMonitorFromRawMessage(rawmes []byte) (monitorI, error) {
 var typeData struct {
  Type string `json:"type"`
 }
 if err := json.Unmarshal(rawmes, &typeData); err != nil {
  return nil, err
 }
 var m monitorI
 switch typeData.Type {
 case monitorTypeConnectivity:
  m = &MonitorConnectivity{}
 case monitorTypeHostMeric:
  m = &MonitorHostMetric{}
 case monitorTypeServiceMetric:
  m = &MonitorServiceMetric{}
 case monitorTypeExternalHTTP:
  m = &MonitorExternalHTTP{}
 case monitorTypeExpression:
  m = &MonitorExpression{}
 }
 if err := json.Unmarshal(rawmes, m); err != nil {
  return nil, err
 }
 return m, nil
}

2) mitchellh / mapstructure

  • декодировать в список map[string]interface{}
  • получить значение «типа»
  • преобразовать map[string]interface{} в каждый тип монитора с помощью mitchellh / mapstructure
func decodeMonitorFromMap(mmap map[string]interface{}) (monitorI, error) {
 typ, ok := mmap["type"]
 if !ok {
  return nil, errors.New("`type` field not found")
 }
 var m monitorI
 switch typ {
 case monitorTypeConnectivity:
  m = &MonitorConnectivity{}
 case monitorTypeHostMeric:
  m = &MonitorHostMetric{}
 case monitorTypeServiceMetric:
  m = &MonitorServiceMetric{}
 case monitorTypeExternalHTTP:
  m = &MonitorExternalHTTP{}
 case monitorTypeExpression:
  m = &MonitorExpression{}
 }
 c := &mapstructure.DecoderConfig{
  TagName: "json",
  Result:  m,
 }
 d, err := mapstructure.NewDecoder(c)
 if err != nil {
  return nil, err
 }
 if err := d.Decode(mmap); err != nil {
  return nil, err
 }
 return m, nil
}

Вы можете увидеть полную реализацию и результат тестирования здесь.

$ go test -v -run="^$" -bench=Monitor_JSON_ | prettybench
benchmark                              iter    time/iter
---------                              ----    ---------
BenchmarkMonitor_JSON_mapstructure-4   1000   1.98 ms/op
BenchmarkMonitor_JSON_rawmessage-4     1000   1.40 ms/op

Перед запуском теста я подозреваю, что первый метод json.RawMessage может быть медленным, потому что ему нужно декодировать байт JSON 3 раза для каждого монитора JSON. Однако тест показывает, что он быстрее, чем второй метод mapstructure, хотя, я думаю, оба достаточно быстрые. Исходя из результатов теста и того факта, что encoding / json является стандартной библиотекой, я решил использовать метод json.RawMessage для декодирования JSON мониторов.

gosumcheck - проверить, что все дела обрабатываются в переключателе типов статически

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

Одна из лучших составляющих типов суммы заключается в том, что в функциональном языке, таком как Haskell, Scala и т. Д.… Компилятор может проверить, что все возможные случаи обрабатываются для сопоставления с образцом типа суммы. Мы можем пропустить некоторые случаи, и когда мы добавляем новый тип к типу суммы, мы должны найти все совпадения с шаблоном (или оператор переключения типа). Лучше использовать компьютеры для проверки этих случаев, чем тщательно искать такие случаи людьми.

Да, я создал инструмент статического анализа для этого для Golang!



Установка

go get -u github.com/haya14busa/gosum/cmd/gosumcheck

Пример

Результат содержит ложноположительные результаты, но обнаруживает вполне возможные ошибки!

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

Идея в основном такая же, как инструменты гуру и инструменты анализа годок. Типы, реализующие интерфейс, можно найти с помощью пакета go / types. gosumcheck собирает все возможные типы и выводит недостающие типы, которые не обрабатываются операторами case. Это просто, но работает хорошо !!

В середине этой публикации я представил «общедоступный» интерфейс и «внутренний» интерфейс. «Общедоступный» интерфейс - это базовый тип интерфейса, который имеет только общедоступный метод, и любые внешние пакеты могут определять свои собственные типы, реализующие интерфейс. Думаю, наиболее известный пример - интерфейс с «ошибкой». С другой стороны, «внутренний» интерфейс требует неэкспортированных методов, поэтому все типы, реализующие интерфейс, должны находиться в одном пакете.

Вы уже можете это заметить. «внутренний» интерфейс подходит для «gosumcheck», потому что все типы, реализующие интерфейс, являются фиксированными и находятся в одном пакете.

Но вы также можете использовать gosumcheck для общедоступного интерфейса. Хотя любой внешний пакет может добавлять типы, реализующие интерфейс, мы можем получить все зависимые пакеты, запустив «gosumcheck» в качестве линтера. Итак, мы можем перечислить возможные типы, хотя это может просто неожиданно реализовать интерфейс.

Эвристика для максимального подавления ложноположительных результатов

Поскольку переключатель типа не всегда используется для типов суммы, и есть случаи, когда нам не нужно обрабатывать все типы, результат может быть искажен ложноположительными результатами. Чтобы решить эту проблему, «gosumcheck» использует «уровень покрытия» типа switch для расчета достоверности выходных данных. Я предположил, что мы обычно не пропускаем много дел, но пропускаем лишь несколько дел. Таким образом, если уровень покрытия составляет почти 100%, gosumcheck сообщает об остальных пропущенных случаях. С другой стороны, коэффициент покрытия низкий, например менее 50% предполагает, что программист намеренно не перечислил множество случаев.

Если вы хотите, чтобы gosumcheck сообщал о большем количестве случаев, укажите флаг -min_confidence. (например, $ gosumcheck -min_confidence=0.1 ./...)

Я думаю, мы можем улучшить качество выходных данных, используя другие эвристики, поэтому, если у вас есть какие-то идеи, пожалуйста, откройте запрос на вытягивание или опубликуйте идею в системе отслеживания проблем. Https://github.com/haya14busa/gosum