Перевод статьи Go For C++ Programmers с официального сайта языка Go. Go — язык системного программирования, созданный для широкого применения, как и C++. Здесь приводятся некоторые заметки для опытных программистов C++. Этот документ описывает различия между Go и C++, но почти ничего здесь нет об их сходствах. Концептуальные различия— В Go нет классов с конструкторами или деструкторами. Вместо методов класса, иерархии наследования классов и виртуальных функций, в Go имеются интерфейсы, которые будут рассмотрены более детально позднее. Интерфейсы также используются там, где в C++ используются шаблоны. — В Go используется сборка мусора. Нет необходимости (или возможности) освобождать память прямым образом. Сборка мусора инкрементная и высокоэффективна на современных процессорах. — В Go есть указатели, но нет арифметики для них. Вы не сможете использовать переменную-указатель для прохода по байтам или строке. — Массивы в Go являются значениями первого класса. Когда массив используется в качестве параметра функции, функция получает копию массива, а не указатель на него. Тем не менее, на практике функции часто используют срезы для параметров; срезы содержат указатели к базовым массивам. Подробнее срезы описаны ниже. — В языке предусмотрены строки. Будучи один раз созданными, они не могут меняться. — В языке предусмотрены хеш-таблицы. Они называются словарями (англ. maps). — Языком предусмотрены разделенные потоки исполнения и каналы связи между ними. Они также детально описаны ниже. — Некоторые типы (словари и каналы) передаются по ссылке, а не по значению. Например, передача словаря в функцию, не копирует словарь, а если функция изменяет словарь, то изменение будет видно там, откуда её вызвали. В файле, который использует этот пакет. — в Go не используются заголовочные файлы. Вместо этого, каждый файл с исходным кодом — часть определенного пакета. Когда пакет определяет объект (тип, константу, переменную, функцию) с именем, начинающимся с буквы в верхнем регистре, этот объект виден для всех других файлов, которые используют этот пакет. — В Go не используется неявное преобразование типов. Операции, которые сочетают разные типы, требуют приведения (называемого преобразованием в Go). — В Go нет перегрузки функций и нет определяемых пользователем операций. — Go не поддерживает спецификаторы const или violatile. — В Go используется nil для неправильных указателей, в то время, как в C++ в тех же случаях используются NULL или просто 0. СинтаксисСравнивая с C++, синтаксис объявления переменных перевернут. Вы пишете имя, а за ним — тип. В отличие от C++, синтаксис для типа не совпадает с тем, как переменная используется. Объявления типов можно легко читать слева направо: Go C++ var v1 int // int v1; var v2 string // const std::string v2; (примерно) var v3 [10]int // int v3[10]; var v4 []int // int* v4; (примерно) var v5 struct { f int } // struct { int f; } v5; var v6 *int // int* v6; (но нет арифметики для указателей) var v7 map[string]int // unordered_map Объявления принимают форму ключевого слова, за которым следует имя объявляемого объекта. Ключевыми словами являются var, func, const или type. Объявление методов является небольшим исключением — получатель идет до имени объявляемого объекта; это будет продемонстрировано ниже. Также, вы можете за ключевым словом разместить несколько объявлений в скобках: var ( i int m float ) При объявлении функции, вы должны либо дать имя для каждого параметра, либо не давать имени всем параметрам. Вы не можете пропустить несколько имен для одних переменных, дав имена другим. Можно группировать несколько имен одним типом: func f(i, j, k int, s, t string) Переменная может быть инициализирована при объявлении. При этом допускается не указывать её типа. Когда тип не указан, типом переменной становится тип присваиваемого выражения. var v = *p Если переменная не инициализирована сразу, должен быть указан её тип. В таком случае, она будет неявно инициализирована нулевым значением данного типа (0, nil и т.д.). В Go нет неинициализированных переменных. Вне функции, короткий синтаксис присвоения переменным значения это := v1 := v2 Что равносильно var v1 = v2 Go допускает множественные присвоения, которые выполняются параллельно: i, j = j, i // Поменять местами значения i и j. Функции могут возвращать несколько значений, обозначенных в виде списка в круглых скобках. Возвращаемые значения можно с помощью присвоения к списку переменных: func f() (i int, j int) { ... } v1, v2 = f() На практике, в коде на Go содержится очень мало точек с запятой. Технически, все выражения в Go разделены точкой с запятой. Тем не менее, Go трактует конец не пустой линии, как точку с запятой. В результате чего в ряде случаев нельзя использовать перенос строки. Например, вы не можете написать func g() { // НЕВЕРНО } Точка с запятой будет поставлена после g(), и это приведет к тому, что данный код будет являться объявлением функции, а не её определением. Аналогично вы не можете написать if x { } else { // НЕВЕРНО } Точка с запятой будет поставлена после } и перед else, что вызовет синтаксическую ошибку. Так как точка с запятой обозначает конец выражения, вы можете продолжать использовать их так же, как и в C++. Тем не менее, это не рекомендуется. Идиоматический Go опускает ненужные точки с запятой, а на практике это все, кроме случае с циклом for и случаев, когда вы хотите разместить на одной строке несколько коротких выражений. К слову говоря, вместо того, чтобы беспокоиться о расположении точек с запятой и скобок, форматируйте ваш код с помощью программы gofmt. Она дает единый стандартный стиль Go и позволяет вам волноваться за свой код, а не его форматирования. При использовании указателя на структуру, применяется . вместо ->. С точки зрения синтаксиса, структура и указатель на структуру используются одинаково. type myStruct struct { i int } var v9 myStruct // v9 является структурой var p9 *myStruct // p9 указатель на структуру f(v9.i, p9.i) В Go не требуются круглые скобки вокруг условия в выражениях if, условий для выражения for или значения выражения switch. С другой стороны, требуется заключать в фигурные скобки тело выражений if и for. if a < b { f() } // Корректно if (a < b) { f() } // Корректно if (a < b) f() // НЕКОРРЕКТНО for i = 0; i < 10; i++ {} // Корректно for (i = 0; i < 10; i++) {} // НЕКОРРЕКТНО В Go нет ни выражения while, ни выражения do/while. Выражение for может быть использовано с одним условием, что делает его аналогичным выражению while. Если же условия опущены, будет создан бесконечный цикл. Go допускает использование break и continue для обозначения меток. Метка должна ссылаться на выражения for, switch или select. В выражении switch, метки case не являются проходными. Вы можете сделать их проходными с помощью ключевого слова fallthrough. Это применяется даже в смежных случаях. switch i { case 0: // пустое тело case case 1: f() // f не вызовется, когда i == 0! } Но case может иметь несколько значений. switch i { case 0, 1: f() // f будет вызвана если i == 0 || i == 1. } Значения в case не обязательно должны быть константами, или даже целыми числами. Любые виды типов, которые поддерживает оператор сравнения, — такие, как строки или указатели, — могут быть использованы. И если значение switch опущено, по умолчанию становится true. switch { case i < 0: f1() case i == 0: f2() case i > 0: f3() } Операторы ++ и -- могут быть использованы только в утверждениях, а не в выражениях. Вы не можете написать c = *p++. *p++ воспринимается, как (*p)++. Утверждение defer может быть использовано после того, как функция, содержащая выражение defer, возвратит результат. fd := open("filename") defer close(fd) // fd будет закрыта после завершения функции КонстантыВ Go константы могут не иметь типа. Это применимо даже для констант, объявленных с помощью const, если в объявлении не указано типа, а инициализирующее выражение использует только константы без типа. Значение из константы без типа становится типизированным при использовании вне контекста, который требует типизированное значение. Это позволяет пользоваться константами относительно свободно и не требует неявного преобразования типов. var a uint f(a + 1) // Численная константа без типа "1" становится типа uint Язык не налагает ограничений по размеру численных констант без типа или константных выражений. Ограничение применяется только там, где при использовании константы требуется тип. const huge = 1 << 100 f(huge >> 98) Go не поддерживает enum. Вместо этого, вы можете использовать специальное имя iota в одном объявлении const, чтобы получить набор увеличивающегося значения. Когда в const опущено инициализирующее выражение, повторно используется предыдущее выражение. const ( red = iota // red == 0 blue // blue == 1 green // green == 2 ) СрезыКонцептуально, срез это структура с тремя полями: указатель на массив, длина и объем. Срезы поддерживают оператор [] для доступа к элементам соответствующего массива. Встроенная функция len возвращает длину среза. Встроенная функция cap возвращает его объем. Дан массив или другой срез. Новый срез создается с помощью выражения a[I:J]. Будет создан новый срез, ссылающийся на a, начинающийся с индекса I и заканчивающийся перед индексом J. Он будет иметь длину J - I. Новый срез ссылается на тот же массив, на который ссылается a. Так, изменения, сделанные с помощью нового слоя, можно увидеть, используя a. Объем нового среза просто объем a минус I. Объемом массива является его длина. Также вы можете присвоить указатель на массив переменной типа среза. Дано: var s []int; var a[10] int, присвоение s = &a эквивалентно s = a[0:len(a)]. Что значит, что в Go срезы используются для некоторых случаев, где в C++ используются указатели. Если вы создадите значение типа [100]byte (массив из 100 байт, — возможно, буфер) и захотите передать его в функцию без копирования, вы должны объявить параметр функции типа []byte и передать адрес массива. В отличие от C++, нет необходимости передавать длину буфера, он просто доступен через len. Синтаксис среза может быть также использован со строками. Вернется новая строка, чье значение будет подстрокой исходной. Так как строки неизменны, строковые срезы могут быть реализованы без выделения новой памяти для содержимого среза. Создание значенийВ Go есть встроенная функция new, которая принимает тип и выделяет пространство в куче. Выделяемое пространство будет инициализировано нулем данного типа. Например, new(int) выделит новый int в куче, инициализирует его значением 0 и вернет его адрес, который имеет тип *int. В отличии от C++, new это функция, а не оператор, поэтому new int приведет к синтаксической ошибке. Значения словарей и каналов должны выделяться с помощью встроенной функции make. Переменная с типом map или channel без инициализации будет автоматически инициализировано nil. Вызов make(map[int]int) вернет новое выделенное значение типа map[int]int. Отметим, что make возвращает значение, а не указатель. Это согласуется с тем фактом, что значения map и channel передаются по ссылке. Вызов make с типом map принимает необязательный аргумент, обозначающий объем словаря. Вызов make с типом channel принимает необязательный аргумент, который устанавливает объем буфера канала (по умолчанию равен 0). Функция make может также использоваться для выделения среза. В таком случае, будет выделена память под соответствующий массив и возвращен срез, ссылающийся на него. Требуется один аргумент, количество элементов среза. Второй, необязательный, это объем среза. Например, make([]int, 10, 20) аналогично new([20]int)[0:10]. Так как в Go реализована сборка мусора, новый выделенный массив будет уничтожен после того, как не будет ссылок на возвращаемый срез. ИнтерфейсыТам где в C++ используются классы, подклассы и шаблоны, в Go задействованы интерфейсы. Интерфейс Go похож на чистый абстрактный класс C++: класс без членов, с методами которые все чисто виртуальны. Тем не менее, в Go каждый тип, предоставляющий методы, обозначенные в интерфейсе, может трактоваться как реализация интерфейса. Не требуется явного объявления иерархии. Реализация интерфейса полностью отделена от самого интерфейса. Метод выглядит как обычное определение функции, за исключением того, что она имеет получателя. Получатель (receiver) похож на указатель this в методе класса C++. type myType struct { i int } func (p *myType) get() int { return p.i } Данный код объявляет метод get, ассоциированный с myType. В теле функции получатель назван p. Методы определяются именованными типами. Если вы преобразуете значение к другому типу, у нового значения будут методы нового типа, а не старого. Вы можете определять новый именованный тип, полученный из встроенного типа. Новый тип будет отдельным. type myInteger int func (p myInteger) get() int { return int(p) } // Требуется преобразование. func f(i int) { } var v myInteger // f(v) некорректно. // f(int(v)) корректо; у int(v) нет определенных методов. Дан интерфейс type myInterface interface { get() int set(i int) } Мы можем сделать так, чтобы myType соответствовал интерфейсу, добавив func (p *myType) set(i int) { p.i = i } Теперь любая функция, принимающая myInterface в качестве параметра будет принимать переменную типа *myType. func getAndSet(x myInterface) {} func f1() { var p myType getAndSet(&p) } Другими словами, если рассматривать myInterface как пустой абстрактный базовый класс C++, определения set и get для *myType автоматически сделают наследование *myType от myInterface. Тип может соответствовать нескольким интерфейсам. Анонимное поле может быть использовано для реализации чего-то, вроде класса-наследника в C++ type myChildType struct { myType; j int } func (p *myChildType) get() int { p.j++; return p.myType.get() } Что эффективно реализует myChildType как наследника myType. func f2() { var p myChildType getAndSet(&p) } Метод set наследован от myChildType, так как методы, связанные с анонимным полем, становятся методами исходного типа. В данном случае, так как в myChildType есть анонимное поле типа myType, методы myType также становятся методами myChildType. В этом примере, метод get был перегружен, а метод set — наследован. Но это не совсем то же, что и класс-наследник в C++. Когда вызывается метод анонимного поля, его получатель является полем, а не окружающей структуры. Другими словами, методы в анонимных полях не являются виртуальными функциями. Когда вам нужен эквивалент виртуальной функции, используйте интерфейс. Переменная с типом interface может быть преобразована в другой тип интерфейса с помощью специальной конструкции, называемой утверждением типа. Это реализуется динамически во время исполнения, как и dynamic_cast в C++. В отличие от dynamic_cast, нет необходимости объявлять связи между двумя интерфейсами. type myPrintInterface interface { print() } func f3(x myInterface) { x.(myPrintInterface).print() // утверждение типа к myPrintInterface } Преобразование к myPrintInterface осуществляется полностью динамически. Это будет работать так долго, как соответствующий тип x (динамический тип) будет определять метод print. Так как преобразование динамическое, его можно использовать для реализации обобщенного программирования, схожего с шаблонами в C++. Это делается с помощью манипуляций над значениями минимального интерфейса. type Any interface { } type iterator interface { get() Any set(v Any) increment() equal(arg *iterator) bool } Go-процедурыGo дает возможность создать новый поток выполнения программы (go-процедуру) c помощью выражения go. Выражение go запускает функцию в другой, заново созданной, go-процедуре. Все go-процедуры в одной программе используют одно и то же адресное пространство. Изнутри, go-процедуры действуют как подпрограммы, которые размножены по разным потокам в операционной системе. Вам не следует беспокоиться об этих деталях. func server(i int) { for { print(i) sys.sleep(10) } } go server(1) go server(2) (Обратите внимание, что выражение for в функции server эквивалентно циклу while (true) в C++) Go-процедуры не потребляют много ресурсов (так предполагается). Функции первого класса (которые Go реализует как замыкания) могут быть полезны при использовании выражения go. var g int go func(i int) { s := 0 for j := 0; j < i; j++ { s += j } g = s }(1000) КаналыКаналы используются для связи между go-процедурами. Любое значение может быть передано через канал. Каналы эффективны и потребляют мало ресурсов. Чтобы передать значение в канал, используйте <- в качестве бинарного оператора. Чтобы получить сообщение из канала, используйте <- в качестве унарного оператора. При вызове функций, каналы передаются по ссылке. Библиотека Go предоставляет мьютексы, но вы также можете использовать одну go-процедуру с открытым каналом. Вот пример использования управляющей функции для контроля доступа к единственной переменной. type cmd struct { get bool; val int } func manager(ch chan cmd) { var val int = 0 for { c := <- ch if c.get { c.val = val; ch <- c } else { val = c.val } } } В этом примере один канал использован и на вход, и на выход. Это некорректно, если несколько go-процедур сообщаются с управляющей функцией одновременно: go-процедура, ждущая ответа от управляющей функции, может вместо него получить запрос от другой go-процедуры. Решением будет передать канал в качестве аргумента. type cmd2 struct { get bool; val int; ch <- chan int } func manager2(ch chan cmd2) { var val int = 0 for { c := <- ch if c.get { c.ch <- val } else { val = c.val } } } Для использования manager2, дается канал: func f4(ch <- chan cmd2) int { myCh := make(chan int) c := cmd2{ true, 0, myCh } // Composite literal syntax. ch <- c return <-myCh } |
Статьи с такими же тегами: — Написание web-приложений на языке Go — Введение в O3D от Google — Разработка на C/C++ в Eclipse IDE — Опасный код на C — Objective-C для программистов C++ |