Go компилируемый язык со статической типизацией. Он имеет C-подобный синтаксис и сборку мусора. Что это означает?
Компиляция
Компиляция это процесс перевода исходного кода, который написали вы, в язык более низкого уровня – либо ассемблер (как в случае с Go), либо в какой-то другой промежуточный язык (как в случае с Java или C#).
С компилируемыми языками бывает неприятно работать потому, что компиляция может быть медленной. Сложно совершать быстрые итерации, если вы тратите минуты или часы в ожидании компиляции вашего кода. Скорость компиляции является одной из основных целей Go. Это хорошие новости для людей, которые работают над большими проектами, а так же для тех, кто привык быстро получать обратную связь при работе с интерпретируемыми языками.
Компилируемые языки, как правило, работают быстрее, и их исполняемые файлы могут быть выполнены без установки дополнительных зависимостей (по крайней мере это верно для таких языков, как C, C++ и Go, которые компилируются непосредственно в ассемблер).
Статическая типизация
Статическая типизация означает, что все переменные должны быть определенного типа (int, string, bool, []byte, и т. д.). Это достигается либо путем указания типа при объявлении переменной, либо, во многих случаях, тип определяет компилятор (мы скоро увидим примеры).
Можно много говорить о статической типизации, но я считаю, что её лучше понять глядя на код. Если вы использовали языки с динамической типизацией, вам она может показаться громоздкой. Вы не ошибаетесь, но есть и преимущества, особенно когда она используется в сочетании с компиляцией. Эти две особенности нередко объединяются. Когда есть одна, обычно присутствует и другая, но это не строгое правило. Со строгой системой типов, компилятор может обнаруживать проблемы, которые выходят за рамки синтаксических ошибок, а также проводить дополнительную оптимизацию.
C-подобный синтаксис
Говоря о том, что язык имеет C-подобный синтаксис, имеется ввиду то, что если вы уже использовали любой другой C-подобный язык как C, C++, Java, JavaScript и C#, то Go вам покажется похожим – по крайней мере на первый взгляд. Например, это значит, что &&
используется как логическое И, ==
применяется для сравнения, {
и }
обозначает начало и конец области видимости, а индексы массивов начинаются с 0.
C-подобный синтаксис также имеет тенденцию к постановке точки с запятой в окончании строк и использовании скобок вокруг условий. В Go нет ни того, ни другого, хотя скобки все еще используются для разделения приоритета. Например, оператор if
выглядит так:
if name == "Leto" {
print("the spice must flow")
}
Но в более сложных случаях скобки все еще полезны:
if (name == "Goku" && power > 9000) || (name == "gohan" && power < 4000) {
print("super Saiyan")
}
Помимо этого, Go гораздо ближе к C, чем к C# или Java — не только с точки зрения синтаксиса, но и с точки зрения назначения. Это отражается в лаконичности и простоте языка, которая, надеюсь, будет очевидной, как только вы начнете изучать его.
Сборка мусора
Некоторые переменные при создании имеют легко определяемую жизнь. Локальная переменная функции, например, исчезает при выходе из функции. В других случаях это не так очевидно, по крайней мере для компилятора. Например, жизнь переменной, которая была возвращена функцией или на которую ссылаются другие переменные и объекты, бывает сложно определить. Без сборки мусора задачей разработчика являлась очистка памяти от переменных там, где по их мнению они не нужны. Как? В C, вы выполняли free(str);
для переменной.
Языки со сборщиками мусора (такие, как Ruby, Python, Java, JavaScript, C#, Go) способны отслеживать и освобождать переменные. которые больше не используются. Сборка мусора добавляет свои накладные расходы, но также устраняет ряд разрушительных ошибок.
Запуск Go кода
Давайте начнем наше путешествие созданием простой программы и научимся её компилировать и выполнять. Откройте ваш любимый текстовый редактор и наберите следующий код:
package main
func main() {
println("it's over 9000!")
}
Сохраните файл с именем main.go
. Сейчас, вы можете сохранить его где угодно; не обязательно использовать рабочее пространство Go для тривиальных примеров.
Затем откройте оболочку/командную строку и перейдите в папку, в которую вы сохранили файл. Мне для этого нужно было набрать cd ~/code
.
Наконец, запустите программу, введя:
go run main.go
Если все работает, вы должны увидеть it’s over 9000!.
Но подождите, что насчет этапа с компиляцией? go run
это удобная команда, которая компилирует и запускает ваш код. Она использует временную директорию для сборки программы, выполняет её и затем очищает. Вы можете увидеть расположение временной папки выполнив:
go run --work main.go
Чтобы явно скомпилировать код, используйте go build
:
go build main.go
Эта команда создаст исполняемый файл main
который вы сможете запустить. В Linux / OSX не забудьте, что перед именем выполняемого файла нужно набрать точку и слэш: ./main
.
Во время разработки, вы можете использовать go run
или go build
. Однако в случае развертывания кода, вам нужно переносить бинарный файл, полученный с помощью go build
и выполнять его.
Main
Надеюсь код, который мы только что выполнили, был понятен. Мы создали функцию и напечатали строку с помощью встроенной функции Println
. Команда go run
знала что выполнять потому, что у нее не было выбора? Нет, в Go точкой входа в программу является функция с именем main
в пакете main
.
Мы поговорим о пакетах в позже. Сейчас, когда вы сфокусированы на понимании основ Go, мы всегда будем писать наш код в пакете main
.
Если хотите, вы можете отредактировать код и изменить имя пакета. Запустите код с помощью команды go run
и вы получите ошибку. Затем измените имя обратно на main
, но используйте другое имя функции. Вы должны увидеть другое сообщение об ошибке. Попробуйте сделать тоже самое используя команду go build
. Обратите внимание на то, что код компилируется, но в нем нет точки входа чтобы запустить его. Это совершенно нормально когда вы, к примеру, создаете библиотеку.
Импорты
Go имеет ряд встроенных функций, таких как Println
, которые могут быть использованы без упоминания. Вы не сможете зайти далеко без использования стандартной библиотеки Go и библиотек других разработчиков. В Go ключевое слово import
используется для объявления пакета, который будет использован кодом в файле.
Давайте изменим нашу программу:
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) != 2 {
os.Exit(1)
}
fmt.Println("It's over ", os.Args[1])
}
И запустим её с помощью команды:
go run main.go 9000
Здесь мы используем два стандартных пакета Go: fmt
и os
. Мы также можем увидеть другую встроенную функцию len
. len
возвращает длину строки, или число значений в словаре, или, как мы видим здесь, число элементов в массиве. Если вам интересно, почему мы ожидаем два аргумента, это потому, что первый аргумент имеет индекс 0 – это всегда путь к текущему исполняемому файлу. (Изменить программу чтобы вывести его и убедитесь сами.)
Вы наверное заметили префикс перед именем функции, совпадающий с именем пакета, например fmt.Println
. Это отличается от многих других языков. Мы узнаем больше о пакетах в следующих главах. Сейчас, знание того, как импортировать и использовать пакеты, уже хороший старт.
Go строго относится к импорту пакетов. Программа не будет скомпилирована, если вы импортируете пакет и не используете его. Попробуйте выполнить:
package main
import (
"fmt"
"os"
)
func main() {
}
Вы должны получить две ошибки о том, что fmt
и os
были импортированы и не использованы. Раздражает ли это? Безусловно. Со временем вы привыкните к этому (хотя это всё равно будет раздражать). Go строг в этом плане потому, что неиспользуемые пакеты замедляют компиляцию; правда у большинства из нас не возникает проблем с этим.
Еще одна вещь, которую хотелось бы отметить, это то, что стандартная библиотека Go хорошо документирована. Вы можете взглянуть на http://golang.org/pkg/fmt/#Println чтобы узнать больше о функции Println
, которую мы использовали. Можно кликнуть на заголовок и увидеть её исходный код. Также, промотайте вверх страницы, чтобы узнать больше о возможностях форматирования Go.
Если у вас нет доступа к Интернету, можно посмотреть документацию локально, выполнив команду:
godoc -http=:6060
И ввести в браузере адрес http://localhost:6060
Переменные и определения
Было бы неплохо начать и закончить обзор переменных просто сказав: вы можете объявить переменную и задать её значение с помощью x = 4. К сожалению в Go все сложнее. Начнем наш разговор глядя на простые примеры. Затем, в следующей главе, познакомимся с ними более подробно рассматривая создание и использование структур. Тем не менее, полное понимание, возможно, займет некоторое время.
Вы можете подумать Ого! Что тут может быть сложного? Давайте посмотрим на несколько примеров.
Наиболее явный способ использования переменных в Go также наиболее подробный:
package main
import (
"fmt"
)
func main() {
var power int
power = 9000
fmt.Printf("It's over %d\n", power)
}
Здесь мы определяем переменную power
типа int
. По умолчанию, Go присваивает переменным нулевые значения. Для целых чисел это 0
, для булевых false
, для строк ""
и так далее. Затем, мы задаем значение 9000
для переменной power
. Две строки кода можно объединить в одну:
var power int = 9000
Все еще много печатать. В Go есть короткий оператор объявления переменных :=
, с которым можно объявить тип так:
power := 9000
Это удобно и работает и точно также с функциями:
func main() {
power := getPower()
}
func getPower() int {
return 9001
}
Важно помнить, что :=
используется для объявления переменной, а так же задания ей значения. Почему? Потому, что переменную нельзя объявить дважды (по крайней мере в той же области видимости). Если вы попытаетесь выполнить следующее, вы получите ошибку.
func main() {
power := 9000
fmt.Printf("It's over %d\n", power)
// COMPILER ERROR:
// no new variables on left side of :=
power := 9001
fmt.Printf("It's also over %d\n", power)
}
Компиляция завершится с сообщением нет новых переменных слева от :=. Это значит, что когда переменная объявляется в первый раз, используется :=
, но при последующих присваиваниях используется оператор =
. В этом есть большой смысл, но может быть трудным для запоминания когда переключаться между этими операторами.
Если вы читали сообщение об ошибке внимательно, вы могли заметить, что слово переменные во множественном числе. Это потому, что Go позволяет присваивать несколько значений переменным (использванием =
или :=
):
func main() {
name, power := "Goku", 9000
fmt.Printf("%s's power is over %d\n", name, power)
}
До тех пор пока переменная является новой, можно использовать :=
. Рассмотрим пример:
func main() {
power := 1000
fmt.Printf("default power is %d\n", power)
name, power := "Goku", 9000
fmt.Printf("%s's power is over %d\n", name, power)
}
Хотя power
используется дважды с помощью :=
, компилятор не будет жаловаться когда мы будем использовать эту переменную второй раз, он видит что другая переменная name
новая и разрешает использовать :=
. Однако, вы не можете изменить тип переменой power
. Она была объявлена (косвенным образом) как целое число и может принимать только целочисленные значения.
Теперь, последнее что нужно знать, это то, что Go, как и в случае с импортами, не позволяет иметь в программе неиспользуемые переменные. Например,
func main() {
name, power := "Goku", 1000
fmt.Printf("default power is %d\n", power)
}
не будет скомпилировано потому, что name
была объявлена, но не используется. Как и неиспользуемые импорты это будет причинять одни расстройства, но в целом, я думаю, это улучшает чистоту кода и его читаемость.
Еще многое предстоит узнать об объявлениях и присваиваниях. А пока запомните, что когда используется var ИМЯ ТИП
переменная объявляется с нулевым значением, ИМЯ := ЗНАЧЕНИЕ
значение присваивается одновременно с объявлением переменной, а ИМЯ = ЗНАЧЕНИЕ
когда присваивается значение уже объявленной переменной.
Объявление функций
Настало время рассказать о том, что функции могут возвращать несколько значений. Возьмем три функции: одна не возвращает значение, другая возвращает одно значение, и третья возвращает два значения.
func log(message string) {
}
func add(a int, b int) int {
}
func power(name string) (int, bool) {
}
Последнюю можно использовать так:
value, exists := power("goku")
if exists == false {
// handle this error case
}
Иногда нужно только одно из возвращаемых значений. В этом случае другое значение присваивают переменной _
:
_, exists := power("goku")
if exists == false {
// handle this error case
}
Это больше, чем просто договорённость. _
– пустой идентификатор, его особенность в том, что возвращаемое значение в действительности не присваивается. Это позволяет вам использовать _
снова и снова не зависимо от возвращаемого типа переменной.
И наконец еще кое-что, с чем вы наверняка столкнетесь при объявлении функций. Если параметры имеют одинаковый тип, можно использовать короткий синтаксис:
func add(a, b int) int {
}
Вы будете часто использовать возможность возвращения нескольких значений. И так же часто использовать идентификатор _
для их игнорирования. Множественные возвращаемые значения и чуть более короткий способ указания параметров не являются обязательными правилами. Тем не менее, рано или поздно вы столкнетесь с ними, поэтому важно знать о них.
Перед тем, как продолжить
Мы рассмотрели несколько небольших отдельных кусочков, кажущихся разрозненными на данном этапе. Мы будем постепенно создавать примеры побольше, в надежде, что куски сойдутся вместе.
Если вы раньше работали с динамическим языком, сложности, связанные с типами и объявлениями, могут показаться шагом назад. Я соглашусь с вами. Для некоторых систем динамические языки являются более продуктивными.
Если вы работали со статически типизированным языком, вы, вероятно, почувствуете себя комфортно с Go. Определяемые типы и множественные возвращаемые значения выглядят изящно (хотя, конечно, они есть не только в Go). Надеюсь, когда мы узнаем больше, вы оцените чистый и лаконичный синтаксис.