Статьи

Сравнение Голанга с Java

Прежде всего я хотел бы сделать отказ от ответственности. Я не эксперт в го. Я начал изучать его несколько недель назад, поэтому высказывания здесь являются своего рода первыми впечатлениями. Я могу ошибаться в некоторых субъективных областях этой статьи. Возможно, я напишу некоторое время обзор этого позже. Но до тех пор, вот он, и если вы программист на Java, вы можете увидеть мои чувства и переживания, и в то же время вы можете прокомментировать и исправить меня, если я ошибаюсь в некоторых утверждениях.

Голанг впечатляет

В отличие от Java, Go компилируется в машинный код и выполняется напрямую. Очень похоже на C. Поскольку это не машина с виртуальной машиной, она очень сильно отличается от Java. Он является объектно-ориентированным и в то же время функциональным, поэтому это не только новый C с некоторой автоматизированной сборкой мусора. Это где-то между C и C ++, если мы думаем, что мир языков программирования находится на одной строке, а это не так. С точки зрения программиста на Java некоторые вещи настолько сильно отличаются, что их изучение является сложной задачей и может дать более глубокое понимание структур языка программирования и того, как объекты, классы и все эти вещи… даже в Java.

Я имею в виду, что если вы понимаете, как OO реализован в Go, вы также можете понять некоторые причины, по которым Java их отличает.

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

GC а не GC

Управление памятью является ключевым моментом в языках программирования. Сборка позволяет вам делать все. Вернее, это требует от вас всего этого. В случае C в стандартной библиотеке есть некоторые вспомогательные функции, но вам все еще нужно освободить всю память, выделенную перед вызовом malloc . Автоматизированное управление памятью начинается где-то в C ++, Python, Swift и Java. Голанг также в этой категории.

Python и Swift используют подсчет ссылок. Когда есть ссылка на объект, сам объект содержит счетчик, который подсчитывает количество ссылок, которые указывают на него. Нет указателей или ссылок в обратном направлении, но когда новая ссылка получает значение и начинает ссылаться на объект, счетчик увеличивается, а когда ссылка становится равной нулю / нулю, или ссылается на другой объект, счетчик выключается. Так что известно, когда счетчик равен нулю, нет ссылок на объект, и он может быть отброшен. Проблема с этим подходом состоит в том, что объект все еще может быть недоступен, пока счетчик положителен. Могут быть круги объектов, ссылающиеся друг на друга, и когда последний объект в этом круге освобождается от статических, локальных и иным образом достижимых ссылок, тогда круг начинает плавать в памяти, как пузырьки в воде: все счетчики положительны, но объекты недостижим. Учебник Swift содержит очень хорошее объяснение этого поведения и того, как его избежать. Но суть все еще здесь: вам нужно немного позаботиться об управлении памятью.

В случае Java, других языков JVM (включая реализацию JVM Python) память управляется JVM. Существует полномасштабная сборка мусора, которая периодически запускается в одном или нескольких потоках, параллельно с рабочими потоками или иногда останавливая их (иначе говоря, остановите мир), отмечая недоступные объекты, сметая их и сжимая предположительно рассеянную память. Все, что вам нужно беспокоиться, это производительность, если вообще.

Голанг также находится в этой категории с небольшим маленьким исключением. У него нет ссылок. У него есть указатели. Разница имеет решающее значение. Он может быть интегрирован с внешним кодом C, и по соображениям производительности во время выполнения нет ничего лучше эталонного реестра. Фактические указатели не известны системе исполнения. Выделенная память все еще может быть проанализирована для сбора информации о достижимости, и неиспользованные «объекты» все еще могут быть отмечены и удалены, но память не может быть перемещена, чтобы выполнить сжатие. Это не было очевидно для меня какое-то время из документации, и когда я понял, как обрабатывать указатели, я искал магию, которую волшебники Голанга реализовали для компактирования. Мне было жаль учиться, их просто нет. Там нет магии.

Golang имеет сборку мусора, но это не полный сборщик мусора, как в Java, где нет сжатия памяти. Это не обязательно плохо. Он может очень долго работать с серверами, не фрагментируя память. Некоторые из сборщиков мусора JVM также пропускают этапы сжатия, чтобы уменьшить паузу при сборке мусора при очистке старых поколений и выполнять сжатие только в качестве крайней меры. Этот последний шаг в Go пропущен, и он может вызвать некоторые проблемы в редких случаях. Вы вряд ли столкнетесь с проблемой при изучении языка.

Локальные переменные

Локальные переменные (а иногда и объекты в более новых версиях) хранятся в стеке на языке Java. Так же и в C, C ++ и в других языках, где реализован стек вызовов как таковой. Голанг, связанный с локальными переменными, не является исключением, кроме…

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

Так что это абсолютно законно написать :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package main
 
import (
    "fmt"
)
 
type Record struct {
    i int
}
 
func returnLocalVariableAddress() *Record {
    return ℜcord{1}
}
 
func main() {
    r := returnLocalVariableAddress()
    fmt.Printf("%d", r.i)
}

Затворы

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package main
 
import (
    "fmt"
)
 
func CounterFactory(j int) func() int {
    i := j
    return func() int {
        i++
        return i
    }
}
 
func main() {
    r := CounterFactory(13)
    fmt.Printf("%d\n", r())
    fmt.Printf("%d\n", r())
    fmt.Printf("%d\n", r())
}

Функция возвращает значения

Функции могут возвращать не только одно значение, но и несколько значений. Это кажется плохой практикой, если не используется должным образом. Python делает это. Perl делает это. Это может быть полезно. Он в основном используется для возврата значения и nil или кода ошибки. Таким образом, старая привычка кодирования ошибки в возвращаемый тип (обычно возвращающая -1 в качестве кода ошибки и некоторое неотрицательное значение в случае наличия значимого возвращаемого значения, как в вызовах библиотеки C std) заменяется чем-то гораздо более читабельным.

Множественные значения по сторонам присваиваются не только функциям. Чтобы поменять местами два значения, вы можете написать:

1
a,b = b,a

Ориентация на объект

С замыканиями и функциями, являющимися гражданами первого класса, Go по крайней мере объектно-ориентирован как JavaScript. Но это на самом деле больше, чем это. Go Lang имеет интерфейсы и структуры. Но они не совсем классы. Это типы значений . Они передаются по значению, и где бы они ни хранились в памяти, данные — это только чистые данные и никакой заголовок объекта или что-то в этом роде. struct s в Go очень похожи на C. Они могут содержать поля, но не могут расширять друг друга и не могут содержать методы. Ориентация объекта приближается немного по-другому.

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

Например :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
 
import (
    "fmt"
)
 
type A struct {
    a int
}
 
func (a *A) Printa() {
    fmt.Printf("%d\n", a.a)
}
 
type B struct {
    A
    n string
}
 
func main() {
    b := B{}
    b.Printa()
    b.A.a = 5
    fmt.Printf("%d\n", b.a)
}

Это почти или своего рода наследство.

Когда вы указываете структуру, для которой метод может быть вызван, вы можете указать саму структуру или указатель на нее. Если метод применяется к структуре, то метод получит доступ к копии структуры вызывающей стороны (эта структура передается по значению). Если метод применяется к указателю на структуру, то указатель будет передан (передан по ссылочному типу). В последнем случае метод также может модифицировать структуру (в этом смысле структуры не являются типами значений, поскольку типы значений являются неизменяемыми). Любой из них может быть использован для выполнения требования интерфейса. В приведенном выше примере к Printa применяется указатель на структуру A Го говорит, что A является получателем метода.

Синтаксис Go также немного снисходительно относится к структурам и указателям на него. В C вы можете иметь структуру, и вы можете написать ba для доступа к полю структуры. В случае указателя на структуру в C вы должны написать b->a для доступа к тому же полю. В случае указателя ba это синтаксическая ошибка. Го говорит, что писать b->a бессмысленно (вы можете интерпретировать это буквально). Зачем засорять код операторами -> если оператор точки может быть перегружен. Доступ к полю в случае структуры и доступ к полю через указатели. Очень логично

Поскольку указатель так же хорош, как и сама структура (в некоторой степени), вы можете написать :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
 
import (
    "fmt"
)
 
type A struct {
    a int
}
 
func (a *A) Printa() {
    if a == nil {
        fmt.Println("a is nil")
    } else {
        fmt.Printf("%d\n", a.a)
    }
}
 
func main() {
    var a *A = nil
    a.Printa()
}

Да, именно в этом и состоит настоящий Java-программист, которого не стоит волновать. Мы вызвали метод для нулевого указателя! Как это может случиться?

Введите переменную, а не объект

Вот почему я использовал кавычки, пишущие «объект». Когда Go хранит структуру, это часть памяти. У него нет заголовка объекта (хотя это может быть, поскольку это вопрос реализации, а не определения языка, но это не так). Это переменная, которая содержит тип значения. Если тип переменной является структурой, то это известно уже во время компиляции. Если это интерфейс, то переменная будет указывать на значение, и в то же время она будет также ссылаться на фактический тип, для которого она имеет значение.

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

Реализация интерфейсов

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

Зачем нам нужно ключевое слово «Implements» в Java, а не в Go? Go не нуждается в этом, потому что он полностью скомпилирован и нет ничего лучше, чем загрузчик классов, который загружает отдельно скомпилированный код во время выполнения. Если структура должна реализовывать интерфейс, но это не так, то это будет обнаружено во время компиляции без явной классификации того, что структура реализует интерфейс. Вы можете преодолеть это и вызвать ошибку во время выполнения, если вы используете отражение (которое имеет Go), но объявление ‘Implements’ в любом случае не поможет.

Go компактен

Код Go компактен и не прощает. На других языках есть символы, которые просто бесполезны. Мы привыкли к ним в течение последних 40 лет с момента изобретения C, и все другие языки следовали синтаксису, но это не обязательно означает, что это лучший способ следовать. В конце концов, после C мы все знаем, что проблему «трейлинг-ин» лучше всего решать с помощью { и } вокруг ветвей кода в операторе «if». (Возможно, Perl был первым основным C-подобным синтаксическим языком, который запросил это.) Однако, если нам нужны скобки, нет смысла заключать условие в скобки. Как вы могли видеть в коде выше:

1
2
3
4
5
6
7
...
    if a == nil {
        fmt.Println("a is nil")
    } else {
        fmt.Printf("%d\n", a.a)
    }
...

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

Вы можете использовать ‘: =’, чтобы объявить новую переменную и присвоить ей значение. С правой стороны выражение обычно определяет тип, поэтому нет необходимости писать ‘ var x typeOfX = expression ‘. С другой стороны, если вы импортируете пакет, присвойте ему переменную, которая впоследствии не будет использоваться: это ошибка. Так как это может быть обнаружено во время компиляции, это ошибка кода, компиляция не удалась. Очень умно. (Иногда раздражает, когда я импортирую пакет, который я собираюсь использовать, и перед обращением к нему я сохраняю код, а IntelliJ разумно удаляет импорт, просто чтобы помочь мне.)

Потоки и очереди

Потоки и очереди встроены в язык. Их называют горутинами и каналами. Для запуска программы вам нужно написать go functioncall() и функция будет запущена в другом потоке. Хотя в стандартной библиотеке Go есть методы / функции для блокировки «объектов», нативное многопоточное программирование использует каналы. Канал — это встроенный тип в Go, который является каналом FIFO фиксированного размера любого другого типа. Вы можете вставить значение в канал, и программа может выполнить его. Если канал заполнен нажатием на блоки, а канал пуст, блокировка блокируется.

Есть ошибки, без исключений. Паника!

В Go есть обработка исключений, но она не должна использоваться как в Java. Исключение называется «паника», и это действительно нужно использовать, когда в коде присутствует настоящая паника. В языке Java это похоже на некоторый метод throwable, который заканчивается на «… Error». Когда есть какой-то исключительный случай, некоторая ошибка, которая может быть обработана, это состояние возвращается системным вызовом, и ожидается, что функции приложения будут следовать аналогичной схеме. Например

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package main
 
import (
    "log"
    "os"
)
 
func main() {
    f, err := os.Open("filename.ext")
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
}

функция «Открыть» возвращает обработчик файла и nil или nil и код ошибки. Если вы выполните его на Go Playground (нажмите на ссылку выше), вы получите сообщение об ошибке.

Это не совсем соответствует практике, к которой мы привыкли при программировании на Java. Легко пропустить некоторое состояние ошибки и написать

01
02
03
04
05
06
07
08
09
10
package main
 
import (
    "os"
)
 
func main() {
    f := os.Open("filename.ext")
    defer f.Close()
}

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

Нет, наконец, отложите

С обработкой исключений тесно связана функция, которую Java реализует с помощью функции try / catch / finally. В Java вы можете иметь код, который выполняется в конечном коде, несмотря ни на что. Go предоставляет ключевое слово ‘defer’, которое позволяет вам указать вызов функции, который будет вызван до возврата метода, даже если была / была паника. Это решение проблемы, которое дает вам меньше возможностей злоупотреблять. Вы не можете написать произвольный код, который будет выполняться, отложив только вызов функции. В Java вы даже можете иметь оператор return в блоке finally или увидеть беспорядок, пытающийся справиться с ситуацией, когда код, выполняемый в блоке finally, может также вызвать исключение. Go склонен к этому. Мне нравится, что.

Другие вещи…

это также может показаться странным на первый взгляд, как

  • публичные функции и переменные пишутся с заглавной буквы, нет таких ключевых слов, как ‘public’, ‘private’
  • исходный код библиотек должен быть импортирован в исходный код проекта (я не уверен, что правильно понял)
  • отсутствие дженериков
  • поддержка генерации кода, встроенная в язык в виде директив комментариев (это действительно wtf)

В общем, Go — интересный язык. Это не замена Java даже на уровне языка. Они не должны выполнять одни и те же задачи. Java — это язык разработки предприятий, Go — язык системного программирования. Go, так же как и Java, постоянно развивается, поэтому мы можем увидеть некоторые изменения в этом в будущем.

Ссылка: Сравнение Golang с Java от нашего партнера по JCG Питера Верхаса в блоге Java Deep .