Урок 8 «Учись работать быстро» — Как строятся программные здания

Java задняя часть Go C++
Урок 8 «Учись работать быстро» — Как строятся программные здания

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

Из-за наличия структуры переменные языка Go имеют более красочные формы, а многоэтажки программы языка Go собираются слой за слоем через структуру.

Определение типа структуры

Структуры похожи на «классы» в других языках высокого уровня. Затем мы используем синтаксис структуры для определения типа «круг».

type Circle struct {
  x int
  y int
  Radius int
}

Внутри структуры Circle есть три переменные: координаты центра круга и радиус. Особое внимание следует уделить регистру переменных внутри структуры.Первая буква с заглавной буквы предназначена для общедоступных переменных, а первая буква со строчной буквы для внутренних переменных, которые эквивалентны категориям Public и Private переменных-членов класса соответственно. Доступ к внутренним переменным возможен только непосредственно из кода, принадлежащего тому же пакету (просто понимаемому как один и тот же каталог).

Создание структурных переменных

Существует множество форм создания структурной переменной. Давайте рассмотрим наиболее распространенную форму создания структурной переменной.

package main

import "fmt"

type Circle struct {
	x int
	y int
	Radius int
}

func main() {
	var c Circle = Circle {
		x: 100,
		y: 100,
		Radius: 50,  // 注意这里的逗号不能少
	}
	fmt.Printf("%+v\n", c)
}

----------
{x:100 y:100 Radius:50}

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

package main

import "fmt"

type Circle struct {
	x int
	y int
	Radius int
}

func main() {
	var c1 Circle = Circle {
		Radius: 50,
	}
	var c2 Circle = Circle {}
	fmt.Printf("%+v\n", c1)
	fmt.Printf("%+v\n", c2)
}

----------
{x:0 y:0 Radius:50}
{x:0 y:0 Radius:0}

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

package main

import "fmt"

type Circle struct {
	x int
	y int
	Radius int
}

func main() {
	var c Circle = Circle {100, 100, 50}
	fmt.Printf("%+v\n", c)
}

-------
{x:100 y:100 Radius:50}

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

package main

import "fmt"

type Circle struct {
	x int
	y int
	Radius int
}

func main() {
	var c *Circle = &Circle {100, 100, 50}
	fmt.Printf("%+v\n", c)
}

-----------
&{x:100 y:100 Radius:50}

Обратите внимание на приведенный выше вывод, форма указателя имеет адресный символ &, указывающий, что напечатанный объект является типом указателя. После введения формы указателя структурных переменных ниже можно представить третью форму создания структурных переменных.Глобальная функция new() используется для создания структуры с нулевым значением, и все поля инициализируются соответствующим типом.нулевое значение .

package main

import "fmt"

type Circle struct {
	x int
	y int
	Radius int
}

func main() {
	var c *Circle = new(Circle)
	fmt.Printf("%+v\n", c)
}

----------
&{x:0 y:0 Radius:0}

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

package main

import "fmt"

type Circle struct {
	x int
	y int
	Radius int
}

func main() {
	var c Circle
	fmt.Printf("%+v\n", c)
}

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

var c1 Circle = Circle{}
var c2 Circle
var c3 *Circle = new(Circle)

Структуры с нулевым значением и нулевые структуры

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

var c *Circle = nil

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

Объем памяти структуры

Небезопасный пакет языка Go предоставляет функцию Sizeof() для получения памяти, занимаемой структурой.

package main

import "fmt"
import "unsafe"

type Circle struct {
	x int
	y int
	Radius int
}

func main() {
	var c Circle = Circle {Radius: 50}
	fmt.Println(unsafe.Sizeof(c))
}

-------
24

Структура Circle на моей 64-битной машине занимает 24 байта, потому что каждое целое — 8 байт. На 32-битной машине структура Circle будет занимать всего 12 байт.

копия структуры

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

package main

import "fmt"

type Circle struct {
	x int
	y int
	Radius int
}

func main() {
	var c1 Circle = Circle {Radius: 50}
	var c2 Circle = c1
	fmt.Printf("%+v\n", c1)
	fmt.Printf("%+v\n", c2)
	c1.Radius = 100
	fmt.Printf("%+v\n", c1)
	fmt.Printf("%+v\n", c2)

	var c3 *Circle = &Circle {Radius: 50}
	var c4 *Circle = c3
	fmt.Printf("%+v\n", c3)
	fmt.Printf("%+v\n", c4)
	c3.Radius = 100
	fmt.Printf("%+v\n", c3)
	fmt.Printf("%+v\n", c4)
}

---------------
{x:0 y:0 Radius:50}
{x:0 y:0 Radius:50}
{x:0 y:0 Radius:100}
{x:0 y:0 Radius:50}
&{x:0 y:0 Radius:50}
&{x:0 y:0 Radius:50}
&{x:0 y:0 Radius:100}
&{x:0 y:0 Radius:100}

Попробуйте интерпретировать приведенный выше вывод

вездесущая структура

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

Структура заголовка слайса следующая, на 64-битной машине он будет занимать 24 байта.

type slice struct {
  array unsafe.Pointer  // 底层数组的地址
  len int // 长度
  cap int // 容量
}

Строковая форма заголовка строки, занимающая 16 байт на 64-битной машине.

type string struct {
  array unsafe.Pointer // 底层数组的地址
  len int
}

Структурная форма заголовка словаря

type hmap struct {
  count int
  ...
  buckets unsafe.Pointer  // hash桶地址
  ...
}

Массивы и срезы в структурах

В главе про массивы и слайсы мы самостоятельно проанализировали разницу между массивами и слайсами в виде памяти. У массивов есть только «тело», а у срезов помимо «тела» есть «голова». Заголовок и тело среза разделены и связаны с помощью указателей. Читатели, пожалуйста, попробуйте объяснить вывод следующего кода

package main

import "fmt"
import "unsafe"

type ArrayStruct struct {
	value [10]int
}

type SliceStruct struct {
	value []int
}

func main() {
	var as = ArrayStruct{[...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
	var ss = SliceStruct{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
	fmt.Println(unsafe.Sizeof(as), unsafe.Sizeof(ss))
}

-------------
80 24

Обратите внимание, что при инициализации массива в коде используется [...] синтаксический сахар, позволяющий компилятору автоматически определять длину массива.

Передача параметров структуры

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

package main

import "fmt"

type Circle struct {
	x int
	y int
	Radius int
}

func expandByValue(c Circle) {
	c.Radius *= 2
}

func expandByPointer(c *Circle) {
	c.Radius *= 2
}

func main() {
	var c = Circle {Radius: 50}
	expandByValue(c)
	fmt.Println(c)
	expandByPointer(&c)
	fmt.Println(c)
}

---------
{0 0 50}
{0 0 100}

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

метод структуры

Язык Go не является объектно-ориентированным языком, в нем нет понятия класса, а структура является заменой класса. К классам может быть присоединено множество методов-членов, как и к структурам.

package main

import "fmt"
import "math"

type Circle struct {
 x int
 y int
 Radius int
}

// 面积
func (c Circle) Area() float64 {
 return math.Pi * float64(c.Radius) * float64(c.Radius)
}

// 周长
func (c Circle) Circumference() float64 {
 return 2 * math.Pi * float64(c.Radius)
}

func main() {
 var c = Circle {Radius: 50}
 fmt.Println(c.Area(), c.Circumference())
 // 指针变量调用方法形式上是一样的
 var pc = &c
 fmt.Println(pc.Area(), pc.Circumference())
}

-----------
7853.981633974483 314.1592653589793
7853.981633974483 314.1592653589793

Язык Go не любит неявное преобразование типов, поэтому необходимо преобразовать целочисленное отображение в тип с плавающей запятой, что не очень красиво, но это основное правило языка Go.Явный код может быть не лаконичен достаточно, но это легко понять. В структурном методе языка Go нет ключевых слов, таких как self и this, для ссылки на текущий объект Это определяемое пользователем имя переменной, которое обычно представляется одной буквой. Имя метода языка Go также чувствительно к регистру. Его правила разрешений такие же, как и для полей. Первая буква в верхнем регистре — это общедоступный метод, а первая буква в нижнем регистре — внутренний метод. Доступ может получить только код, принадлежащий к тому же пакету. внутренний метод. Доступ к внутренним полям и методам типа значения структуры и типа указателя формально одинаков. Это отличается от языка C++, где для доступа к значению используется оператор точки ., а для доступа к указателю требуется оператор стрелка ->.

методы указателя структуры

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

func (c Circle) expand() {
  c.Radius *= 2
}

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

func (c *Circle) expand() {
  c.Radius *= 2
}

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

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

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

Встроенная структура

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

package main

import "fmt"

type Point struct {
	x int
	y int
}

func (p Point) show() {
  fmt.Println(p.x, p.y)
}

type Circle struct {
	loc Point
	Radius int
}

func main() {
	var c = Circle {
		loc: Point {
			x: 100,
			y: 100,
		},
		Radius: 50,
	}
	fmt.Printf("%+v\n", c)
	fmt.Printf("%+v\n", c.loc)
	fmt.Printf("%d %d\n", c.loc.x, c.loc.y)
	c.loc.show()
}

----------------
{loc:{x:100 y:100} Radius:50}
{x:100 y:100}
100 100
100 100

анонимная встроенная структура

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

package main

import "fmt"

type Point struct {
	x int
	y int
}

func (p Point) show() {
	fmt.Println(p.x, p.y)
}

type Circle struct {
	Point // 匿名内嵌结构体
	Radius int
}

func main() {
	var c = Circle {
		Point: Point {
			x: 100,
			y: 100,
		},
		Radius: 50,
	}
	fmt.Printf("%+v\n", c)
	fmt.Printf("%+v\n", c.Point)
	fmt.Printf("%d %d\n", c.x, c.y) // 继承了字段
	fmt.Printf("%d %d\n", c.Point.x, c.Point.y)
	c.show() // 继承了方法
	c.Point.show()
}

-------
{Point:{x:100 y:100} Radius:50}
{x:100 y:100}
100 100
100 100
100 100
100 100

Наследование здесь — это просто формальный синтаксический сахар, c.show() эквивалентен c.Point.show() после преобразования в двоичный код, а c.x и c.Point.x также эквивалентны.

Структуры Go не имеют полиморфизма

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

class Fruit {
  public void eat() {
    System.out.println("eat fruit");
  }
  
  public void enjoy() {
    System.out.println("smell first");
    eat();
    System.out.println("clean finally");
  }
}

class Apple extends Fruit {
  public void eat() {
    System.out.println("eat apple");
  }
}

class Banana extends Fruit {
  public void eat() {
    System.out.println("eat banana");
  }
}

public class Main {
  public static void main(String[] args) {
    Apple apple = new Apple();
    Banana banana = new Banana();
    apple.enjoy();
    banana.enjoy();
  }
}

----------------
smell first
eat apple
clean finally
smell first
eat banana
clean finally

Метод Enjoy, определенный родительским классом Fruit, вызывает метод eat, реализованный дочерним классом.Метод дочернего класса может переопределить метод, определенный родительским классом, а метод eat родительского класса скрыт.

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

package main

import "fmt"

type Fruit struct {}

func (f Fruit) eat() {
	fmt.Println("eat fruit")
}

func (f Fruit) enjoy() {
	fmt.Println("smell first")
	f.eat()
	fmt.Println("clean finally")
}

type Apple struct {
	Fruit
}

func (a Apple) eat() {
	fmt.Println("eat apple")
}

type Banana struct {
	Fruit
}

func (b Banana) eat() {
	fmt.Println("eat banana")
}

func main() {
	var apple = Apple {}
	var banana = Banana {}
	apple.enjoy()
	banana.enjoy()
}

----------
smell first
eat fruit
clean finally
smell first
eat fruit
clean finally

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

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

Обратите внимание на паблик-аккаунт "Code Cave" и читайте больше глав "Learn Go Language Quickly"