Раскрытие концепций языка программирования Go

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



Давайте вскочим.

Go имеет обычный синтаксис. Разработчики стремились создать язык с простым и понятным синтаксисом.

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

package main
import "fmt"

func main() {
    fmt.Println("hello world")
}

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

В Go нет файлов заголовков. Код организован в пакеты, и каждый пакет определяется в отдельном файле или нескольких файлах в одном каталоге. Объявление пакета выполняется с использованием ключевого слова package в начале каждого файла.

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

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

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

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

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

Не допускается:

package packageA

import "packageB"

func A() {
    packageB.B()
}
package packageB

import "packageA"

func B() {
    packageA.A()
}

Регистр буквен идентификатора (переменной, константы, функции, типа и т. д.) определяет его видимость или доступность в программе.

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

package mypackage

var MyVariable int         // Exported variable
func MyFunction() { ... }  // Exported function
type MyType struct { ... } // Exported type


var myVariable int         // Unexported variable
func myFunction() { ... }  // Unexported function
type myType struct { ... } // Unexported type

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

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

type Person struct {
    Name  string
    Age   int
    Email string
}
type Employee struct {
    Person
    EmployeeID int
    Salary     float64
}

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

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

Файлы, скомпилированные вместе в одном пакете, в процессе компиляции рассматриваются как связные единицы.

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

package mypackage

// Code specific to file1.go
package mypackage

// Code specific to file2.go

И «file1.go», и «file2.go» принадлежат пакету «mypackage». Они компилируются вместе как часть одного и того же пакета в процессе сборки.

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

package mypackage

import "fmt"

// Package-level variables
var x = 10
var y = 20

// Package-level function
func CalculateSum() {
    sum := x + y
    fmt.Println("Sum:", sum)
}

// Package-level constant
const pi = 3.14159

// Package-level function
func PrintPi() {
    fmt.Println("Pi:", pi)
}

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

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

func main() {
 x := 10
 y := 3.14
 result := x * y
 fmt.Println(result)
}

//invalid operation: x * y (mismatched types int and float64)

Этот выбор дизайна является преднамеренным и направлен на повышение ясности кода, предотвращение непредвиденного поведения и предотвращение распространенных ошибок программирования.

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

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

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

  • Встраивание структур: в Go вы можете встраивать одну структуру в другую для достижения композиции и повторного использования поведения. Это похоже на композицию в объектно-ориентированном программировании. При внедрении структуры поля и методы встроенной структуры становятся частью внешней структуры. Это позволяет повторно использовать код и напрямую обращаться к элементам встроенной структуры.
  • Реализация интерфейса: Go поощряет программирование на основе интерфейса. Вы можете определить интерфейсы, которые задают набор сигнатур методов, и любая структура, реализующая эти методы, автоматически удовлетворяет интерфейсу. Это допускает полиморфизм и взаимозаменяемость различных типов, реализующих один и тот же интерфейс.
package main

import "fmt"

// Animal interface
type Animal interface {
 Sound()
}

// Dog struct
type Dog struct {
 Name string
}

// Sound method for Dog
func (d Dog) Sound() {
 fmt.Println(d.Name + " says Woof!")
}

// Cat struct
type Cat struct {
 Name string
}

// Sound method for Cat
func (c Cat) Sound() {
 fmt.Println(c.Name + " says Meow!")
}

// Pet struct embedding Dog and Cat
type Pet struct {
 Dog
 Cat
}

func main() {
 // Create a new Pet
 pet := Pet{
  Dog: Dog{Name: "Buddy"},
  Cat: Cat{Name: "Whiskers"},
 }

 // Access embedded struct methods
 pet.Dog.Sound() // Outputs: Buddy says Woof!
 pet.Cat.Sound() // Outputs: Whiskers says Meow!

 // Polymorphic behavior using interface
 var animal Animal

 animal = pet.Dog
 animal.Sound() // Outputs: Buddy says Woof!

 animal = pet.Cat
 animal.Sound() // Outputs: Whiskers says Meow!
}

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

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

Разрешение метода основано исключительно на имени метода, а не на типе получателя или типах аргументов. Эта характеристика известна как диспетчеризация метода на основе имени.

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

Операторы постинкремента (++) и постдекремента (--) считаются операторами, а не выражениями. Это означает, что их нельзя использовать как часть более крупного выражения или напрямую присваивать переменной. Кроме того, в Go нет оператора преинкремента или декремента.

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

 w := x = 3 // This is not allowed in Go

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

package main

import "fmt"

func main() {
 var num int
 var flag bool
 var str string
 var arr [3]int
 var person struct {
  Name string
  Age  int
 }
 var ptr *int
 var slice []int
 var mp map[string]int

 fmt.Println(num)         // Outputs: 0
 fmt.Println(flag)        // Outputs: false
 fmt.Println(str == "")   // Outputs: true
 fmt.Println(arr)         // Outputs: [0 0 0]
 fmt.Println(person)      // Outputs: { 0}
 fmt.Println(ptr == nil)  // Outputs: true
 fmt.Println(slice == nil)// Outputs: true
 fmt.Println(mp == nil)   // Outputs: true
}

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

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

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

package main

import "fmt"

type Circle struct {
    radius float64
}

func (c Circle) calculateArea() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    c := Circle{radius: 5.0}
    area := c.calculateArea()
    fmt.Println("Area:", area)
}

В этом примере мы определяем структуру Circle с полем radius. Затем мы определяем метод calculateArea() для типа Circle, объявляя функцию с параметром получателя типа Circle (тип, для которого определен метод). Параметр получателя указывается между ключевым словом func и именем метода.

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

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

Увидимся в следующей главе.

Читать далее













Источники

https://www.youtube.com/watch?v=VM1rYOMoLmY&t=1173s

https://go.dev/doc/

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу