Блог им. LRNЗлые языки программирования "новой волны" [2/2]

Продолжаем разбор статьи (первая часть — тут) «Синтаксис языка Go» из мини-журнала OpenSource, №92.


Теперь — о классах (которых нет, но слово «класс» зачем-то употребляется вместо православного «struct»)
В Go нет наследования как такового, а единственный инструмент, с помощью которого можно изменять поведение классов и обеспечивать полиморфизм, являются интерфейсы.
Заговорил про наследование — не забудь про embedding.

Если вдуматься, то мы никак не указали, что наш класс MyStruct совместим с интерфейсом AddSub, но компилятор сам «догадался» об этом, сравнив сигнатуру метода Add в момент его вызова с сигнатурой интерфейса. Такое поведение называют «duck typing» (динамическая типизация, основанная на сигнатурах методов и текущей семантике) и в нем кроется, пожалуй, самая большая странность языка Go.
Проверка соответствия интерфейсов по большей части статична, и по большей части выполняется на этапе компиляции.

В самом деле, язык со строгой статической типизацией во время выполнения неожиданно начинает использовать динамическую типизацию. Для
этого программа на Go вынуждена «таскать с собой» во время выполнения полную информацию о типах всех переменных и сигнатурах всех методов, что означает нешуточные расходы памяти и меньшее быстродействие, поскольку каждый динамический вызов проверяется на соответствие интерфейсов.
См. предыдущий абзац. В тех же случаях, когда всё-таки происходит сравнение типов в рантайме, то Гугл сообщает (тут и там), что type assertion достаточно «дешёвый» в плане производительности.
А между тем о языке Vala, который использует GObject introspection в рантайме, и потому вынужден таскать с собой информацию о типах, в №85 отзывы весьма лестные, и о «нешуточных расходах памяти» — ни слова.

Автор переходит к интерфейсам:
Любой объект в Go реализует «пустой» интерфейс interface{}, и его можно использовать как «универсальный тип» так же, как в С используется указатель типа void. Для того чтобы узнать фактический тип переданного объекта, предусмотрен специальный вариант оператора switch:

  func WhichOne(x interface{}) {
          // Так называемый type-switch                         
          // синтаксис непонятен: почему не typeof(x)?          
          switch t := x.(type) {                                
          // t теперь содержит тип объекта х                    
          case bool:
                    fmt.Printf("Логический тип %t\n", t)
          case int:                                             
                    fmt.Printf("Целое число %d\n", t)           
          default:                                              
                    fmt.Printf("Что-то странное... %T\n", t)
  }

Этой функции можно передать значение любого типа: она сама определит, может ли с ним работать, – однако делается это во время выполнения и негативно влияет на быстродействие.
Это не «специальный вариант оператора switch». Просто switch в Go, в отличие от C, работает не только на целых числах.
typeof в Go есть — в пэкэдже reflect. Но я не уверен, что использование reflect.typeof() вместо «x.(T)» даст прирост быстродействия (скорее всего — наоборот).
Что касается формы «x.(T)», то она, видимо, используется из-за своей необычной формы (ни человек, ни парсер ни за что не спутают её с вызовом функции, как это могло бы быть с val.typeof() или typeof(val)).
Про быстродействие я уже писал. Ну а про то, что пустой интерфейс является аналогом «void *», и потому далеко не всегда кошерен (так уж ли много причин отказываться от статический типизации, когда она есть?) — и так понятно.

Далее написано о конкурентном программировании. Наконец-то!
Такая функция называется goroutine (русского перевода этого термина я не нашел).
Википедия доходчиво объясняет, что goroutine — это отсылка к coroutine. А русская версия этой страницы даже пытается как-то это перевести.

Из ошибок в разделе про конкурентное программирование — всё. Да, написано мало. В частности:
1) Из статьи не очевидно, что каналы могут иметь любой тип данных (не только int)
2) Из статьи не очевидна одна из основных идиом Go: «do not communicate by sharing memory; share memory by communication». Отдал данные через канал — считай, что получатель теперь владеет ими. Позволяет избежать использования mutex'ов и прочих хардкорных вещей.
3) Из статьи не очевидно, что можно передавать одни каналы через другие, что является своеобразным аналогом callback'a: «вот, я по каналу X передаю тебе вместе с данными канал Y; закончишь работать — передашь мне результаты по каналу Y».
4) Не показано использование select для опроса каналов (аналог select() в POSIX-совместимых ОС).
5) Не сказано, что goroutines могут выполняться как в одном, так и в нескольких тредах (в зависимости от ряда факторов, например от того, блокируют ли они).
6) Не сказано, что конкурентность (представление программы в виде независимых частей (те самые coroutines/goroutines) для упрощения реализации некоторых алгоритмов) != параллелизм (разбивка программы на несколько одновременно выполняющихся частей для достижения желаемого быстродействия)

И последнее: эксэпшны.
Паника похожа на генерацию исключения в С++ или Java – функция завершается, текущий стек вызовов очищается и программа аварийно останавливается, если на каком-то этапе не происходит «восстановление» (перехват исключения). Восстановление может происходить только в блоке defer и типичный пример его использования выглядит так:
func SafeDiv(a float32, b float32) float32 {
      defer func() {
          if err := recover(); err != nil {
               fmt.Println("Прекращаем панику по поводу:",err)
          }
      }()
      ret := Div(a,b)
      return ret
 }

Если функция Div начинает паниковать, то блок defer прекращает панику и сообщает о случившемся. Основное назначение паники и восстановления – корректное завершение потока, если в нем произошла ошибка. Если функция SafeDiv вызывается как goroutine, то ошибка не распространится за пределы потока и не «обрушит» всю программу. По сравнению с полноценными исключениями такая система выглядит неуклюжей и ограниченной в возможностях, но это все же лучше, чем ничего.
Остаётся только в очередной раз сослаться на FAQ, которая объясняет, почему эксэпшны — бяка. Имея опыт работы с Питоном, который любит бросать эксэпшны по всякому поводу, я могу подтвердить, что и впрямь — бяка.
Также не совсем корректно описан механизм поведения panic (всё-таки стэк не «очищается», а «раскручивается»).

На этой яркой ноте заканчивается №92. Что нас ждёт в №93? Кто знает… Но №91 обещал показать синтаксис, стандартную библиотеку, и порассуждать о перспективах и применении Go. С синтаксисом вроде всё. Значит предстоит обзор стандартной библиотеки, и «финальный мысль». Впихнуть всю немаленькую стандартную библиотеку Go в пару страниц — нереально. Скорее всего будет похоже на некое перечисление вида «есть пэкэдж X, с помощью которого можно легко, из коробки, делать Y». Возможно — с парой примеров. Ну а направление «финального мысля» уже намечается: пока что Go описан как причудливая (с дичайшим, непонятным синтаксисом) замена Питону (ибо, не смотря на все «обнаруженные» недостатки, компилируемый язык всяко быстрее интерпретируемого), особенно для написания web-сервисов, благо стандартная библиотека по своей мощи стремится к питоновской.
Так ли это? В принципе — да, Go можно (а в некоторых случаях — нужно!) использовать вместо Питона. А ещё его можно использовать вместо C и C++ в областях, где «классическая» (то есть C++'сная) объектная система не является обязательной (мне точно известно две области, где классическое ООП является панацеей: моделирование и GUI; как себя покажет Go в этих областях — зависит от программистов, но gogtk уже можно пощупать руками и составить собственное мнение).

Кстати, gc портирован под винду исключительно усилиями коммьюнити. Неужели вдруг разом столько народу настолько заинтересовалось этим еретическим, не похожим на С языком, обладающим низкой производительностью и чудовищным расходом памяти, чтобы бесплатно портировать его под вянду (ИМХО, то ещё удовольствие!)? Ужас!

В заключение не могу не отметить слабую работу автора с Гуглом, а также высокий уровень субъективности статей. Тебя никто не заставляет любить и понимать Go. Но если уж не можешь соблюдать журналистский нейтралитет, и освещаешь одно (своё) мнение, то для поддержания баланса освети и противоположное тоже! Учись у Википедии.

Вообще, мне стыдно, что пришлось писать такой пост.

EDIT: К вопросу о remove() для map:
Попробовал определить метод на map — не получилось, поскольку map — ключевое слово, а не тип. Типом может быть конкретный map, например — map[int]string, поэтому можно так:
package main

import (
	"fmt"
)

type MyMap map[int]string

func (m MyMap) remove(key int) {
	m[key] = "", false
}

func main() {
	a := MyMap{1:"one", 2:"two"}
	b, ok := a[1]
	fmt.Printf("1 = %s, %t\n", b, ok)
	a[1] = "", false
	b, ok = a[1]
	fmt.Printf("1 = %s, %t\n", b, ok)
	b, ok = a[2]
	fmt.Printf("2 = %s, %t\n", b, ok)
	a.remove(2)
	b, ok = a[2]
	fmt.Printf("2 = %s, %t\n", b, ok)
}

выводит:
1 = one, true
1 = , false
2 = two, true
2 = , false

Сразу становится очевидной проблема: затруднительно сделать универсальную удалялку без использования type assertion'ов или reflect.

EDIT: В Go версии 1 (должен выйти где-то в начале следующего года) обещают добавить delete:
map deletion

The “m[x] = ignored, false” map assignment syntax is a special case (the only 1=2 assignment), requires passing a value (ignored) that is evaluated but discarded, and requires passing a value that is nearly always a constant (the value false).

Go 1 will remove the special map assignment and introduce a new built-in function, delete: delete(m[k]) will delete the map entry retrieved by the expression m[k]. Deleting a non-existent entry is a no-op.

Gofix will convert “m[x] = ignored, false” into “delete(m[x])” when it is clear that the ignored value can be safely discarded from the program and false refers to the predefined boolean constant. It will flag other uses of the syntax for inspection by the programmer.
  • +8
  • LRN
  • 29 сентября 2011, 01:03

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

  • avatar
  • ghost
  • 29 сентября 2011, 12:12
  • #
  • 0
Почему злые? :D
Есть такое выражение — «злые языки». © К.О.
А если по теме? То есть про статью.
Это такая попытка связать словосочетания «злые языки» и «языки программирования», в результате у читателя должно сложиться нечто вроде «вот что говорят злые языки о языках программирования новой волны». К.О.
Блин… Опять… Я не про это!
А хотя похер…
Как вообще могут быть связаны заголовок и тема? :) Заголовок — чтобы привлечь внимание, заинтриговать. То есть, как «языки программирования новой волны» связаны — это и так ясно. А «злые» — это замануха :) Ну, и настроение задаёт (когда писал пост — был злой). Ну, в общем, ты понял.
+1
Точнее, уже +2
  • avatar
  • fog
  • 29 сентября 2011, 12:33
  • #
  • 0
Да, хорошо так прошёлся по автору. :-)
Отправь Шурупову в OpenSource, там такой разбор в тему смотреться будет
Отправил, а толку?
А толк, помоему, есть. ;-) Автор получил фидбек, а ты удовлетворён тем, что автор прочитал комментарии. Мир, дружба, жевачка. :-)
  • avatar
  • Qt4
  • 05 октября 2011, 20:37
  • #
  • 0
GO неразвитый и непопулярный ЯП, имхо.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.