10 особенностей Go, которые отличают его от других языков.
Что касается языков программирования, Golang довольно молод. Впервые он был выпущен 10 ноября 2009 года. Его создатели Роберт Гриземер
Роб Пайк и Кен Томпсон работали в Google, где проблема массового масштабирования вдохновила их на создание Go как быстрого и эффективного решения для программирования проектов с большими объемом кода, управляемыми несколькими разработчиками, имеющими строгие требования к производительности и охватывающими несколько сетей и ядер обработки.
Основатели Go также воспользовались возможностью при создании своего нового языка, чтобы изучить сильные и слабые стороны и недостатки других языков программирования. В результате получился чистый, понятный и практичный язык с относительно небольшим набором команд и функций.
В этой статье я расскажу о 10 особенностях Go, которые (по моим личным наблюдениям) отличают его от других языков.
1. Go всегда включает двоичный файл в сборки
Среда выполнения Go предоставляет такие услуги, как выделение памяти, сборка мусора, поддержка параллелизма и работа в сети. Он компилируется каждый файл Go в бинарный. Это отличается от многих других языков, многие из которых используют виртуальную машину, которую необходимо установить вместе с программой для правильной работы.
Включение среды выполнения непосредственно в бинарный файл упрощает распространение и запуск программ Go и позволяет избежать проблем несовместимости между средой выполнения и программой. Виртуальные машины таких языков, как Python, Ruby и JavaScript, также не оптимизированы для сборки мусора и выделения памяти, что объясняет более высокую скорость Go по сравнению с другими подобными языками. Например, Go хранит столько, сколько возможно, в стеке, где данные выстраиваются последовательно для гораздо более быстрого доступа, чем к куче. Подробнее об этом позже.
И последнее, что касается статических двоичных файлов Go: поскольку для запуска не требуются внешние зависимости, они запускаются невероятно быстро. Это полезно, если вы используете такую службу, как Google App Engine, платформу как сервис, работающий в Google Cloud, которая может масштабировать ваше приложение до нуля для экономии затрат на облако. При поступлении нового запроса App Engine может мгновенно запустить экземпляр вашей программы Go. Тот же самый опыт в Python или Node обычно приводит к 3–5-секундному ожиданию (или дольше), поскольку требуемая виртуальная среда также запускается вместе с новым экземпляром.
2. Go не имеет централизованной службы для зависимостей программ.
Чтобы получить доступ к опубликованным программам Go, разработчики не полагаются на централизованно размещенную службу, такую как Maven Central для Java или реестр NPM для JavaScript. Скорее, проекты распространяются через их репозитории исходного кода (чаще всего Github). Командная строка позволяет загружать репозитории с помощью go install
.
Почему мне нравится эта функция? Я всегда считал централизованно размещенные службы зависимостей, такие как Maven Central, PIP и NPM, несколько устрашающими черными ящиками, возможно, абстрагирующими от хлопот с загрузкой и установкой зависимостей (и зависимостей зависимостей), но неизбежно вызывающих страшную боль при возникновении ошибки зависимости.
Часто меня расстраивало то, что я никогда полностью не понимал, как они работают под капотом. Отказ от централизованной службы делает процесс установки, управления версиями и управления зависимостями вашего проекта Go очень понятным и, следовательно, более чистым.
Кроме того, сделать ваш модуль доступным для других так же просто, как поместить его в систему контроля версий, что является привлекательно простым способом распространения ваших программ.
3. Go - это ценность по запросу.
В Go, когда вы предоставляете примитив (число, логическое значение или строка) или структуру (грубый эквивалент объекта класса) в качестве параметра функции, Go всегда делает копию значения переменной.
Во многих других языках, таких как Java, Python и JavaScript, примитивы передаются по значению, но объекты (экземпляры классов) передаются по ссылке, что означает, что принимающая функция фактически получает указатель на исходный объект, а не его копию.
Это означает, что любые изменения, внесенные в объект в принимающей функции, отражаются в исходном объекте.
В Go структуры и примитивы по умолчанию передаются по значению с возможностью передачи указателя с помощью оператора звездочки:
// pass by value
func MakeNewFoo(f Foo) (Foo, error) {
f.Field1 = "New val"
f.Field2 = f.Field2 + 1
return f, nil
}
Вышеупомянутая функция получает копию Foo и возвращает новый объект Foo.
// pass by reference
func MutateFoo(f *Foo) error {
f.Field1 = "New val"
f.Field2 = 2
return nil
}
Вышеупомянутая функция получает указатель на Foo и изменяет исходный объект.
Это четкое различие между вызовом по значению и вызовом по ссылке делает ваши намерения очевидными и снижает вероятность того, что вызывающая функция непреднамеренно изменяет переданный объект, хотя этого не должно быть.
Как резюмирует MIT: «Изменчивость затрудняет понимание того, что делает ваша программа, и намного труднее обеспечивать выполнение контрактов».
Более того, вызов по значению значительно сокращает объем работы сборщика мусора, что означает более быстрые и эффективные с точки зрения памяти приложения. В этой статье делается анекдотический вывод, что поиск указателя (получение значений указателя из кучи) в 10-20 раз медленнее, чем получение значения из непрерывного стека. Хорошее практическое правило, которое следует запомнить: самый быстрый способ чтения из памяти - это чтение ее последовательно, а это означает сокращение до минимума количества указателей, случайным образом хранящихся в ОЗУ.
4. Ключевое слово defer.
В NodeJS, прежде чем я начал использовать knex.js, я бы вручную управлял соединениями с базой данных в моем коде, создавая пул БД, а затем открывая новое соединение из пула в каждой функции, освобождая соединение в конце функции после того, как Требуемая функциональность CRUD базы данных была завершена.
Это был своего рода кошмар обслуживания, потому что, если я не освобожу соединение в конце каждой функции, количество невыпущенных соединений с БД будет медленно расти до тех пор, пока в пуле не останется свободных соединений, и после этого приложение сломается.
Реальность такова, что программам часто приходится освобождать, очищать и выполнять разборку ресурсов, файлов, соединений и т.д., поэтому Go ввел ключевое слово defer как эффективный способ управления этим.
Любой оператор, которому предшествует defer, откладывает его вызов до тех пор, пока не завершится окружающая функция. Это означает, что вы можете разместить код очистки / разрыва в верхней части функции (где это очевидно), зная, что это будет так, как только функция завершится.
func main() {
if len(os.Args) < 2 {
log.Fatal("no file specified")
}
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatal(err)
}
defer f.Close()
data := make([]byte, 2048)
for {
count, err := f.Read(data)
os.Stdout.Write(data[:count])
if err != nil {
if err != io.EOF {
log.Fatal(err)
}
break
}
}
}
В приведенном выше примере метод закрытия файла отложен. Мне нравится этот шаблон, когда вы объявляете свое намерение в верхней части функции, а затем забываете об этом, зная, что он выполнит свою работу после выхода из функции.
5. Go использует лучшие возможности функционального программирования.
Функциональное программирование - это эффективная и творческая парадигма, и, к счастью, Golang использует лучшие возможности функционального программирования. В Go:
- функции являются значениями, то есть они могут добавляться в качестве значений в объект, передаваться как параметры в другие функции, устанавливаться в переменные и возвращаться из функций (называемых "функциями высшего порядка'' и часто используются в Go для создания промежуточного программного обеспечения с использованием шаблона декоратора).
- можно создавать и автоматически вызывать анонимные функции.
- функции, объявленные внутри других функций, допускают замыкание (где функции, объявленные внутри функций, могут обращаться к переменным, объявленным во внешней функции, и изменять их). В идиоматическом Go замыкания широко используются для ограничения области действия функции и для установки состояния, которое функции затем используют в своей логике.
func StartTimer (name string) func(){
t := time.Now()
log.Println(name, "started")
return func() {
d := time.Now().Sub(t)
log.Println(name, "took", d)
}
}
func RunTimer() {
stop := StartTimer("My timer")
defer stop()
time.Sleep(1 * time.Second)
}
Выше приведен пример замыкания. Функция StartTimer возвращает новую функцию, которая через замыкание имеет доступ к значению t, установленному во время ее области рождения. Затем эта функция может сравнивать текущее время со значением t, тем самым создавая полезный таймер.
6. В Go есть неявные интерфейсы.
Любой, кто читал литературу о SOLID и шаблонах проектирования, вероятно, слышал мантру «Предпочитайте композицию наследованию». Короче говоря, это предполагает, что вам следует разбить свою бизнес-логику на разные интерфейсы, а не полагаться на иерархическое наследование свойств и логики от родительского класса.
Другим популярным является "Программа для интерфейса, а не реализация": API должен публиковать только контракт о своем ожидаемом поведении (сигнатуры методов), но не сведения о том, как это поведение реализовано.
Оба они указывают на критическую важность интерфейсов в современном программировании.
Поэтому неудивительно, что Go поддерживает интерфейсы. Фактически, оказывается, что интерфейсы - единственный абстрактный тип в Go.
Однако, в отличие от других языков, интерфейсы в Go не реализованы явно, а скорее неявно. Конкретный тип не заявляет, что он реализует интерфейс. Скорее, если набор методов для этого конкретного типа содержит все наборы методов базового интерфейса, Go считает, что объект реализует интерфейс.
Эта неявная реализация интерфейса (формально называемая структурной типизацией) позволяет Go обеспечивать как безопасность типов, так и разделение, сохраняя большую часть гибкости, присущей динамическим языкам.
Явные интерфейсы, напротив, связывают клиента и реализацию вместе, делая замену зависимости, например, в Java намного более сложной задачей, чем в Go.
// this is an interface declaration (called Logic)
type Logic interface {
Process(data string) string
}
type LogicProvider struct {}
// this is a method called 'Process' on the LogicProvider struct
func (lp LogicProvider) Process(data string) string {
// business logic
}
// this is the client struct with the Logic interface as a property
type Client struct {
L Logic
}
func(c Client) Program() {
// get data from somewhere
c.L.Process(data)
}
func main() {
c := Client {
L: LogicProvider{},
}
c.Program()
}
В LogicProvider ничего не объявлено, чтобы указать, что он соответствует интерфейсу Logic. Это означает, что клиент может легко заменить своего поставщика логики в будущем, если этот поставщик логики содержит все наборы методов базового интерфейса (Logic).
7. Обработка ошибок
Ошибки в Go обрабатываются иначе, чем в других языках. Короче говоря, Go обрабатывает ошибки, возвращая значение типа error в качестве последнего возвращаемого значения для функции.
Когда функция выполняется, как ожидалось, для параметра ошибки возвращается nil, в противном случае возвращается значение ошибки. Затем вызывающая функция проверяет возвращаемое значение ошибки и обрабатывает ошибку или выдает собственную ошибку.
// функция возвращает int и ошибку
func calculateRemainder(numerator int, denominator int) (int, error) {
if denominator == 0 {
return 9, errors.New("denominator is 0"
}
return numerator / denominator, nil
}
Есть причины, по которым Go работает таким образом: он заставляет программистов думать об исключениях и обрабатывать их должным образом. Традиционные исключения try-catch также добавляют по крайней мере один новый путь кода через код и делают отступы в коде, которые могут быть трудными для понимания. Go предпочитает рассматривать «счастливый путь» как код без отступов, при этом любые ошибки выявляются и возвращаются до того, как «счастливый путь» будет завершен.
8. Параллелизм
Пожалуй, самая известная функция Go - параллелизм позволяет выполнять обработку параллельно по количеству доступных ядер на машине или сервере. Параллелизм имеет наибольший смысл, когда отдельные процессы не зависят друг от друга (нет необходимости запускаться последовательно) и когда производительность по времени имеет решающее значение. Это часто случается с требованиями ввода-вывода, когда чтение или запись на диск или в сеть происходит на несколько порядков медленнее, чем все, кроме самых сложных процессов в памяти.
go
ключевое слово перед вызовом функции запустит эту функцию параллельно текущему потоку.
func process(val int) int {
// что-то делаем с val
}
// для каждого значения в 'in' запускаем функцию процесса,
// и считываем результат процесса в 'out'
func runConcurrently(in <-chan int, out chan<- int){
go func() {
for val := range in {
result := process(val)
out <- result
}
}
}
Параллелизм в Go - это глубокая и довольно продвинутая функция, но там, где это имеет смысл, она предоставляет мощный способ обеспечить оптимальную производительность вашей программы.
9. Стандартная библиотека Go
Go придерживается философии «включенные батарейки», и многие требования современного языка программирования встроены в стандартную библиотеку, что значительно упрощает жизнь программистам.
Как уже упоминалось, тот факт, что Go является относительно молодым языком, означает, что многие проблемы / требования современных приложений обслуживаются в стандартной библиотеке.
Во-первых, Go предлагает поддержку мирового класса для работы в сети (в частности, HTTP / 2) и управления файлами. Он также предлагает собственное кодирование и декодирование JSON. В результате настройка сервера для обработки HTTP-запросов и возврата ответов (JSON или других) чрезвычайно проста, что объясняет популярность Go для разработки веб-служб HTTP на основе REST.
10. Отладка: Go playground
Отладка на любом языке - критическое требование. Большинство языков полагаются на сторонние онлайн-инструменты или умные IDE, чтобы предоставить инструменты отладки, которые позволяют разработчикам быстро проверять свой код. Go предоставил Go Playground - https://play.golang.org - бесплатный онлайн-инструмент, где вы можете опробовать небольшие программы и поделиться ими. Это очень полезный инструмент, который упрощает отладку.