Golang сколько памяти занимает переменная

Обновлено: 07.07.2024

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

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

Почему именно Golang? Go понравился мне по нескольким причинам.

“Из коробки” имеет многопоточность

Сборщик мусора о котором нет необходимости задумываться

Развивается как open source проект

Что рассмотрим в этой статье

Что нужно для работы с Go

Основные типы данных

Условные выражения и условные конструкции

Что нужно для работы с Go

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

Для написания кода можно использовать специальные интегрированные среды разработки (IDE), которые поддерживают Go.

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

для знакомства с языком и написания своих первых пет-проектов хватит и инструментов описанных ниже

Другие инструменты для разработки на Go:

Visual Studio Code + плагин для разработки на Go

Atom + плагин для разработки на Go

Sublime Text + плагин для разработки на Go

Существуют и другие IDE у которых есть плагины для Go например: Intellij IDEA, Netbeans. Но не думаю что стоит заострять на них внимание т.к. это достаточно большие IDE с обширным функционалом и новичок в них очень легко запутается.

Также можно программировать на Go прямо из браузера не устанавливая ничего на своё устройство.

Repl.it - поддерживает полноценный ввод и терминал

Переменные

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

В Go имя переменной может быть произвольным и может состоять из алфавитных и цифровых символов, а также символа подчеркивания. При этом при объявлении переменной первый символ должен быть либо алфавитный символ, либо символ подчеркивания. Важно, что имена переменных не могут быть такими же как зарезервированные в Go кодовые слова: break, case, chan, const, continue, default, defer, else, fallthrough, for, func, go, goto, if, import, interface, map, package, range, return, select, struct, switch, type, var.

Самое простое определение переменной:

Эта переменная называется hello и представляет строковый тип данных string.

Можно одновременно объявить несколько переменных через запятую:

В этом случае объявлены переменные a, b и c которые имеют тип данных string. В конце также указывается тип данных и все переменные ему принадлежат.

Одновременно с объявлением переменной ей можно присвоить какое-то значение:

Такой прием называется инициализацией

Неявная типизация

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

Так же можно объявить переменную сокращенным способом:

Такой способ эквивалентен предыдущему.

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

Поэтому необходимо либо указать тип, либо начальное значение, либо и то и другое

Так же можно объявить несколько переменных

Основные типы данных

Стандартное значение для всех числовых типов = 0

Целочисленные типы

int8 - число от -128 до 127 занимает в памяти 1 байт (8 бит).

int16 - число от -32768 до 32767 занимает в памяти 2 байта (16 бит).

int32 - число от -2147483648 до 2147483647 занимает в памяти 4 байта (32 бита).

int64 - число от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 занимает в памяти 8 байт (64 бита).

uint8 - целое число от 0 до 255 занимает 1 байт.

uint16 - целое число от 0 до 65535 занимает 2 байта.

uint32 - целое число от 0 до 4294967295 занимает 4 байта.

uint64 - целое число от 0 до 18 446 744 073 709 551 615 занимает 8 байт.

byte - тоже самое что и uint8, целое число от 0 до 255 занимает 1 байт.

rune - тоже самое что и int32, число от -2147483648 до 2147483647 занимает в памяти 4 байта (32 бита).

int - аналог int32 или int64 целое число которое в зависимости от разрядности операционной системы может занимать либо 4 либо 8 байт.

uint - целое беззнаковое число, аналог типа int, соответствует типам int32 и int64 занимает 4 или 8 байт.

Тип с плавающей точкой

Они же десятичные дроби.

float32 - число с плавающей точкой от 1.410-45 до 3.41038 (для положительных). Занимает в памяти 4 байта (32 бита).

float64 - число с плавающей точкой от 4.910-324 до 1.810308 (для положительных) и занимает 8 байт (64 бита).

Комплексные типы

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

complex64 - комплексное число где вещественная и мнимая части представляют числа float32.

complex128 - комплексное число где вещественная и мнимая части представляют float64.

Подробнее про комплексные числа можете прочитать здесь

Логический тип

bool - логический тип данных и может иметь только 2 значения true (истина) или false (ложь)

Строковый тип

string - В Go строки это последовательность символов заключенная в двойные кавычки

Помимо обычных символов строка может содержать в себе специальные символы

\n - переход на новую строку

\r - возврат каретки (возвращает курсор в начало текущей строки)

\t - табуляция

" - двойная кавычка внутри строк

Константы

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

Для определения константы используется ключевое слово const.

Если мы попробуем изменить значение константы, то получим ошибку.

В одном представлении можно объявить сразу несколько констант

Если у константы не указан тип, то он выводится неявно в зависимости от того значения каким инициализируется константа

Также константам обязательно необходимо присваивать значения при объявлении.

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

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

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

Арифметические операции

У переменных есть разные операции, как в алгебре.

+ сложение

- вычитание

* умножение

/ деление

При делении необходимо быть внимательным! Если в операции два целых числа, то результат деления будет округляться до целого числа даже если результат деления присваивается переменной типа float32/float64:

Чтобы получить в результате деления вещественное число, хотя бы один из операндов должен быть вещественным числом:

% (деление по модулю) возвращает остаток от деления. В этой операции могут быть использованы только целые числа:

Постфиксный инкремент (x++). Увеличивает значение переменной на единицу:

Постфиксный декремент (x--). Уменьшает значение переменной на единицу:

Комментарии

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

Однострочный комментарий пишется в одну строку после двойного слеша (//), многострочный комментарий может заключаться между символами /* и */ и занимать несколько строк:

Ввод/вывод данных

Работать с переменными конечно интересно, но намного интереснее работать с ними и видеть как их значения выводятся через консоль. Для того чтобы это сделать, необходимо воспользоваться методом Scan из стандартного пакета Go под названием fmt, например:

&a - ссылка на переменную (или адрес переменной) под названием a

Если проще, то введённое число или слово в консоль, запишется в эту переменную и будет в ней храниться пока не понадобится изменить значение переменной.

Теперь можно прочитать например, имя и возраст. Для начала создадим файл в любом удобном для вас месте на вашем компьютере. В моём случае это файл main.go расположенный по пути E:\main.go и содержит в себе следующий код:

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

Далее чтобы увидеть работу этого кода и поиграть с ним, необходимо запустить командную строку или терминал. Например: Git Bash, встроенный терминал LiteIDE. Если пользуетесь этой IDE как я, терминал можно запустить сочетанием клавиш Ctrl+Shift+4. Итак, вводим заветную команду go run main.go и нажимаем Enter.

После консоль просит нас по очереди ввести имя и возраст. Когда программа получает эту информацию, выводится строка “Человеку по имени Авдей, 99 лет.” и программа завершает свою работу.

Условные выражения и условные конструкции

Условные выражения представляют операции отношения и логические операции. Они представляют условие и возвращают значение типа bool: true - если условие истинно и false - если условие ложно.

Операции отношения позволяют сравнивать два значения. В Go есть следующие операции отношения:

== Операция “равно”, возвращает true, если оба операнда равны или false если они не равны.

> Операция “больше чем”, возвращает true, если первый операнд больше второго, и false если первый операнд меньше второго, либо оба операнда равны.

< Операция “меньше чем”, возвращает true, если первый операнд меньше второго, и false если первый операнд больше второго, либо оба операнда равны.

>= Операция “больше или равно”, возвращает true, если первый операнд больше второго или равен второму, и false если первый операнд меньше второго.

<= Операция “меньше или равно”, возвращает true, если первый операнд меньше или равен второму, и false если первый операнд больше второго.

!= Операция “не равно”, возвращает true, если первый операнд не равен второму, и false если оба операнда равны.

Логические операции сравнивают два условия. Они применяются к отношениям и объединяют несколько операций отношения. К логическим операциям относятся:

! - операция отрицания (логическое НЕ). Инвертирует значение. Если операнд равен true, то возвращает false. И наоборот, если операнд равен false, вернет true.

&& - конъюнкция, логическое умножение (логическое И). Возвращает true, если оба операнда не равны false. Возвращает false, если хотя бы один операнд равен false.

|| - дизъюнкция, логическое сложение (логическое ИЛИ). Возвращает true, если хотя бы один операнд не равен false. Возвращает false, если оба операнда равны false.

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

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

if else

Конструкция if принимает условие - выражение, которое возвращает значение типа bool. И если это условие истинно, то выполняется последующий блок инструкций:

В приведенном примере после запуска кода, консоль выведет “x меньше y”.

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

В этом примере после запуска кода, консоль выведет “x больше y” т.к. условие в блоке else будет верным.

Если необходимо проверить несколько альтернативных вариантов, то можно добавить выражение else if:

В этом случае выполнится код в блоке else, потому как условия описанные в блоках if и else if равны false.

Выражений else if может быть множество.

Switch

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

Запустив этот код, мы увидим что выполнится case 7, потому что значение в этом кейсе совпадает со значением в switch.

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

Стоит отметить, что в Go код после case выполняется до следующего case, то есть нет необходимости заканчивать каждый блок case словом break. Это было добавлено в язык специально, чтобы уменьшить количество ошибок в блоках switch. Если в текущем блоке написать ключевое слово fallthrough, то следующий case выполнится вне зависимости от того, истинно ли его условие:

Выполнив этот код, мы увидим следующую картину в консоли:

В консоль будет выведено всё начиная с 4 кейса, т.к. значение лежащее в переменной x равно 4, а остальные case выполнятся из-за того, что внутри case написано ключевое слово fallthrough.

В switch допускается использование произвольных условий в каждом case:

Если запустить этот код и ввести допустим число 256, то получим в консоли:

Заключение

В заключении статьи хотелось бы поделиться полезными ссылками:

Книга - Golang для профи. Работа с сетью, многопоточность, структуры данных и машинное обучение с Go | Цукалос Михалис. (Для новичков будет не совсем понятна, хоть и позиционируется как для начинающих разработчиков, так и для тех, кто работает не один год.)

50 оттенков Go. Очень полезная статья, спасибо VK Team за перевод оригинала

Для тех кто любит проходить различные курсы:

Программирование на Golang (курс составлен неплохо, но полные новички возможно испытают некоторые сложности из-за того, что нельзя посмотреть какими тестами облагаются задания)

Ресурсы с заданиями для практики:

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

Мне интересно узнать стоимость памяти map и slice , поэтому я написал программу для сравнения размеров. Я получаю объем памяти unsafe.Sizeof(s) , но, очевидно, это неправильно, потому что, когда я изменяю размер, вывод такой же.

3 ответа

unsafe.SizeOf() и reflect.Type.Size() возвращают только размер переданного значения без рекурсивный обход структуры данных и добавление размеров указанных значений.

Срез является относительно простой структурой: reflect.SliceHeader , и, поскольку мы знаем, что он ссылается резервный массив, мы можем легко вычислить его размер "вручную", например:

Карты - это гораздо более сложные структуры данных, я не буду вдаваться в подробности, но посмотрите на этот вопрос + ответ: Golang: вычисление объема памяти (или длины в байтах) карты

Расчет размера любой переменной или структуры (рекурсивно)

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

(Удалите время и распечатки вызовов из getSlice() и getMap() , конечно.)

B/op значения говорят вам, сколько байтов было выделено для операции. allocs/op сообщает, сколько (отдельных) выделений памяти произошло за операцию.

В моей 64-битной архитектуре (где размер int равен 8 байтам) он говорит, что размер фрагмента, содержащего 2000 элементов, составляет примерно 16 КБ (в соответствии с 2000 * 8 байтов). Карта с 1000 int-int парами требуется приблизительно для выделения 42 КБ.

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

Это правильный путь, используя unsafe.Sizeof(s) . Просто результат останется неизменным для данного типа - целое число, строка и т. Д., Независимо от точного значения.

Sizeof принимает выражение x любого типа и возвращает размер в байтах гипотетической переменной v, как если бы v было объявлено через var v = x. Размер не включает в себя память, на которую может ссылаться х. Например, если x является слайсом, Sizeof возвращает размер дескриптора слайса, а не размер памяти, на которую ссылается слайс.

Вы можете использовать сортировку и затем сравнивать представления значений в байтах с Size() . Это только вопрос преобразования данных в байтовую строку.

Оптимизация структур в Golang для эффективного распределения памяти

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

Хорошо, когда программа написана просто и логично и обладает хорошей гибкостью. Программы на Go быстры: в этом они сопоставимы с программами на C++.

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

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

Поэкспериментируем с размером структуры

Сначала создадим структуру без полей type Foo struct <> и посмотрим, сколько памяти занимает она и указатель на нее:

Запустив программу в консоли, мы получим:

Теперь добавим пару полей:

Результат будет 8 и 8.

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

Теперь поиграем с количеством и последовательностью полей:

Результат будет 12.

В языке Си используется термин «машинное слово», размер которого соответствует 4 или 8 байтам (в зависимости от разрядности процессора компьютера — 32 или 64).

А в Go используется «требуемое выравнивание». Его значение равно размеру памяти, требующемуся самому большому полю в структуре.

То есть, если в структуре есть только поля int32 , то «требуемое выравнивание» составит 4 байта. А если есть и int32 , и int64 — то 8 байтов.

Поэтому пример можно записать в таком виде:

Добавим в конец структуры еще одно поле — ddd bool .

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

Здесь результат такой же — 12. Потому что сработала оптимизация (из-за наличия смещений).

Поля ccc bool и ddd bool этой структуры «свернулись» до единичного «требуемого выравнивания», т. е. 4 байтов (из-за наличия int32 ).

Ответ: 24. Почему? Потому что теперь «требуемое выравнивание» для этой структуры составляет 8 байтов.

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

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

Конструкция .Sizeof(Foo<>) из unsafe дала результат: 12.

Вот как она оптимизируется:

В этом случае unsafe.Sizeof(Foo <>) дала уже такой результат: 8.

Кстати, в пакете unsafe есть, кроме unsafe.Sizeof , еще несколько подобных функций.

Одна из них — unsafe.Offsetof — возвращает количество байтов между началом структуры и началом поля.

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

В качестве примера рассчитаем «требуемое выравнивание» для структур из приведенного выше кода:

Другие инструменты оптимизации

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

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

Используем aligncheck для наших структур Foo и Bar :

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

Теперь используем maligned для структур Foo и Bar :

И снова отличный результат!

Такие инструменты очень удобно использовать в golangci-lint , превосходном средстве контроля качества кода для Go.

Для этого нужно просто подключить maligned в настройках golangci-lint , например из конфигурационного файла . golangci.example.yml :

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

Итак, мы выяснили, что такое выравнивание, и разобрались со спецификой его работы. Кое-что узнали о том, как оптимизировать потребление памяти, выделяемой для структур, и научились измерять разницу до и после оптимизации. А потом нашли инструменты, позволяющие автоматизировать поиск структур для оптимизации ( aligncheck , maligned и golangci-lint ).

Я представил нам использование Lua несколько дней назад,implement our new Web Application Firewall.

ДругойCloudFlare(Компания автора) Большой популярностью стал язык Голанг. В прошлом я писал статьюhow we use GoВвести аналогичныеRailgunПодготовка сетевых сервисов.

Есть большая проблема в написании долго работающих сетевых сервисов на таких языках, как Golang с GC, и это управление памятью.

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

Вот простая программа, которая создает много мусора, создавая массив размером от 5 000 000 до 10 000 000 байт каждую секунду. Программа поддерживает 20 таких массивов, остальные отбрасываются. Программа разработана таким образом, чтобы моделировать очень распространенную ситуацию: с течением времени разные части программы обращаются к памяти, некоторые зарезервированы, но большинство из них больше не используются повторно. В сетевом программировании на языке Go, когда горутины используются для обработки сетевых подключений и сетевых запросов (сетевых подключений или запросов), горутины обычно применяются к части памяти (например, срезы для хранения полученных данных), а затем больше не используют их. Со временем большой объем памяти будет использоваться сетевыми соединениями, и накопившийся мусор от соединения будет приходить и уходить.

Использование программыruntime.ReadMemStatsФункция для получения информации об использовании кучи. Он напечатал четыре значения,

HeapSys: память, которую программа запрашивает у приложения.

HeapAlloc: выделенная в настоящее время память в куче

HeapIdle: в настоящее время неиспользуемая память в куче

HeapReleased: память, восстановленная в операционной системе

Сборщик мусора часто запускается в Golang (см. Переменную среды GOGC (переменная среды GOGC), чтобы понять, как управлять операцией сборки мусора), поэтому некоторая память помечается как «неиспользуемая» во время работы, размер памяти в куче изменится : Это приведет к изменению HeapAlloc и HeapIdle. Сборщик в Golang освободит память, которая не использовалась более 5 минут, поэтому HeapReleased не будет часто меняться.

На следующем рисунке показана ситуация после того, как вышеуказанная программа проработала 10 минут:


(На этом и последующих рисунках левая ось - это размер памяти в байтах, а правая ось - количество выполнений программы)

Красная линия показывает количество байтовых буферов в пуле. 20 буферов быстро достигли 150 000 000 байт. Синяя линия вверху указывает объем памяти, который программа запрашивает у операционной системы. Стабильный - 375 000 000 байт.Таким образом, программа потребовала в 2,5 раза больше места!

Когда происходит сбор мусора, происходит скачок HeapIdle и HeapAlloc. Оранжевая линия - это количество отправлений makeBuffer ().

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

Когда программа выполняется непрерывно, простаивающая память (HeapIdle) будет повторно использоваться, но она редко возвращается в операционную систему.

Один из способов решить эту проблему - вручную выполнить управление памятью в программе. Например,

Программу можно переписать так:

На следующем рисунке показана ситуация после того, как вышеуказанная программа проработала 10 минут:


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

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

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

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

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

Аналогичная технология используется в нашем реальном проекте. Фактическое использование ресайклера (простая версия) показано ниже. Существует горутина, которая обрабатывает построение буферов и используется несколькими горутинами. Два канала get (получение нового буфера) и give (повторное использование буфера в пуле) используются всеми горутинами.

Сборщик поддерживает соединение с восстановленными буферами и периодически отбрасывает те буферы, которые слишком стары и больше не могут использоваться (в примере кода этот цикл составляет одну минуту). Это позволяет программе автоматически реагировать на взрывоопасные потребности в буфере.

После выполнения программы в течение 10 минут изображение будет похоже на второе:


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

Читайте также: