Я не мог устоять перед соблазном выбрать Go и посмотреть, чем он отличается от PHP и Rust.

После этого комментария в моей последней статье я слегка осознал, проверяя свои варианты: я не рассматривал язык Go во всем этом.

Оба языка, Rust и Go, появились почти одновременно, но их направленность… разная. В то время как Rust предлагает «C ++, сделанный правильно», предоставляет ручное управление почти везде и производительность, близкую к металлической, Go говорит, что для некоторых задач вам не нужно заходить ТАК глубоко, пока вы ' re нормально разделяет небольшую задержку для включенного в него сборщика мусора.

Лорис Кро упрощает сравнение производительности в красивой запоминающейся фразе:

Go работает быстро, но Rust быстрее

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

Легче сравнивать Go с PHP, чем делать это напрямую с Rust, поскольку я пришел из мира PHP, и вы, вероятно, тоже. Go кажется чем-то средним между двумя мирами, но я все же расскажу о некоторых особенностях Rust.

Примечание: иногда я буду называть Го Голангом. Не волнуйтесь, они такие же. «Голанг» происходит от того факта, что официальный сайт - «www.golang.com».

1. Go статичен и (в основном) безопасен

PHP - это язык с динамической типизацией, все может быть всем и так далее. Несколько лет назад PHP начал поддерживать типизированные аргументы, возвраты и только что типизированные свойства, которые интерпретатор использует для некоторого повышения производительности и безопасности кода. С другой стороны, Rust является явным, и очень немногое оставлено на волю случая.

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

Написать функцию просто: вы устанавливаете параметры с их типом, возвращаемым значением (или значениями), и все готово.

func sumSomething(number int) int {
    return number + 10
}
func numberFruit(fruit Fruit) (int, fruit) {
    return 10, fruit
}

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

Это означает, что здесь нет манипуляций с типами, как в PHP, но есть своего рода полиморфизм. Мы до этого дойдем, не волнуйтесь. А пока учтите, что «Go безопасен, но Rust безопаснее».

func sum(number int) int {
    return number + 10
}
func main() {
    // This won't fly on Go as it's considered a string
    fmt.Println(sum("10"))
}

Слово return в Rust может быть неявным в конце функции или использоваться в ранних возвратах. Между тем, и PHP, и Go используют обязательное return, если вы хотите что-то вернуть, что также неплохо для ранних возвратов.

func sum(sum int) int {
    return sum + 10
}

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

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

2. Документация тоже отличная

Помните, что в Rust есть собственная онлайн-площадка? Что ж, У Go тоже есть онлайн-площадка, так что вы можете протестировать свой код, чтобы узнать, что хорошо, а что нет.

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

Как и в случае с введением Rust, я полностью рекомендую Go Tour для новичков.

3. Установить Go очень просто.

PHP получает массу критики за процедуру установки. Rust проще, чем PHP, но для Windows вам понадобится дополнительное программное обеспечение. Голанг… просто запустите установщик, и все готово.

Да, скриншоты сделаны из браузера на базе Windows, но вы все равно можете установить и использовать Go на своем компьютере MacOS и Linux.

Это было все, что вам нужно для игры в го. Нет, серьезно, вот и все!

4. Переменные - это… переменные!

Хотя в Rust переменные по умолчанию неизменяемы, вы будете рады узнать, что это не применяется в Go. Подобно PHP, переменные предназначены для изменения, но вам все равно нужно объявить их с указанием их типа.

В основном вы будете использовать := для определения типа при объявлении значения, но вы также можете сделать это отдельно. Очень редко оставляют переменную (особенно структуру) неинициализированной.

func doSomething() {
    var favorite_number int
    favorite_number = 10
    other_number := 33
}

Как видите, приведенное выше позволяет иметь переменные со значениями по умолчанию или равными nil (подробнее об этом позже). В PHP вы можете сделать то же самое, используя $empty = null;, что очень редко.

Следует отметить, что вы не можете повторно объявить переменную. Как только он будет установлен, это будет до тех пор, пока функция не завершится.

var number int = 10  // <-- Here it's declared.
var number string = "hello" // <-- This won't compilation error.
{
    var number int = 20  // <-- This works in this scope.
}

Что, если вам нужна константа внутри функции? Ты можешь это сделать.

5. Строгие константы и отсутствие статики

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

func main() {
    // This works:
    const A_STRING string = "Hello world"
    // This doesn't even compile
    const AN_ARRAY_OF_STRINGS = [2]string{"Hello", "world"}
}

Это отличается от того, что в Rust известно как «неизменяемая переменная». В Rust все переменные по умолчанию неизменяемы, поэтому вы должны использовать len mut my_variable чаще.

6. Чувак, а где мои статики?

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

var global_variable = "Hello World"
const GLOBAL_STRING = "This is constant"
func main() {
    // ... very important stuff
    fmt.Println(global_variable, " and ", GLOBAL_STRING)
}

Глобальные значения обычно определяются наверху пакета, могут быть доступны где угодно и при бесконтрольном использовании рассматриваются как яд. Если вы планируете изменить переменную, совместно используемую в вашем приложении, вы можете использовать Каналы или Взаимное исключение (Mutex), чтобы избежать повреждения.

7. Все передается по значению.

Хотя в Rust концепция передачи по значению отличается «перемещением» вместо «копирования» значения в вызов функции, вы обнаружите, что Go очень похож на PHP: все передается по значению .

func sum(number int) int {
    number += 10
    return number
}
func main() {
    number := 10
    fmt.Println(sum(number)) // This will output "20"
    fmt.Println(number) // But this, will output "10"
}

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

Если вы планируете передать структуру или любое другое значение большого размера, но вам не нужно изменять исходное значение, лучше передать указатель в милях вместо того, чтобы позволить Go скопировать все данные. Представьте, что большая строка JSON копируется снова и снова, просто чтобы проверить, содержит ли она значение. Безумие!

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

func sum(number *int) int {
    *number += 10 // Get the value under the pointer and update it.
    return *number // Return the value under the pointer.
}
func main() {
    number := 10
    fmt.Println(sum(&number)) // This will output "20".
    fmt.Println(number) // This outputs "20" because we updated it.
    fmt.Println(&number) // Prints the memory address like "0x...".
}

Передача указателя означает передачу копии указателя (адреса памяти) функции вместо реального значения. Мы можем использовать *number, чтобы получить фактическое значение под этим указателем.

Не бойтесь использовать указатели: если вам нужно изменить исходное значение или передаваемые данные слишком велики, вы должны получить указатель. Вы не столкнетесь с 10% -ным падением производительности, как это происходит в PHP при передаче по указателю, потому что интерпретатор сильно настроен для копирования при записи.

Одно из ключевых различий между передачей по ссылке и передачей по указателю заключается в том, что указатели в Go могут быть указывающими на nil или «нулевыми», как вы, возможно, знаете из PHP, если вы говорите, что указатель соответствует с интерфейсом, но был инициализирован без значения. Погодите, мы обсудим это сразу после интерфейсов.

8. Сборщик мусора готов… верно?

В то время как в Rust нет сборщика мусора, вместо этого выбрана более простая техника программирования «владения», в PHP и Rust есть, но они различаются по своей реализации.

Сборщик мусора в Go, называемый GOGC, отмечает память, используемую горутиной, и переменные, которые должны быть очищены. Как только объем памяти кучи увеличивается вдвое, она очищает те, которые не используются, одновременно, чтобы избежать слишком частой остановки приложения - также известной как «Остановить мир».

Чтобы сделать это во время выполнения других горутин, есть несколько уловок, но реализация на удивление очень чистая.

Хотя в большинстве случаев вам не нужно думать об этом, Go удобно предоставляет ручку для обмена использования памяти на использование ЦП: слишком поспешно, и ваше приложение ухудшит свою задержку, вызывая его слишком много раз. ; слишком слабый, и он будет съедать много памяти, пока не освободит ее полностью.

GOGC - это то, что вам придется настроить, если вы столкнетесь с очень редким случаем проблемы с производительностью, особенно в приложениях с ограничением времени ожидания, где миллисекунды могут иметь значение: обработка мультимедиа, потоковая передача данных, взаимодействие с человеком и т. Д.

Если вы хотите глубоко погрузиться в управление памятью и сборку мусора, есть две статьи: одна в Ardan Labs, а вторая в Блоге Deepu.

9. Не думайте о стеке или куче

В PHP почти все отправляется в кучу. В Rust вы узнаете, когда что-то находится в стеке или куче, просто взглянув на код.

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

Это может иметь недостатки в некоторых случаях, например, если вы хотите использовать стек вручную или вручную настраивать память, чтобы компилятор не мог во всем разобраться. Для этого есть Rust.

10. Никаких потоков, никакой асинхронности, просто Go.

В PHP параллелизма не существует. В Rust есть разделение между потоками и асинхронными функциями. Go объединяет потоки и асинхронные функции в так называемые «горутины».

Горутины - это не сигнатуры функций, а то, как они вызываются в области видимости. Просто вызовите функцию с префиксом go, и все, ваша новая горутина. Нет необходимости отправлять обратные вызовы в поток, async-await или неизвестно что еще. И если вы хотите дождаться, пока все горутины закончат свою работу, просто используйте WaitGroup.

Есть закулисная магия, к которой у вас нет доступа. Короче говоря, Go реализует собственный «планировщик задач» вместо того, чтобы полагаться только на планировщик ОС. Каждая задача, называемая горутиной, обрабатывается этим планировщиком, который бесплатно входит в ваше приложение.

Горутины - это абстракция для обработки параллелизма через функции. Это уводит вас от более глубоких манипуляций, которые вы могли бы иметь в Rust: думайте не о потоках и асинхронных функциях, а о том, что можно делать асинхронно. Планировщик Go будет работать с потоками и асинхронными функциями за вас.

Что касается потоков, то ядро ​​ЦП (логическое) может обрабатывать 1 поток или 1 000 000 потоков. Для этого требуется переключение контекстов между каждым из них.

Я честно рекомендую эту статью о том, как Go обрабатывает горутины, если вы хотите получить более полное представление о том, как и почему. В любом случае у вас все еще есть некоторый контроль над потоками, но забудьте о низкоуровневом доступе, который вы могли бы иметь с C ++ или Rust.

11. По-прежнему не те массивы, которые вам нужны.

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

$mylist = ["foo", new Bar, 3]; // This is valid.

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

var list := [2]string{"Hello", "world!"}

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

Теперь, что касается списков ключ-значение, как в PHP, Go предлагает карты, если его члены имеют один и тот же тип.

var thisMap = map[string]string{
   "greeting": "Hello",
   "subject": "world!",
}

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

func main() {
    var list := []interface{}{1, 2, "apple", true}
    fmt.Println(arr)
    // however, now you need to use type assertion access elements
    i := arr[0].(int)
    fmt.Printf("i: %d, i type: %T\n", i, i)
    s := arr[2].(string)
    fmt.Printf("b: %s, i type: %T\n", s, s)
}

Поскольку массивы в Golang немного более мягкие, чем Rust, мы переходим к настоящему делу: структурам.

12. Не ваш язык ООП.

PHP имеет давнюю традицию ООП, и это наиболее предпочтительный способ обработки кода при написании на нем. Go, как и Rust, разделяет данные и поведение.

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

type Fruit struct {
    family: string,
    name: string,
}
func (fruit Fruit) setFamily(family string) {
    fruit.family = family
}

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

13. Новые типы, кортежи и перечисления

PHP нигде не поддерживает кортежи или перечисления, но если вы новичок в Go, вам нужно будет только беспокоиться о Newtypes. По сути, они представляют собой псевдонимы для определенного типа, которые не требуют затрат и делают ваш код более идиоматическим.

type UserName string
func whatIsYourName(un UserName) {
    fmt.Println(un)
}

Но самое большое отсутствие - это Enum. Как и в случае с PHP, Go не имеет собственных Enum, которые являются «перечисляемыми» типами, которые могут быть одним из множества возможных.

Вам придется обойти это, используя настраиваемый тип как целое число и назначив ему выражения констант с йотой, что позволяет этим постоянным параметрам иметь внутренний псевдоним как целые числа. Если ничего из этого не имело смысла, посмотрите этот код:

type Color int
 
const (
    Red Color = iota // Make each constant an integer
    Green
    Blue
)
func main() {
    var color Color = Red
    if (color == Red) {
        fmt.Println("The color is: ", color)
    }
}

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

Что касается кортежей, вы в основном будете использовать вместо них структуры или функции, возвращающие более одного значения. Наиболее близким к Tuples в Go является функция, возвращающая более одного значения:

func returnsFooAndBar() (int, int) {
    return 1, 2
}
foo, bar := returnsFooAndBar()

В Rust в этом нет необходимости, перечисления поставляются бесплатно из коробки, и кортежи тоже, если вы хотите их использовать.

14. Не существует «статических методов».

В PHP вы можете присоединять статические методы к классу, то есть вы можете вызывать заданную логику, идентифицированную именем класса. Обычно вы создаете их там, чтобы обрабатывать логику, относящуюся к классу, например конструкторы.

class Equation {
    protected $color = "transparent";
    public static function new()
    {
        return new Equation;
    }
}
$equation = Equation::new();

В Rust вы также можете прикреплять статические методы к структуре. Там тоже нет проблем.

struct CarChassis {
    color: String
}
impl CarChassis {
    pub fn new() -> Self {
        CarChassis {
            color: "transparent"
        }
    }
}

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

type CarChassis struct {
    color: string
}
func NewCarChassis() *CarChassis {
    return CarChassis {
        color: "transparent"
    }
}
--- Later in the code
var car = NewCarChassis()

15. Функции имеют вариативные параметры.

PHP очень снисходительно относится к функциям с параметрами по умолчанию или дополнительными параметрами. Rust заставляет вас соблюдать подпись функции.

Go похож на Rust, но менее строг. У вас может быть полиморфизм с пустыми интерфейсами (подождите), и у вас есть переменные параметры. Это делает вызовы некоторых функций удобными, поскольку они не позволяют обрабатывать полные массивы, что требует меньше кода.

func compare_cars(cars ...Car) bool {
    // ...
}
func main() {
    // ...
    compare_cars(car_1, car_2, car_3)
}

Но, кроме этого, для функции нет параметров по умолчанию или дополнительных параметров.

16. Отсрочка - это волшебство

«Defer» - это волшебное ключевое слово в Go, которое позволяет продвинуть вызов функции до конца текущей области видимости.

func write_something(file File, text string) {
    defer file.Unlock()
    // If the file cannot be locked, the program panics, but the
    // deferred `file.Unlock()` will be called nonetheless.
    file.Lock()
    // And if we cannot append the text, it will also be called.
    file.Append(string)
}

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

В Rust есть неофициальный пакет под названием ScopeGuard, который реализует то же поведение, а в PHP вы можете сделать то же самое, используя try/finally:

public function readFile($file) {  
    try {
        return $this->filesystem->read_contents($file);
    } finally {
        // This will be executed even if no exception is returned.
        $this->cleanup($file);
    }
}

Отложенная функция может использоваться для восстановления горутины от паники, как мы увидим позже.

17. Мои старые друзья «если», «за» и «переключить»

Вы будете чувствовать себя как дома с Go, если вы пришли с PHP, поскольку операторы if, for и switch работают более простым образом.

if работает так, как вы ожидаете, но скобки указывать необязательно. Это позволяет выполнить оператор перед условием, которое существует только для области действия блока условия. Керлинговые скобы являются обязательными, тройных подкладок нет.

if roses == "red" {
    fmt.Println("Roses are red")
} else {
    fmt.Println("Roses are not red")
}
if _, ok := flower.(Rose); ok && roses == "red" {
    fmt.Println("Yes, this is a rose and it's red")
}

switch тоже знаком и проще: он выполняет только первый истинный случай, поэтому нет необходимости добавлять break в конце каждого. Если вы не выставляете значение для сравнения, предполагается, что вы используете true.

switch v {
    case "red":
        fmt.Printf("This is a red rose")
    case "blue":
        fmt.Printf("This is a blue rose")
    default:
        fmt.Printf("This is not a red or blue rose!")
 }

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

switch v := i.(type) {
    case Rose:
        fmt.Printf("This is a Rose")
    case Violet:
        fmt.Printf("This is a Violet")
    default:
        fmt.Printf("This may be not a flower at all!")
 }

Наконец, for циклы - единственный способ перебрать повторяемые данные. Удаление точек с запятой делает его while циклом, а использование ничего не приводит к бесконечному циклу кода.

for i =: 0; i < 10; i++ {
    fmt.Println("The number is: ", i)
}
for isSystemReady() {
    fmt.Println("The system is not ready... yet")
}
for {
    fmt.Prinln("I can't get out!")
}

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

colors := []string{"Red", "Green", "Blue"}
for index, color := range colors {
    fmt.Println("The color is: ", color)
}

Но for циклы настолько просты, что создают проблему для повторения в следующей точке.

18. Не существует итеративного типа.

Go отлично работает с массивами, срезами, каналами, картами и даже строками. Проблема в том, что вы не можете элегантно создать цикл другого типа в качестве итератора.

Go не реализует тип Iterable, то есть стандартный способ получить следующий элемент из списка до конца. Например, если мы хотим сделать «KeyChain» итерабельным, нам придется реализовать нашу собственную логику итератора, вне и внутри цикла:

В PHP и Rust есть эти Iterable типы, реализующие необходимую логику для обхода циклов, что снижает нагрузку на разработчика. Проблема, когда у вас нет этого типа из самого языка, проста: каждый будет создавать свой собственный вариант итерации для той же цели.

19. Nil - это не тип, а значение.

Концепция nil в Go очень проста: это просто еще одно значение. Единственные типы, которые могут иметь нулевое значение - это указатели, срезы, карты, каналы, функции… и интерфейсы. Погодите за последним, мы очень близки к их объяснению.

var number []int              // number   == nil -> true
var list map[string]string    // list     == nil -> true
var pointer *int              // pointer  == nil -> true
var channel chan int          // channel  == nil -> true
var function func()           // function == nil -> true
type Something struct {
    pointer *int 
}
var something Something       // Something.pointer == nil -> true
var anyType interface{}       // anyType           == nil -> true
var anything = nil            // compile error: use of untyped nil

В остальном вы получите значения по умолчанию:

  • неинициализированный string будет пустым,
  • неинициализированный номер будет 0, а
  • неинициализированный bool будет false.

Зачем вам использовать nil? Что ж, большинство разработчиков будут использовать его, чтобы проверить, вернула ли функция ошибку с if err != nil.

Это только верхушка айсберга. Один из редких случаев или «крайних случаев», когда nil может стать помехой во время выполнения, - это когда он используется для интерфейсов.

20. Интерфейсы похожи на уток

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

type Duck interface {
    Looks() string
    Swims() string
    Quacks() string
}
type Animal struct {}
func (a Animal) Looks() string {
    return "duck"
}
func (a Animal) Swims() string {
    return "duck"
}
func (a Animal) Quacks() string {
    return "duck"
}

По сути, это Утиный тест:

Если он похож на утку, плавает, как утка, и крякает, как утка, то, вероятно, это утка.

Это кажется странным. Если вы посмотрите на код, действительно, присутствуют методы интерфейса, но нет явного объявления для интерфейса «Duck». Go предполагает, что вы это реализуете.

Например, если вы реализуете метод String в структуре, он волшебным образом становится реализацией Stringer, и метод будет использоваться для печати того, что он возвращает.

type Thing struct {}
func (t Thing) String() string {
    return "Hello world!"
}
func main() {
    fmt.Println(new(Thing)) // <-- "Hello world!"
}

Методы интерфейса автоматически объявляются общедоступными, но, как бы странно ни выглядел Go, если вы определите swims(), начиная с нижнего регистра, он все равно будет общедоступным методом.

type Duck interface {
    swims() string
}

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

21. Пустые интерфейсы. Вы правильно прочитали.

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

func receive(anything interface{}) {
    // ... What type is "anything"???
}

Есть много ошибок, связанных с пустыми интерфейсами в Go, которые могут вас немного отпугнуть, как в примере выше. Есть хорошая статья Удая Хиварале об обработке интерфейсов в различных формах, чтобы избежать ошибок во время выполнения, а не при компиляции.

Скажем, пустой интерфейс похож на то, что функция в PHP получает какое-либо значение.

22. Интерфейсы могут иметь нулевое значение, и это плохо.

Проблема в Go заключается в сравнении значений с nil, потому что сравнение строгое, что означает, что оба типа и значения должны совпадать.

Одна из ловушек nil - это когда вы ожидаете значение для типа интерфейса. В приведенном ниже случае value относится к типу SomethingInterface, который является указателем на адрес, но значение этого адреса равно nil.

Итак, если вы скажете value == nil, вы спросите следующее:

«Равно ли значение nil типа SomethingInterface значению nil типа nil

… И явно нет.

type SomethingInterface interface {
   DoAnything() string
}
type Box struct{}
func (box *Box) DoAnything() string {
    return "I did something!"
}
func isThisNil(value interface{}) {
    if value == nil { // <-- This will be false!
        fmt.Println("The box is nil")
    } else {
        fmt.Println("The box is NOT nil") // <-- And this will run!
    }
}
func main() {
   // Here we got the pointer of Box, with no data.
   var box *Box = nil
   // This says the pointer implements an interface, and it's valid
   var pointer_of_box SomethingInterface = box
// Pass the pointer to the function.
   // The function will get this as valid, and execute
   isThisNil(pointer_of_box)
}

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

Итак, опять же, большую часть времени вы будете использовать nil, чтобы проверить, вернула ли функция ошибку. Просто лучше избегать использования неинициализированных значений вместе и всегда полагаться на собственные значения по умолчанию, если вы не стремитесь к очень низкому объему памяти.

Кстати, в Rust нет указателей, указывающих на пустоту, скорее, он заставляет вас иметь дело с отсутствием чего-либо и не будет компилироваться, пока вы не сделаете это тщательно. Это мгновенно делает Rust намного безопаснее, чем Go, что может быть хорошо, если вам нужно на 500% безопасное программное обеспечение и вы готовы пожертвовать большим количеством времени.

23. Государственное или частное, ничего промежуточного.

PHP имеет наследование. В Rust and Go нет. Поскольку нет наследования, нет «Защищенной» видимости; методы в структуре могут быть объявлены как Public, то есть они могут вызываться извне, и как Private, где их могут видеть только члены самого объекта.

Хотя это выглядит достаточно просто, вы должны думать о том, что видимость происходит только на уровне пакета.

type Fruit struct { // <-- This is public
    family: string,
    name: string,
}
func (fruit Fruit) setFamily(family string) { // <-- This is private
    fruit.family = family
}
func (fruit Fruit) WhatFamilyIs() string { // <-- This is public
    return fruit.family
}

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

import ( "fmt", "fruits" )
func main() {
    fruit := Fruit { family: "Apple" }
    fmt.Println(fruit.WhatFamilyIs()) // <-- This will work
    
    fruit.setFamily("Pineapple") // <-- But this won't.
}

В Digital Ocean есть статья, которая дает более подробную информацию о видимости, но пока просто помните это правило строчных-прописных букв.

24. Никаких черт характера

Я знаю, что PHP иногда может стать очень удобным. Например, использование признака для множества похожих, но разных классов становится очень полезным, чтобы не переписывать одно и то же снова и снова.

trait Barks()
{
    public function bark() {
        return property_exists($this, 'bark_sound') ?
               $this->bark_sound : "woof!";
    }
}

Rust предлагает исключительное удобство за счет смешивания признаков и интерфейсов в простые признаки, в значительной степени заставляя наследование над композицией.

trait Barks {
    fn bark(&self) -> String {
        self.bark_sound
    }
}

Go, с другой стороны, вообще не поддерживает функции в стиле PHP. Если вы хотите повторно использовать код, приготовьтесь написать больше кода.

25. Хорошо, ошибка. Паника.

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

Go также идет по пути Rust: ожидайте неудачи и позвольте способу исправить это. Нежелательное поведение «инкапсулируется» тремя способами.

Первый и самый «безобидный» способ сказать, в порядке что-то или нет, - это просто вернуть логическое значение рядом с желаемым значением. Он используется в основном для того, чтобы отметить, что вы все еще можете продолжить, но в чем-то идете на компромисс.

function AreYouOkay(number int) (int, bool) {
    // Sum 3, and return false if the number was below 3
    number += 3
    return number, number > 3
}
func main() {
    number, ok := AreYouOkay(10)
    // If the number is not okay, print this next string line.
    if !ok {
        fmt.Println("Note the number is over 3")
    }
}

Второе - очень распространенное нормальное соглашение: в своей функции вы также можете возвращать ошибку по возвращаемому значению. Это считается нарушением, и разработчик должен действовать соответствующим образом.

import (
 "errors"
)
func ThisMayFail(number int) (int, error) {
    if int == 0 {
        return 0, errors.New("You cannot use zero as number")
    }
    return number + 3, nil
}

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

func main() {
    number, error = ThisMayFail(0);
    if error != nil {
       log.Fatal(error) // This properly terminates the program.
    }
    fmt.Println(number)
}

Поверьте, вы увидите много кода в Go, выполняющего if error != nil. Go пытается быть проще, но это просто лень.

В то время как if error != nil дает возможность изящно восстановиться, иногда у вас не получается. Вот почему вы можете использовать defer.

Третий метод - использование defer для восстановления после паники с recover() для более опасных ситуаций, когда вам нужно справиться с неприятной паникой.

func dontFail() int {
    // Ensure the call to `recover()` is executed regardless.
    defer func() {
        if recovered := recover(); recovered != nil {
            return 0
        }
    }()
    ThisWillPanic(); // <-- This will panic
    return 10;
}

Вышеупомянутое откладывает выполнение функции recover(), которая, в двух словах, фиксирует значение паники, а также позволяет что-то вернуть вызывающему.

26. Пространства имен - это пакеты.

Пространства имен PHP очень мягкие, но существует стандарт под названием PSR-4, который заставляет вас зеркально отображать каталог файла, чтобы использовать его в качестве пространства имен. Это делает автозагрузку классов лучше, чем просто использование require_once во всех ваших проектах.

// app/MyApp/Something.php
<?php
namespace App\MyApp
class Something
{
    // ...
}

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

Пакеты в Go представляют имя каталога, так же как пространства имен работают в PHP. Файлы Go внутри каталога ссылаются на каталог, в котором они находятся, поэтому вы можете создать файл в src/user/name.go со следующим содержимым:

// src/user/name.go
package user
var PublicName = "John Doe"
var privateName = "Maria Doe"

Тогда вы сможете ссылаться на них в вашем main.go файле.

package main
import (
    "fmt"
    "user/name"
)
func main() {
    fmt.Println(name.PublicName)
}

Имена пакетов должны быть очень простыми, например arrutils, hashutils и т. Д., Но лично я считаю, что лучше использовать выразительные имена, а не короткий текст, который делает их неразборчивыми.

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

Теперь представьте, что вы хотите использовать пакет, доступный в Github. Это насколько удобно:

import "github.com/vendor/goodies"
func main() {
    goodie.Name()
}

Мы поговорим об этом в следующем пункте.

27. Децентрализованные зависимости

При использовании пакетов в PHP вы в основном потребуете их в вашем composer.json и загрузите их из Packagist. Каждый серьезный проект PHP заканчивается тысячами файлов в каталоге vendor.

Rust выполняет то же самое через ваш crates.toml и загружает их из crates.io, но эти ящики помещаются в глобальный каталог, а не в каталог проекта.

Go аналогичен тому, что использует файл go.mod для хранения информации о зависимостях и устанавливает эти библиотеки там, где установлен Go. Но есть большая разница: он децентрализован.

Если вы хотите использовать зависимости, как вы это делаете в большинстве случаев, вы инициализируете go.mod с помощью go mod init.

go mod init my-proyect
# Or, if you want to publish it in Github
# go mod init github.com/myname/mypackage

После этого вы импортируете зависимость с go get и ее местонахождением. В основном вы будете делать это с репозиториями Github и другими VCS, но это также могут быть каталоги.

go get github.com/my-vendor/my-package

Вы можете добавить:

  • ветка вроде, my-package@master,
  • релиз типа [email protected],
  • или даже конкретный коммит, например my-package@c5dw84

Например, мы можем использовать пакет GoDotEnv для чтения файла .env.

package main

import (
   env "github.com/joho/godotenv"
)

func main() {
   err := env.Load()
   // ...do something important
}

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

Более подробная информация о модулях содержится в статье в HoneyBadger, если вы хотите более подробное объяснение того, как их использовать в ваших интересах.

Я не могу говорить о том, сколько модулей доступно для Go от третьих лиц, так как я должен учитывать Github, Gitlab, Bitbucket и другие. Трудно сказать, какой модуль Go используется чаще всего, но вы можете проверить Awesome Go, чтобы узнать, какие из них выделяются.

Кстати, Go и gRPC очень хорошо уживаются с официальным пакетом gRPC для Go.

28. Отражение похоже на крошечное зеркало.

Вы могли подумать, что в Go нет какой-либо системы отражения, как в PHP, вместо этого вы выбрали макросы или другие утилиты для генерации кода, как это происходит в Rust. Нет, он есть, но очень простой.

В пакет reflect входят утилиты, которые в основном используются для получения типов и значений из переменных и вызова именованных методов.

var pi float64 = 3.14
fmt.Println("The value is: ", reflect.ValueOf(x).String())

Так что нет, вы не сможете, например, проверить имя функции, в которой находитесь, как это можно сделать динамически в PHP.

В Golang есть целая запись в блоге о« Законах отражения », но примите во внимание две вещи из нее: вы будете в основном использовать ее при работе с пустыми интерфейсами, которые, как вы ожидаете, будут иметь много разных типов, и есть потеря производительности. для приложений, которые сильно зависят от задержки.

29. Тестирование намного лучше, но не совсем

У PHP нет официальной библиотеки тестирования, и PHPUnit стал де-факто средством тестирования кода. Rust находится в том же состоянии, но с некоторыми макросами утверждения. В Go вас порадует наличие официального пакета тестирования и некоторых дополнительных инструментов.

Просто нажмите go test, и он автоматически выполнит любую функцию, которая начинается с Test{Name}(*testing.T), внутри файла _test.go, который вам нужно поместить в каталог вашего пакета.

func returnTen() int {
    return 10
}
func TestReturnsTen(*testing.T) {
    if returnTen() != 10 {
        t.Errorf("Value returned is not 10.")
    }
}

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

Что касается Mocking, Go также включает официальный пакет mocking.

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

30. Профилирование - это круто… после правильной настройки

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

Профилирование позволит вам проверить, что происходило в вашем приложении, пока оно было профилировано, например, что заставляло ЦП до смерти, какая горутина требовала времени для завершения и что решило укусить слишком много памяти. Хотя может потребоваться время для правильной настройки.

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

31. Голанг любит Интернет, а ты?

Я должен сказать, что Golang чувствует себя более близким к веб-стороне вещей, чем Rust, но все же ощущается как язык общего назначения, чем что-то строго привязанное к стеку HTTP. Неудивительно, что Istio и Traefik используют Go, Hugo также использует Go, и даже Docker и Kubernetes были созданы с использованием Go.

Лучшие веб-фреймворки для Golang - это Gin, Echo, Chi, Fiber и Revel, но, учитывая их подход, я считаю, что они больше подходят для микросервисов, чем для полноценной полнофункциональной работы. фреймворки swiss-army-knife для Интернета, так как в них отсутствуют некоторые функции, которые предлагались бы другими фреймворками, такими как Laravel. Я был бы не против иметь графический интерфейс с Vue, React или Angular поверх полного набора микросервисов, созданных на Go, и я думаю, что это должен быть идеальный подход.

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

32. Только один способ к базе данных в Go

Как всегда, у вас есть 2 альтернативы при подключении к базе данных: вы делаете это самостоятельно или ищете пакет, который уже делает это. Выбрав позднее, вы обнаружите, что в пространстве Golang есть три конкурирующих ORM: GORM, ent.go и SQLX.

GORM и ent.go от SQLX отличает то, что это почти полнофункциональные системы для запросов к базе данных, в то время как SQLX кажется хорошей оболочкой для необработанных операторов. Не повредит, если вы полностью контролируете ситуацию, но в большинстве случаев вы этого не сделаете, и я считаю, что построители запросов - лучший способ построить оператор SQL, не опасаясь инъекций SQL или манипулирования строками, как в 1995 году.

По сравнению с Rust, SQL в Go очень зрелый, в основном благодаря официальному универсальному интерфейсу для SQL-подобных баз данных и широкому списку нескольких драйверов баз данных, что упрощает разработку решений для взаимодействия с механизмами баз данных.

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

Бонус: чертовски сложно читать

Ненавижу это говорить, но Go страдает проблемой, называемой «ленивый набор текста». В Go принято сокращать каждое слово, включая переменные, функции и все, что вы делаете. Я не получаю какой-то налог за печатание?

func (c *Case) SetC(m int, n string) {
    c.Sum(m).Named(n)
}

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

func (n int) int {
    return (int + 10) / 10
    // In a perfect world, you would have done this:
    // int.Sum(10).Divide(10)
}

Вы можете сказать, что служебные данные функции не нужны, но у нас есть компилятор на другой стороне, поэтому задача компилятора - преобразовать вызовы функций в «более производительный код», например (int + 10) / 10.

Сказанное выше верно и для PHP, примитивные типы не имеют методов. С другой стороны, в Rust есть типы с унаследованными методами, которые не только позволяют вашему мозгу лучше понимать, что происходит, но и имеют больше инструментов для их обработки.

Например, примитив usize в Rust имеет множество методов, таких как бесплатный возврат степени заданного числа, позволяющий избежать кучи операндов.

fn power_of_ten() -> usize {
    let mut = 10;
    10.pow(5)
}

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

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

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

Если вы спросите меня, Rust - лидер по безопасности, удобству Go и скорости PHP, так как с ним вы легко сможете быстрее создать прототип приложения. Как только производительность начнет настаивать, я думаю, будет естественным проверить Go и Rust и заплатить цену за миграцию.