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


Конечно в Go нельзя писать обобщенные программы, например в стиле C++ templates, которые бы практически не влияли на затраты процессорного времени. Такого механизма в языке нет и, вполне возможно, что не предвидится.


С другой стороны, язык представляет довольно мощный встроенный пакет reflect, которой позволяет производить рефлексию как объектов, так и функций. Если не ставить быстродействие во главу угла, то с помощью этого пакета можно достигать интересных и гибких решений.


В этой статье я покажу как реализовать for each в виде типонезависимой рефлексивной функции.



Проблема


В языке Go для перебора элементов коллекции (Array, Slice, String) используется конструкция for range:


for i, item := range items {
      // do something
}

Аналогичным образом можно выбрать элементы из Channel:


for item := range queue {
      // do something
}

В общем-то это перекрывает 80% потребностей в цикле for each. Но у встроенной конструкции for range есть подводные камни, которые легко продемонстрировать на небольшом примере.


Допустим мы имеем две структуры Car и Bike (представим что пишем код для автомобильного магазина):


type Car struct{
      Name string
      Count uint
      Price float64
}

type Bike struct{
      Name string
      Count uint
      Price float64
}

Нам нужно подсчитать стоимость всех автомобилей и мотоциклов которые у нас есть в наличии.
Что бы это можно было сделать одним циклом в Go требуется новый тип, который обобщает доступ к полям:


type Vehicle interface{
      GetCount() uint
      GetPrice() float64
}

func (c Car) GetCount() uint { return c.Count; }
func (c Car) GetPrice() float64 { return c.Price; }
func (b Bike) GetCount() uint { return b.Count; }
func (b Bike) GetPrice() float64 { return b.Price; }

Теперь суммарную стоимость можно подсчитать организуя обход vehicles с помощью for range:


vehicles := []Vehicle{
      Car{"Banshee ", 1, 10000},
      Car{"Enforcer ", 3, 15000},
      Car{"Firetruck", 4, 20000},
      Bike{"Sanchez", 2, 5000},
      Bike{"Freeway", 2, 5000},
}

total := float64(0)

for _, vehicle := range vehicles {
    total += float64(vehicle.GetCount()) * vehicle.GetPrice()
}

fmt.Println("total", total)
// $ total 155000

Что бы не писать цикл каждый раз мы можем написать функцию которая принимает на вход тип []Vehicle и возвращает численный результат:


func GetTotalPrice(vehicles []Vehicle) float64 {
    var total float64

    for _, vehicle := range vehicles {
        total += float64(vehicle.GetCount()) * vehicle.GetPrice()
    }

    return total
}

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


  • Ограничение строгой типизации элементов. Т.к. конструкция for/range строго типизирована и не производит приведение типов элементов, то приходится явно указывать ожидаемый тип элементов в сигнатуре функции. Как следствие нет возможности передать срез []Car или []Bike напрямую, хотя оба типа — и Car, и Bike, удовлетворяют условиям интерфейса Vehicle:

cars := []Car{
      Car{"Banshee ", 1, 10000},
      Car{"Enforcer ", 3, 15000},
      Car{"Firetruck", 4, 20000},
}

fmt.Println("total", GetTotalPrice(cars))
// Compilation error: cannot use cars (type []Car) as type []Vehicle in argument to GetTotalPrice

  • Ограничение строгой типизации коллекции. Например, нет возмоности передать вместо среза []Vehicle словарь map[int]Vehicle:

cars := map[int]Vehicle{
      1: Car{"Banshee ", 1, 10000},
      2: Car{"Enforcer ", 3, 15000},
      3: Car{"Firetruck", 4, 20000},
}

fmt.Println("total", GetTotalPrice(cars))
// Compilation error: cannot use vehicles (type map[int]Vehicle) as type []Vehicle in argument to GetTotalPrice

Другими словами, for/range не позволяет выбрать произвольную часть кода и обернуть её в функцию не теряя гибкости.


Решение


Описанная проблема во многих языках со строгой типизацией решается привлечением механизма параметрического полиморфизма (generics, templates). Но взамен параметрического полиформизма авторы Go представили встроенный пакет reflect реализующий механизм рефлексии.
С одной стороны рефлексия является более затратным по ресурсам решением, но с другой, она позволяет создавать более гибкие и интеллектуальные алгоритмы.


Рефлексия типа (reflect.Type)


По сути в пакете reflect существует два вида рефлексии — это рефлексия типа reflect.Type и рефлексия значения reflect.Value. Рефлексия типа описывает исключительно свойства типа, поэтому две разные переменные с одним типом будут иметь одну и ту же рефлексию типа.


var i, j int
var k float32

fmt.Println(reflect.TypeOf(i) == reflect.TypeOf(j)) // true
fmt.Println(reflect.TypeOf(i) == reflect.TypeOf(k)) // false

Т.к. в Go типы конструируются на основе базовых типов, то для классификации существует специальное перечисление с типом Kind:


const (
        Invalid Kind = iota
        Bool
        Int
        Int8
        Int16
        Int32
        Int64
        Uint
        Uint8
        Uint16
        Uint32
        Uint64
        Uintptr
        Float32
        Float64
        Complex64
        Complex128
        Array
        Chan
        Func
        Interface
        Map
        Ptr
        Slice
        String
        Struct
        UnsafePointer
)

Таким образом, имея доступ к рефлексии типа reflect.Type, можно всегда узнать род типа, который позволяет произвести диспетчеризацию без определения полного типа переменной. Например, достаточно знать что переменная является функцией, не вдаваясь в подробности какой конкретный тип имеет эта функция:


valueType := reflect.TypeOf(value)
switch valuteType.Kind() {
case reflect.Func:
      fmt.Println("It's a function")
default:
      fmt.Println("It's something else")
}

Для удобства записи будем именовать рефлексию типа некоторой переменной тем же именем, но с суффиксом Type:


callbackType := reflect.TypeOf(callback)
collectionType := reflect.TypeOf(collection)

Кроме рода к которому принадлежит тип, с помощью рефлексии типа можно узнать остальную статическую информацию о типе (т.е. ту информацию которая не меняется во время выполнения). Например, можно узнать количество аргументов функции и тип ожидаемого аргумента на некоторой позиции:


if callbackType.NumIn() > 0 {
      keyType := callbackType.In(0) // expected argument type at zeroth position
}

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


type Person struct{
      Name string
      Email string
}

structType := reflect.TypeOf(Person{})

fmt.Println(structType.Field(0).Name) // Name
fmt.Println(structType.Field(1).Name) // Email

Размер массива так же можно узнать через рефлексию типа:


array := [3]int{1, 2, 3}
arrayType := reflect.TypeOf(array)

fmt.Println(arrayType.Len()) // 3

Но размер среза через рефлексию типа уже узнать нельзя, т.к. эта информация меняется во время выполнения.


slice := []int{1, 2, 3}
sliceType := reflect.TypeOf(slice)

fmt.Println(sliceType.Len()) // panic!

Рефлексия значения (reflect.Value)


Аналогично рефлексии типа, в Go существует рефлексия значения reflect.Value, которая отражает свойства конкретного значения хранящегося в переменной. Может показаться, что это довольно тривиальная рефлексия, но т.к. в Go переменная с типом interface{} может хранить всё что угодно — функцию, число, структуру и т.д., то и рефлексия значения вынуждена представлять в более–менее безопасном виде доступ ко всем вероятным возможностям объекта. Что, конечно же, порождает довольно длинный список методов.


Например, рефлексия функции может использоваться для вызова — достаточно передать список аргументов приведённый к типу reflect.Value:


 _callback := reflect.ValueOf(callback)
 _callback.Call([]reflect.Value{ values })

Рефлексию коллекции (среза, массива, строки и т.д.) можно использовать для доступа к элементам:


_collection := reflect.ValueOf(collection)

for i := 0; i < _collection.Len(); i++ {
      fmt.Println(_collection.Index(i))
}

Аналогичным образом работает рефлексия словаря — для обхода нужно получить список ключей через метод MapKeys и выбрать элементы через MapIndex:


for _, k := range _collection.MapKeys() {
      keyValueCallback(k, _collection.MapIndex(k))
}

С помощью рефлексии структуры можно получить значения членов. При этом названия и типы членов следует получать из рефлексии типа структуры:


_struct := reflect.ValueOf(aStructIstance)

for i := 0; i < _struct.NumField(); i++ {
      name := structType.Field(i).Name
      fmt.Println(name, _struct.Field(i))
}

Рефлексивный цикл for each


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


Т.к. единственная возможность в Go передать произвольный тип функции это указать тип interface{}, то в теле функции необходимо произвести проверки на основе информация содержащейся в рефлексии типа callbackType:


  • убедиться что функция обратного вызова действительно является функцией (через метод calbackType.Kind())
  • выяснить количество ожидаемых аргументов (метод callbackType.NumIn())
  • в случае провала вызвать panic()

В итоге получается примерно такой код:


func ForEach(collection, callback interface{}) {
      callbackType := reflect.TypeOf(callback)
      _callback := reflect.ValueOf(callback)

      if callbackType.Kind() != reflect.Func {
            panic("foreach: the second argument should be a function")
      }

      switch callbackType.NumIn() {
      case 1:
            // Callback expects only value
      case 2:
            // Callback expects key-value pair
      default:
            panic("foreach: the function should have 1 or 2 input arguments")
      }
}

Теперь требуется спроектировать вспомогательную функцию которая будет производить обход по коллекции.
В неё удобнее передавать обратный вызов не в бестиповом виде, а в виде функции с двумя аргументами принимающую рефлексии ключа и элемента:


func eachKeyValue(collection interface{}, keyValueCallback func(k, v reflect.Value)) {
      _collection := reflect.ValueOf(collection)
      collectionType := reflect.TypeOf(collection)

      switch collectionType.Kind() {
            // loops
      }
}

Т.к. алгоритм прохода коллекциии зависит от рода который можно получить через метод Kind() рефлексии типа, то для диспетчеризации удобно воспользоваться конструкцией switch-case:


switch collectionType.Kind() {
case reflect.Array: fallthrough
case reflect.Slice: fallthrough
case reflect.String:
      for i := 0; i < _collection.Len(); i++ {
            keyValueCallback(reflect.ValueOf(i), _collection.Index(i))
      }
case reflect.Map:
      for _, k := range _collection.MapKeys() {
            keyValueCallback(k, _collection.MapIndex(k))
      }
case reflect.Chan:
      i := 0
      for {
            elementValue, ok := _collection.Recv()
            if !ok {
                  break
            }
            keyValueCallback(reflect.ValueOf(i), elementValue)
            i += 1
      }
case reflect.Struct:
      for i := 0; i < _collection.NumField(); i++ {
            name := collectionType.Field(i).Name
            keyValueCallback(reflect.ValueOf(name), _collection.Field(i))
      }
default:
      keyValueCallback(reflect.ValueOf(nil), _collection)
}

Как видно из кода, обход массива, среза и строки происходит одинаково. Словарь, канал и структура имеют свой собственный алгоритм обхода. В случае, если род коллекции не подпадает ни под один из перечисленных, алгоритм пытается передать в обратный вызов саму коллекцию, прим этом в качестве ключа указывается рефлексия указателя nil (которая на вызов метод IsValid() возвращает false).


Теперь, имея функцию производяющую беcтиповый обход коллекции, можно адаптирвоать её к вызову из функции ForEach обернув в замыкание. Это и есть окончательное решение:


func ForEach(collection, callback interface{}) {
      callbackType := reflect.TypeOf(callback)
      _callback := reflect.ValueOf(callback)

      if callbackType.Kind() != reflect.Func {
            panic("foreach: the second argument should be a function")
      }

      switch callbackType.NumIn() {
      case 1:
            eachKeyValue(collection, func(_key, _value reflect.Value){
                  _callback.Call([]reflect.Value{ _value })
            })
      case 2:
            keyType := callbackType.In(0)
            eachKeyValue(collection, func(_key, _value reflect.Value){
                  if !_key.IsValid() {
                        _callback.Call([]reflect.Value{reflect.Zero(keyType), _value })
                        return
                  }

                  _callback.Call([]reflect.Value{ _key, _value })
            })
      default:
            panic("foreach: the function should have 1 or 2 input arguments")
      }
}

Надо заметить, что в случае когда функция обратного вызова ожидает передачу двух аргументов (пары ключ/значение) необходимо производить проверку корректности ключа, т.к. он может оказаться невалидным. В последнем случае на основе типа ключа конструируется нулевой объект.


Примеры


Теперь пришло время продемонстироровать что же даёт наш подход. Если вернуться к проблеме, мы теперь можем решить её таким путём:


func GetTotalPrice(vehicles interface{}) float64 {
      var total float64

      ForEach(vehicles, func(vehicle Vehicle) {
            total += float64(vehicle.GetCount()) * vehicle.GetPrice()
      })

      return total
}

Эта функция, в отличии от приведённой в начале статьи, гораздо гибче, т.к. позволяет подсчитывать сумму вне зависимости от типа коллекции и не обязывает приводить тип элементов к интерфейсу Vehicle:


vehicles := []Vehicle{
      Car{"Banshee ", 1, 10000},
      Bike{"Sanchez", 2, 5000},
}

cars := []Car{
      Car{"Enforcer ", 3, 15000},
      Car{"Firetruck", 4, 20000},
}

vehicleMap := map[int]Vehicle{
      1: Car{"Banshee ", 1, 10000},
      2: Bike{"Sanchez", 2, 5000},
}

vehicleQueue := make(chan Vehicle, 2)
vehicleQueue <- Car{"Banshee ", 1, 10000}
vehicleQueue <- Bike{"Sanchez", 2, 5000}
close(vehicleQueue)

garage := struct{
      MyCar Car
      MyBike Bike
}{
      Car{"Banshee ", 1, 10000},
      Bike{"Sanchez", 1, 5000},
}

fmt.Println(GetTotalPrice(vehicles))  // 20000
fmt.Println(GetTotalPrice(cars)) // 125000
fmt.Println(GetTotalPrice(vehicleMap)) // 20000
fmt.Println(GetTotalPrice(vehicleQueue)) // 20000
fmt.Println(GetTotalPrice(garage)) // 15000

И небольшой бенчмарк для двух идентичных циклов, который наглядно показывает за счёт чего достигается гибкость:


// BenchmarkForEachVehicles1M
total := 0.0
for _, v := range vehicles {
      total += v.GetPrice()
}

//BenchmarkForRangeVehicles1M
total := 0.0
ForEach(vehicles, func(v Vehicle) {
      total += v.GetPrice()
})

PASS
BenchmarkForEachVehicles1M-2    2000000000           0.20 ns/op
BenchmarkForRangeVehicles1M-2   2000000000           0.01 ns/op

Заключение


Да, в Go нет параметрического полиформизма. Но зато есть пакет reflect, который предоставляет обширные возможности в области метапрограммирования. Код с использованием reflect конечно же выглядит намного сложнее, чем типичный код на Go. С другой стороны, рефлексивные функции позволяют создавать более гибкие решения. Это очень важно при написании прикладных библиотек, например, при реализации концепции Active Record.


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


github Исходный код на github

Поделиться с друзьями
-->

Комментарии (34)


  1. VGrabko
    24.07.2016 21:20
    +5

    Надо делать в 2 цикла. А то нагородили рефлексии тут. Если люди начнут юзать рефлексию повсеместно то какой профит от Go вообще? Код же там не очевидный. У меня на работе там где заюзана рефлексия около 300 строк комментариев и один фиг с наскоку не понять что за магия.


    1. deniskreshikhin
      24.07.2016 22:41
      +2

      Спасибо за критическое мнение -)

      Статья больше не про цикл, а про проблему с которой сталкиваются программисты на Go. Эта проблема, как правило, возникает чаще всего именно с for-range.

      Что касается быстродействия, то если бы быстродействие всегда было ключевым критерием, то люди бы писали только на C (или даже asm'е). Но кроме быстродействия порой нужно иметь гибкость, так в общем-то появилось куча языков с динамической типизацией (perl, python, ruby, php, javascript, lua etc). По мне Go тем и отличается, что не только быстр, но еще и представляет кучу плюшек, в том числе и рефлексию.

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

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

      Что касается сложности кода с рефлекскией. Да он сложен внутри, но зато прост извне. Это важно допустим когда есть желание предоставить гибкий однообразный API. Здесь многое зависит от вкуса. Я люблю что бы была одна пусть сложная функция в 100 строчек, чем тысячи строчек однообразного кода.

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


      1. VGrabko
        25.07.2016 03:39

        создатели языка говорят что рефлексию юзать это плохой тон.


        1. deniskreshikhin
          25.07.2016 10:44
          +1

          Что касается рефлексии, то Роб Пайк писал, что:

          «It's a powerful tool that should be used with care and avoided unless strictly necessary.»

          Не думаю, что если бы он считал рефлексии дурным тоном, то использовал бы словосочетание «powerfool tool».


          1. ufm
            25.07.2016 15:36

            ящик с динамитом тоже очень «powerfool tool». Но использовать его везде не думая — явный дурной тон.


            1. deniskreshikhin
              25.07.2016 16:41
              +1

              Больше конструктива пожалуйста)


          1. RomanArzumanyan
            25.07.2016 18:20

            Offtopic: powerfool tool — это прекрасная игра слов. Думаю, может быть использовано в качестве мема. Как нечто, что при попытке выстрела в ногу, отрывает весь пояс нижних конечностей.


      1. VGrabko
        25.07.2016 03:45
        +1

        а дублирование побороть функцией в 2 аргументами :D


      1. Aleksi
        29.07.2016 19:26

        Статья больше не про цикл, а про проблему с которой сталкиваются программисты на Go. Эта проблема, как правило, возникает чаще всего именно с for-range.

        Это проблема, с которой сталкиваются начинающие программисты на Go, пришедшии с JavaScript, Python и Ruby. Не нужно писать на Go как на этих языках. Не нужно бороться с языком.


        1. deniskreshikhin
          29.07.2016 21:16

          Это проблема, с которой сталкиваются начинающие программисты на Go, пришедшии с JavaScript, Python и Ruby

          Не согласен, по мне эта проблема возникает у людей которые до Go использовали C++, Java etc где есть дженерики, шаблоны и т.п. инструменты, и в то же время есть строгая типизация. Т.е. я не видел что бы кто-то задавал вопросы типа «Как смапить массив в lodash стиле»? В основном звучит вопрос «Где дежнерики?»

          Не нужно бороться с языком.

          Боротья с языком никто не предлагает, рефлексия это часть стандартной библиотеки.


  1. ZurgInq
    24.07.2016 23:44

    Более идиоматическим решением будет кодогенерация. Наряду с пакетом reflect, существует возможность спарсить исходники и получить доступ к ast. Правда такая реализация будет пожалуй даже посложнее, чем пляски с reflect.


  1. arvitaly
    25.07.2016 04:09
    +2

    Массивы в go — низкоуровневый тип. Он не предназачен для обобщения. Создавайте свою коллекцию.
    type VehicleCollection struct {
    vehicles []Vehicle
    }
    https://play.golang.org/p/2sfhGl5YlC


    1. VGrabko
      25.07.2016 04:18
      -4

      +


    1. deniskreshikhin
      25.07.2016 10:31
      +1

      Хорошо, у меня есть массив []Car, как его добавить в вашу коллекцию не делая цикл for-range?


      1. arvitaly
        25.07.2016 12:06
        +1

        Откуда он взялся? Я же написал, что нужно оперировать сущностями вашей модели, а не типами Go. Т.е. массив []Car не должен существовать отдельно.
        Приведите бизнес-кейс (не абстрактный), тогда я опишу, как вижу решение без рефлексии.
        А вообще, есть простое правило, рефлексия нужна только для удобства работы с внешними источниками: базы данных, сетевые запросы и т.д. И никогда не нужна для описания вашей модели.


        1. deniskreshikhin
          25.07.2016 12:37
          +1

          Да это не важно откуда он взялся, если следовать Dependency inversion principle, то:

          A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
          B. Abstractions should not depend on details. Details should depend on abstractions.


          Т.е. что бы рассчитать сумму, вы вынуждены подгонять все под массив []Vehicle, который у вас находится в реализации (low-level module), и не важно каким образом вы это делаете в цикле for-range, в методе `Add`, или как-то еще. Это не лучшая практика с позиции DI.

          Рефлексивный foreach производит приведение типов самостоятельно, поэтому архитектурно это более правильно решение, т.к. главное что бы ваша коллекция удовлетворяла двум требованиям: имела семантику коллекции (массив, срез, словарь, канал, поля структуры и т.д.) и хранила элементы реализующие интерфейс Vehicle.

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

          В статье рефлексия для описания модели не используется соврешенно. Она испольуется внутри рефлексивной функции.


          1. arvitaly
            25.07.2016 13:23
            -1

            Еще как важно, он не существует отдельно от структуры моей модели.

            «Т.е. что бы рассчитать сумму, вы вынуждены подгонять все под массив []Vehicle»
            Что означает эта фраза? Обобщение Vehicle придумали вы, а теперь хотите избавиться от автоматической проверки его же интерфейсов с помощью рефлексии. У меня же есть абстракция VehicleCollection, которая никак не зависит от реализации, можете хранить там все в строке, вообще не используя массивы. Или вычислять AllPrice сразу при добавлении и не хранить элементы вообще. Все по одной простой причине, мне не нужно перебирать Vehicle, как массив, мне нужны методы для работы конкретно с этой коллекцией.
            А вам кажется не нужен Go. В любом динамически-типизированном языке уже есть все, что вы хотите.
            Одним из основных преимуществ Go является возможность понимать чужой код. Это достигается за счет Ограниченного числа типов, возможностей их преобразования и синтаксиса. Как только вы вводите новую конструкцию, вы теряете эту возможность.
            Мало того, я утверждаю, что в вашем примере это совершенно не нужно, а рассуждения об абстрактных задачах всегда приводят к overhead ненужных абстракций.


            1. deniskreshikhin
              25.07.2016 16:39

              У меня же есть абстракция VehicleCollection, которая никак не зависит от реализации, можете хранить там все в строке, вообще не используя массивы. Или вычислять AllPrice сразу при добавлении и не хранить элементы вообще. Все по одной простой причине, мне не нужно перебирать Vehicle, как массив, мне нужны методы для работы конкретно с этой коллекцией.

              У вас понятие абстракции сводися по сути к понятию класс из ООП. Но Go не ООП язык программирования, и если бы люди имели ввиду под понятием абстракция класс, то они и писали бы слово класс. Понятие абстракция более широкоее нежели понятие класс.

              А вам кажется не нужен Go. В любом динамически-типизированном языке уже есть все, что вы хотите.

              Не всё там есть, например, во всех динамически-типизированных языках программирования огромная проблема с подключением нативных библиотек через хедеры .h. Например, нормальный биндинг к OpenGL есть только у Python. С OpenCL вообще беда у всех «скриптовых» языков. А в Go это делается очень просто.

              Вообще, странно что люди стали рассматривать Go как некоторую замену C по быстродействию. В этом плане есть гораздо более удачные языки C++, D, Rust etc. Там столько этого быстродействия, что утонуть можно.

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


              Я несколько раз описывал контекст в котором этот пример надо воспринимать, жаль что все это игнорируют. Видимо надо было как-то это выделить.

              Вы предлагаете заменить общее решение (рефлексивный ForEach) на частное решение (VehicleCollection). Но я привел эту частную проблему лишь как иллюстрацию общей проблемы. В Go из-за строгой типизации и отсутсвия параметрического полиморфизма нет возможности разрешить эту проблему кроме как через рефлексию. Статья об этом.

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


              1. arvitaly
                25.07.2016 17:24
                +1

                Go — еще какой ООП язык, и да, структуры и интерфейсы (заметьте, не классы) являются его основными инструментами. Подозреваю, что вы склоняетесь к какому-нибудь Haskell или Scala, но опять же зачем вам тогда Go?
                Вы говорите, потому что можно подключить OpenGL, тогда используйте C++, раз остальные преимущества Go вы предпочитаете переписывать.
                И я ничего не писал про быстродействие, я не против рефлексии, речь именно о композиции, именно это сильная сторона Go. Как только вам хочется рефлексии, значит где-то лишняя абстракция, если речь не идет о внешних источниках.

                Именно в замене общих решений на частные и заключается простота Go и проблемы тут нет, это просто другой язык.
                Я ничего не имею против статей о рефлексии в Go (хотя я тоже могу сказать, что там все просто, достаточно разобраться с указателями), но не могу согласиться с попыткой уничтожения достоинств языка (если это не шутка) без аргументов. Поэтому я попросил описать бизнес-кейс, вдруг действительно существует ситуация, в которой это необходимо, и нужно менять язык? Но пока таких примеров я не видел.
                Это равносильно подмене нативных прототипов в JavaScript, или перегрузке оператора плюс на минус где-нибудь еще.


                1. deniskreshikhin
                  25.07.2016 17:38

                  Вы говорите, потому что можно подключить OpenGL, тогда используйте C++, раз остальные преимущества Go вы предпочитаете переписывать.

                  В C++ нет рефлексии, допустим такие вещи как маршалинг (который базируется на рефлексии) там являются большой головной болью. В Go это все идет «из коробки».

                  Как только вам хочется рефлексии, значит где-то лишняя абстракция, если речь не идет о внешних источниках.

                  В системных библиотека рефлексии используются сплошь и рядом. Получается, у разработчиков системных библиотек Go тоже где-то лишнии абстракции? Ну думаю.

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

                  Тут каждый сам выбирает. В тех приложениях которые я пишу Go прекрасно себе показывает и с рефлексиями. Я вполне доволен. Поэтому и делюсь опытом.


                  1. arvitaly
                    25.07.2016 18:10

                    Системные библиотеки поэтому так и называются, что работают с внешними источниками (через системные вызовы) И, в десятый раз, я не против рефлексии!

                    По поводу «прекрасно показывает», вы так и не привели конкретный кейс, где хорошо показывает себя рефлексия. Пока вы просто не пользуетесь языком, а пытаетесь переписать его, чтобы использовать свой личный предыдущий опыт. Ок, ваш подход подойдет тем, кто перешел из другого языка и кому лень разбираться в best practices Go.


                    1. deniskreshikhin
                      25.07.2016 18:46
                      +1

                      Окей, спасибо за отзыв.

                      Постараюсь учесть ваши замечания при подготовке следующей статьи про рефлексию.


  1. youROCK
    25.07.2016 08:15
    +1

    Вы теряете type safety и вносите возможность легко получить панику во время работы вместо того, чтобы получить ее во время компиляции. Это делает неочевидным профит от использования языков со статической типизацией.


    1. deniskreshikhin
      25.07.2016 10:38
      -2

      Старая как мир тема. Безопасность типов обеспечивает отлов довольно узкого класс багов, так что нельзя сказать что в целом приложеие в чем-то теряет при хорошего покрытия тестами.

      The general argument for static types is that it catches bugs that are otherwise hard to find. But I discovered that in the presence of SelfTestingCode, most bugs that static types would have were found just as easily by the tests. Since the tests found much more than type errors, you needed them in either a static or dynamically typed language, so having the static typing gave you little gain.

      http://martinfowler.com/bliki/DynamicTyping.html


  1. Stronix
    25.07.2016 12:36

    Или я неверно понял задачу, или зачем всё усложнять?
    https://play.golang.org/p/ZX9kkuqQye


    1. deniskreshikhin
      25.07.2016 12:50

      Если кратко. У вас есть некоторая коллекция (что это конкретно — массив, срез, словарь, канал вы не знаете). В этой коллекции хранятся элементы с типом реализующим интерефейс Vehicle. Нужно рассчитать сумму. Рефлексивный foreach позволяет это сделать так:


      total := float64(0)
      ForEach(collection, func(v Vehicle){
          total += v.GetPrice()
      })


      1. Stronix
        25.07.2016 13:03

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


        1. deniskreshikhin
          25.07.2016 13:08

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


  1. ulvham
    26.07.2016 06:42

    С точки зрения среднестатистического программиста — читабельность всегда актуальнее. А вот, если вы делаете пакет для Go, то тут, наверное, «изкоробки» будет лучше.


  1. ardente
    26.07.2016 11:21

    Мне кажется, для иллюстрации рефлексии пример с коллекциями выбран не совсем удачно: обработка типа коллекции зашита в eachKeyValue, что может создать проблемы при расширении ПО (новый тип коллекции, реализация Fold/Map/etc). Задачи перебора коллекций в Go лучше решать через итераторы, например: play.golang.org/p/FPm0vTKYaJ или через каналы, например: play.golang.org/p/seSoJfz6kH


    1. deniskreshikhin
      26.07.2016 11:26

      Виды коллекции в Golang фиксированы, так что проблемы в расширении нет. (В любом случае коллекцию можно обернуть в channel и передать как channel.)

      Я не имею ничего против частных решений, но статья про общее решение без привязки к конкретным типам.


  1. snuk182
    26.07.2016 13:01

    Для даункастинга слайсов есть еще более другое колдунство

    cars := *(*[]Car)(unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&vehicles)))) //cars []Car <= vehicles []Vehicle
    

    Очень помогло в свое время написать сортировку массивов разного типа, которые реализуют один общий интерфейс (по результатам вызова которого происходит сортировка)


    1. deniskreshikhin
      26.07.2016 13:16

      Интересное решение)

      Сортировка без `sort.Sort`, напрямую как я понял?


      1. snuk182
        26.07.2016 13:49

        Почему же, вот:

        type sortvehicles []Vehicle
        
        func (this sortvehicles) Len() int { //sort.Sort
        	return len(this)
        }
        
        func (this sortvehicles) Less(i, j int) bool {//sort.Sort
        	return this[i].ID() < this[j].ID()
        }
        func (this sortvehicles) Swap(i, j int) {//sort.Sort
        	this[i], this[j] = this[j], this[i]
        }
        

        А потом это все счастье вызывается как-то так:
        sort.Sort(sortvehicles(*(*[]Vehicle)(unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&cars))))))
        

        при условии, что нам надо сортировать по Vehicle.ID(), но cars у нас это []Car, лежащий вообще в отдельном месте