Перейти к содержимому

В чем разница make и new

  • автор:

Golang

Встроенная функция make(T, args) служит цели, отличающейся от new(T) . Она создает только срезы, карты и каналы и возвращает инициализированное (не обнуленное) значение типа T (не *T ). Причиной различия является то, что эти три типа представляют под капотом ссылки на структуры данных, которые должны быть инициализированы перед использованием. Например, срез (slice) представляет собой дескриптор из трех элементов, содержащий указатель на данные (внутри массива), длину и емкость, и пока эти элементы не будут инициализированы, срез будет равен nil . Для срезов, карт и каналов, make инициализирует внутреннюю структуру данных и подготавливает значение для использования. Например:

make([]int, 10, 100)

аллоцирует массив из 100 int’ов, а затем создает структуру среза длиной 10 и вместимостью 100, указывающую на первые 10 элементов массива. (При создании среза емкость можно опустить) Напротив, new([] int) возвращает указатель на вновь выделенный обнуленную структуру среза, то есть указатель на значение фрагмента nil .

Следующие примеры иллюстрируют разницу между new и make .

// аллоцирует структуру среза; *p == nil; используется редко var p *[]int = new([]int) // срез v теперь ссылается на новый массив из 100 int'ов var v []int = make([]int, 100) // Излишне сложно: var p *[]int = new([]int) *p = make([]int, 100, 100) // Идиоматично: v := make([]int, 100)

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

  • Эффективный Go: данные, аллокация с помощью new
  • Эффективный Go: конструкторы и составные литералы
  • Эффективный Go: повторная декларация и переназначение

Введение

Когда я учился Го, я встретил make с участием new Проблема различия. Я проверил множество документов в Интернете, и, как правило, есть 4 различия (см. Ниже), но им не хватает более подробных инструкций и более богатых примеров кода. такие как:

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

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

Talk is cheap. Show me the code.

make

func make(t Type, size . IntegerType) Type

The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make’s return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type:

Метод make () выделяет место в памяти только для типов slice, map и chan и инициализирует объект. Его первый параметр — это тип, а второй параметр — параметр переменной длины, который возвращает сам тип.

Slice: The size specifies the length. The capacity of the slice is
equal to its length. A second integer argument may be provided to
specify a different capacity; it must be no smaller than the
length. For example, make([]int, 0, 10) allocates an underlying array
of size 10 and returns a slice of length 0 and capacity 10 that is
backed by this underlying array.
Map: An empty map is allocated with enough space to hold the
specified number of elements. The size may be omitted, in which case a small starting size is allocated.
Channel: The channel’s buffer is initialized with the specified
buffer capacity. If zero, or the size is omitted, the channel is unbuffered.

  1. Для типа среза (среза) первый размер представляет длину среза, а второй размер представляет собой емкость среза. Если указан только один параметр размера, емкость среза равна его длине;
  2. Для типа карты (словаря) существует только один размер, который указывает, сколько места выделено для карты. Если этот параметр опущен, автоматически выделяется небольшое пространство (этот параметр обычно не требуется, поскольку карта автоматически расширяется);
  3. Для типа chan (pipe) существует только один размер, который представляет буфер канала, и отсутствие параметра означает отсутствие буфера.

func new(Type) *Type

The new built-in function allocates memory. The first argument is a type, not a value, and the value returned is a pointer to a newly allocated zero value of that type.
Метод new () выделяет место в памяти. Его первым параметром является тип, а затем он возвращает указатель на область памяти этого типа.

make vs. new

make new
1 Используется только для slice, map, chan Любая структура, включая срез, карту, чан
2 Входящие параметры включают тип и размер. Допускается только один параметр типа, без размера
3 Возвращается сам тип Возврат — это указатель типа
4 Выделить место в памяти и инициализировать Выделить место в памяти без инициализации

Пример кода

Обобщено выше make с участием new Некоторые из различий, ниже мы сосредоточимся на этих различиях и некоторых проблемах, упомянутых выше, экспериментах с кодом.

нулевое значение типа

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

Type Нулевое значение
int, int32, int64 0
float32, float64 0.0
string «»
bool false
pointer, function, interface, slice, channel, map nil
Массив, структура Рекурсивно инициализировать нулевое значение

Примечание: нулевое значение массива — это нулевое значение рекурсивно инициализированного типа элемента массива, а нулевое значение среза равно нулю.

Нулевое значение Go равноЭта статьяНа это жаловались.

type TT struct < A string B int64 C struct < Cd string Ce string Cf []float64 >G bool H float32 > func main() < var i *[]int i = new([]int) fmt.Println(i, len(*i), cap(*i)) // &[] 0 0 var istring *[]string istring = new([]string) fmt.Println(istring, len(*istring), cap(*istring)) // &[] 0 0 var tt = new(TT) fmt.Println(tt) // &< 0 < []>false 0> > 

объявить, выделить, инициализировать, назначить

// 1. объявить объявление var i *int var tt *TT // 2. выделить память выделить i = new(int) tt = new(TT) // Объявление + выделение памяти var i = new(int) var i *int = new(int) i := new(int) var tt = new(TT) var tt *TT = new(TT) tt := new(TT) tt := &TT<> // 3. Инициализировать инициализировать // Инициализация - это первое присвоение var arr = make([]int, 3) // arr => [0, 0, 0] // 4. Присвоение i = 4 tt = TT // объявление + присвоение i := TT

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

Используйте new для выделения памяти для map и chan, эффект аналогичен make.
Но когда выделение памяти выполняется для среза:

 var i *[]int i = new([]int) fmt.Println(i, len(*i), cap(*i)) // &[] 0 0 j := make([]int, 3) fmt.Println(j, len(j), cap(j)) // [0 0 0] 3 3 

Первоначально нулевое значение slice равно nil, но make инициализирует его один раз, и он становится [0 0 0].
И второй параметр make не может быть пропущен, но в этом случае он может быть установлен на 0:

 j := make([]int, 0) fmt.Println(j, len(j), cap(j)) // [] 0 0 

подводить итоги

  1. make Используется только для выделения памяти среза, карты, канала, new Используется в различных типах;
  2. new Указатель возврата
  3. make Чем new Больше параметров

Условно говоря, make Функции и сценарии использования очень понятны, и new если:

  1. Если это базовый тип, например int, string, нет необходимости использовать новый
  2. Если это указатель базового типа, перед использованием указателя (печать, присвоение и т. Д.) Сначала необходимо выделить пространство памяти, а затем можно использовать новое. Но если вы напрямую присваиваете ему адрес из переменной, вам не нужно новое.
var pInt *int pInt = new(int) Эквивалентно var i int = 10 var pInt *int = &i fmt.Println(*pInt) // 10 
  1. Если это массив, фрагмент, структура или указатель карты, он аналогичен:
var s *[3]int s = new([3]int) Эквивалентно var s *[3]int = &[3]int<> var s *[]int s = new([]int) Эквивалентно var s *[]int = &[]int<> var t *TT t = new(TT) Эквивалентно var t *TT = new(TT) Эквивалентно var t *TT = &TT<> var mp = new(map[string]string) Эквивалентно mp := map[string]string<> Примечание: на чане нельзя так играть (а может, я не нашел правильной позы) 

Итак, вопрос в том, почему у вас new Какая?
Если вы знаете друзей, я надеюсь дать вам несколько советов, спасибо!

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

Справочные документы

  1. http://docs.studygolang.com/doc/effective_go.html#allocation_new
  2. http://docs.studygolang.com/doc/effective_go.html#allocation_make
  3. http://docs.studygolang.com/doc/faq#new_and_make
  4. https://studygolang.com/articles/3496
  5. http://www.jb51.net/article/126703.htm
  6. https://github.com/astaxie/build-web-application-with-golang/blob/master/zh/02.2.md
    (в этом документе также сравнивается make и new, а также объясняется нулевое значение)

Аллокация с помощью make в Go

В языке программирования Go существует встроенная функция make(T, args) . По своему назначению она отличается от функции new(T) и создает лишь срезы, каналы и карты, возвращая инициализированное значение типа T(не *T).

Дело в том, что вышеперечисленные три типа представляют под капотом линки на структуры данных, которые необходимо инициализировать перед применением. К примеру, срез (slice) — это не что иное, как дескриптор из 3-х элементов, который содержит указатель на данные, длину и емкость, а пока эти элементы не станут инициализированными, срез будет равняться nil. При этом для срезов, каналов и карт make инициализирует внутреннюю структуру данных, подготавливая значение для использования.

1-1801-267d0b.png

Функция в вышеприведенном примере аллоцирует массив из 100 int’ов, после чего создает структуру среза длиной 10/вместимостью 100, указывающую на первые десять элементов массива (следует понимать, что при создании среза емкость можно опустить). Тогда как new([] int) , напротив, возвращает указатель на вновь выделенную обнуленную структуру среза, то есть возвращает указатель на значение nil-фрагмента.

Для окончательного понимания разницы между функциями new и make, рассмотрим еще один пример:

2-1801-421172.png

Остается отметить, что функция make применяется лишь к картам, каналам и срезам, то есть она не возвращает указатель. Таким образом, если вы хотите получить явный указатель, следует воспользоваться функцией new либо выбрать адрес переменной явно.

В чем разница make и new

Скрытые данные в слайсах

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

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

Рассмотрим эту особенность на конкретных примерах:

// Плохая практика - непредсказуемое потребление памяти func cutSlice() []byte < slice := make([]byte, 256) fmt.Println(len(slice), cap(slice), &slice[0]) // 256 256 return slice[:10] > func main() < res := cutSlice() fmt.Println(len(res), cap(res), &res[0]) // 10 256 > 

Для предотвращения возникшей ошибки следует удостовериться, что копирование производится из временного слайса:

// Хорошая практика - данные скопированы из временного слайса func cutSlice() []byte < slice := make([]byte, 256) fmt.Println(len(slice), cap(slice), &slice[0]) // 256 256 copyOfSlice := make([]byte, 10) copy(copyOfSlice, slice[:10]) return copyOfSlice > func main() < res := cutSlice() fmt.Println(len(res), cap(res), &res[0]) // 10 256 > 

Библиотека Go разработчика
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»
Библиотека Go для собеса
Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса»
Библиотека задач по Go
Интересные задачи по Go для практики можно найти на нашем телеграм-канале «Библиотека задач по Go»

Функции

Функции с множественным возвратом

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

Пример объявления функции с множественным возвратом в Go:

package main import "fmt" func swap(a, b int) (int, int) < return b, a >func main() < x, y := swap(1, 2) fmt.Println(x, y) // 2 1 a, _ := swap(3, 4) fmt.Println(a) // 4 > 

В приведенном примере функция swap принимает два аргумента типа int и возвращает два значения того же типа, меняя местами исходные переменные.

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

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

Приведенная ниже функция openFile возвращает два значения, одно из которых – ошибка или nil в случае ее отсутствия.

func openFile(name string) (*File, error) < file, err := os.Open(name) if err != nil < return nil, err >return file, nil > 

Интерфейсы

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

Используем интерфейсы правильно

��Запомните важное правило

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

package worker // worker.go type Worker interface < Work() bool >func Foo(w Worker) string
package worker // worker_test.go type secondWorker struct < . >func (w secondWorker) Work() bool < . >. if Foo(secondWorker< . >) == "value"

Ниже представлен пример неправильного подхода при работе с интерфейсами:

// Плохая практика package employer type Worker interface < Worker() bool >type defaultWorker struct < . >func (t defaultWorker) Work() bool < . >func NewWorker() Worker < return defaultWorker< . >> 

Верное решение с точки зрения Go — вернуть конкретный тип и позволить Worker имитировать реализацию employer :

// Хорошая практика package employer type Worker struct < . >func (w Worker) Work() bool < . >func NewWorker() Worker < return Worker< . >> 

Конкурентность и параллелизм

Отслеживание горутин

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

  • Подробно горутины рассмотрены в статье «Горутины: что такое и как работают». Её прочтение поможет лучше разобраться в рассматриваемой теме.

�� Запомните важное правило

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

Если вы не знаете ответа на два приведённых вопроса, это может привести к возникновению утечек памяти.

Обратимся к примеру для иллюстрации данной ошибки:

func leakGoroutine() < ch := make(chan int) go func() < received := > 

Здесь функция leakGoroutine запускает горутину, которая блокирует чтение из канала ch . В результате в него ничего не отправится, и сам он никогда не закроется. Горутина будет заблокирована навсегда, вызов функции fmt.Println никогда не произойдет.

Обнаружение утечек

Инженеры из Uber, которые принимают активное участие в развитии Go, создали детектор утечек горутин – пакет goleak, нацеленный на интеграцию с модульными тестами. Рассмотрим пример работы с этим инструментом на практике.

Пусть есть некая функция leakGoroutin с утечкой горутины:

func leakGoroutine() < go func() < time.Sleep(time.Minute) >() return nil > 

И тест этой функции:

func TestLeakGoroutine(t *Testing.T) < defer goleak.VerifyNone(t) if err := leak(); err != nil < t.Fatal("Fatal message") >> 

При запуске тестов появляется сообщение об ошибке found enexpected goroutines , где указывается вершина стека с проблемной горутиной, ее состояние и идентификатор.

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

Обработка ошибок и восстановление

Ошибки в Go представлены интерфейсом error, который определяет метод Error() string . Любой тип, реализующий этот метод, может быть использован как ошибка.

type error interface
  • Чтобы больше узнать об ошибках в Go, рекомендуется прочитать статью «Исключения в Go – это легко?». Из нее вы узнаете о том, как эффективно решать проблемные ситуации в программах.

Обрабатываем ошибки правильно

Игнорирование ошибок может привести к неопределенному поведению и усложнить отладку кода. Рассмотрим правильный способ обработки ошибок на примере работы с файлом:

// плохо file, err := os.Open("filename.txt") if err == nil < // операции с файлом > 
// хорошо file, err := os.Open("filename.txt") if err != nil < log.Fatal(err) // обработка ошибки >defer f.Close() // отложенный вызов функции для закрытия файла 

Без паники, но с восстановлением

Классический способ сообщить об ошибке – вернуть тип error . Но что делать в тех случаях, когда её нельзя быстро восстановить? Тогда на помощь приходит встроенная функция panic (часто её называют просто «паника»), которая завершает программу и выводит настраиваемое сообщение об ошибке.

Ниже представлен пример простой функции с паникой:

package main import "fmt" func examplePanic() < panic("Паника - программа завершена") fmt.Println("Функция examplePanic успешно завершилась") >func main()

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

Чтобы продемонстрировать работу данного механизма, обратимся к примеру:

package main import "fmt" func Recovery() < if recoveryResult := recover(); recoveryResult != nil < fmt.Println(recoveryResult) >fmt.Println("Восстановление. ") > func Panic() < defer Recovery() panic("Паника") fmt.Println("Функция Panic успешно завершилась") >func main()

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

Паника Восстановление. Функция main успешно завершилась 

Заметьте, что функция Panic не завершается после паники. Это происходит из-за того, что с помощью defer вызывается отложенная функция Recovery , которая восстанавливает работу программы. Далее исполнение передается в main , где происходит успешное завершение всего кода.

Заключение

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

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

Материалы по теме

  • �� Golang для веб-разработки: примеры использования
  • �� Параллельное программирование в Go
  • �� Паттерны Go-кода на все случаи жизни

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *