One place for hosting & domains

      Булева логика в Go


      Логический тип данных (bool) может иметь одно из двух значений, true (истина) или false (ложь). Булевы операторы используются в программировании для сравнения и для контроля потока процессов программы.

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

      Тип данных булевых операторов в Go называется bool, все символы указаны строчными буквами. Значения true и false всегда обозначаются символами t и f в нижнем регистре, поскольку это особые значения в Go.

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

      Сравнительные операторы

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

      В таблице ниже показаны булевы операторы сравнения.

      Оператор Значение
      == равно
      ! = не равно
      < меньше
      > больше
      <= меньше или равно
      >= больше или равно

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

      x := 5
      y := 8
      

      Поскольку в этом примере x имеет значение 5, эта переменная меньше y со значением 8.

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

      package main
      
      import "fmt"
      
      func main() {
          x := 5
          y := 8
      
          fmt.Println("x == y:", x == y)
          fmt.Println("x != y:", x != y)
          fmt.Println("x < y:", x < y)
          fmt.Println("x > y:", x > y)
          fmt.Println("x <= y:", x <= y)
          fmt.Println("x >= y:", x >= y)
      }
      

      Output

      x == y: false x != y: true x < y: true x > y: false x <= y: true x >= y: false

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

      • 5 (x) равно 8 (y)? false
      • 5 не равно 8? true
      • 5 меньше 8? true
      • 5 больше 8? false
      • 5 меньше или равно 8? true
      • 5 не меньше или равно 8? false

      Хотя здесь использовались целые числа, вы можете заменить их значениями с плавающей точкой.

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

      Вы можете посмотреть практический пример сравнения строк:

      Sammy := "Sammy"
      sammy := "sammy"
      
      fmt.Println("Sammy == sammy: ", Sammy == sammy)
      

      Output

      Sammy == sammy: false

      Строка Sammy не равна строке sammy, поскольку они не точно совпадают; одно значение начинается с заглавной S, а другое — с s в нижнем регистре. Однако если вы добавите другую переменную, которой присвоено значение Sammy, они будут равняться:

      Sammy := "Sammy"
      sammy := "sammy"
      alsoSammy := "Sammy"
      
      fmt.Println("Sammy == sammy: ", Sammy == sammy)
      fmt.Println("Sammy == alsoSammy", Sammy == alsoSammy)
      

      Output

      Sammy == sammy: false Sammy == alsoSammy true

      Также вы можете использовать для сравнения двух строк и другие операторы сравнения, в том числе > и <. Go проводит лексикографическое сравнение строк, используя значения ASCII для символов.

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

      t := true
      f := false
      
      fmt.Println("t != f: ", t != f)
      

      Output

      t != f: true

      Предыдущий блок кода оценил, что true не равняется false.

      Обратите внимание на различия между операторами = и ==.

      x = y   // Sets x equal to y
      x == y  // Evaluates whether x is equal to y
      

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

      Логические операторы

      Для сравнения используется два логических оператора. Они оценивают выражения до булевых значений, возвращая true или false. Это операторы &&, || и !, которые определены в следующем списке:

      • && (x && y) является оператором и. Это верно, если оба выражения верны.
      • || (x || y) является оператором или. Это верно, если хотя бы одно выражение является верным.
      • ! (! x) является оператором нет. Это верно, только если выражение является ложным.

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

      Чтобы понять принцип работы логических операторов, оценим три выражения:

      fmt.Println((9 > 7) && (2 < 4))   // Both original expressions are true
      fmt.Println((8 == 8) || (6 != 6)) // One original expression is true
      fmt.Println(!(3 <= 1))            // The original expression is false
      

      Output

      true true true

      В первом случае fmt.Println((9 > 7) && (2 < 4)) оба выражения 9 > 7 и 2 < 4 должны быть оценены как истинные, поскольку использовался оператор и.

      Во втором случае fmt.Println((8 == 8) || (6 ! = 6)), поскольку 8 == 8 оценивается как истина, не имеет значения, что 6 ! = 6 оценивается как ложь, поскольку использовался оператор или. Если бы вы использовали оператор и, результат был бы ложным.

      В третьем случае, fmt.Println(!( 3 <= 1)), оператор нет аннулирует ложное значение, возвращаемое выражением 3 <=1.

      Заменим целые числа числами с плавающей точкой и рассмотрим ложные оценки:

      fmt.Println((-0.2 > 1.4) && (0.8 < 3.1))  // One original expression is false
      fmt.Println((7.5 == 8.9) || (9.2 != 9.2)) // Both original expressions are false
      fmt.Println(!(-5.7 <= 0.3))               // The original expression is true
      

      В этом примере:

      • и требует, чтобы хотя бы одно выражение оценивалось как ложное.
      • или требует, чтобы оба выражения оценивались как ложные.
      • ! внутреннее выражение должно быть истинным, чтобы новое выражение оценивалось как ложное.

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

      Также вы можете создавать комплексные выражения, используя операторы &&, || и !:

      !((-0.2 > 1.4) && ((0.8 < 3.1) || (0.1 == 0.1)))
      

      Вначале рассмотрим внутреннее выражение: (0.8 < 3.1) || (0.1 == 0.1). Это выражение оценивается как истинное (true), поскольку оба математических выражения истинные.

      Затем Go берет возвращаемое значение true и сочетает его со следующим внутренним выражением: (-0.2 > 1.4) && (true). В этом примере возвращается значение false, поскольку математическое выражение -0.2 > 1.4 является ложным, а сочетание (false) и (true) возвращает результат false.

      В заключение идет внешнее выражение: !( false), которое оценивается как true, так что окончательное возвращаемое при печати этого выражения значение выглядит так:

      Output

      true

      Логические операторы &&, || и ! оценивают выражения и возвращают булевы значения.

      Таблицы истины

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

      Далее приведены таблицы истины для оператора сравнения ==, а также каждого из логических операторов &&, || и !. Хотя возможно вы и не будете их использовать, запомнить их полезно, потому что это упростит процесс принятия решений при программировании.

      == (равно) Таблица истины

      x == y возвращает
      true == true true
      true == false false
      false == true false
      false == false true

      && (и) Таблица истины

      x и y возвращает
      true и true true
      true и false false
      false и true false
      false и false false

      || (или) Таблица истины

      x или y возвращает
      true или true true
      true или false true
      false или true true
      false или false false

      ! (нет) Таблица истины

      нет x возвращает
      нет true false
      нет false true

      Таблицы истины — это обычные математические таблицы, используемые в логике. Их полезно помнить при построении алгоритмов (инструкций) в компьютерном программмировании.

      Использование булевых операторов для управления потоком

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

      Условие производит оценку до булева значения true или false и представляет собой точку принятия решения в программе. Это означает, что условие позволяет оценить истинность.

      Оператор — это блок кода, который идет за условием и определяет результат выполнения программы. Он показывает, что нужно сделать, в конструкции «если x = true, нужно сделать это».

      В блоке кода ниже приведен пример совместной работы операторов сравнения и условных выражений для управления потоком в программе Go:

      if grade >= 65 {                 // Condition
          fmt.Println("Passing grade") // Clause
      } else {
          fmt.Println("Failing grade")
      }
      

      Программа оценивает результат каждого ученика как проходной или непроходной. Для ученика с оценкой 83 первое выражение имеет значение true и активирует вывод выражения Passing grade. Для ученика с оценкой 59 первое выражение имеет значение false, и программа переходит к выражению вывода, связанному с выражением else: Failing grade.

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

      Заключение

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



      Source link

      Типы данных в Go


      Введение

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

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

      Базовая информация

      Чтобы лучше понять типы данных следует посмотреть на различные типы данных, которые мы используем в реальном мире. Например, в реальном мире мы используем числа. Это могут быть положительные числа (0, 1, 2, …), целые числа (…, -1, 0, 1, …) и, например, иррациональные числа (π).

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

      5 + π
      

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

      5 + π = 5 + 3.14 = 8.14
      

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

      shark + 8
      

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

      Целые числа

      Как и в математике, в программировании к целым числам относятся положительные числа, отрицательные числа и 0 (…, -1, 0, 1, …). В Go целое число определяется как int. Как и в других языках программирования, в числах не следует использовать запятые для отделения нулей, так что вместо 1,000 нужно писать 1000.

      Целое число можно вывести в простой форме:

      fmt.Println(-459)
      

      Output

      -459

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

      var absoluteZero int = -459
      fmt.Println(absoluteZero)
      

      Output

      -459

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

      sum := 116 - 68
      fmt.Println(sum)
      

      Output

      48

      Как видно на экране результатов, математический оператор - был использован для вычитания целого числа 68 из 116, в результате чего получилось 48. Дополнительную информацию о декларировании переменных можно найти в разделе Объявление типов данных для переменных.

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

      Числа с плавающей запятой

      Число с плавающей точкой *или float *— это действительное число, которое нельзя выразить в форме целого числа. В состав действительных чисел входят все рациональные и иррациональные числа, и поэтому числа с плавающей точкой могут содержать дробную часть, например 9,0 или -116,42. Чтобы представить тип float в программе Go, подумайте о числе с десятичной запятой.

      Мы можем вывести число с плавающей запятой так же легко, как и целое число:

      fmt.Println(-459.67)
      

      Output

      -459.67

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

      absoluteZero := -459.67
      fmt.Println(absoluteZero)
      

      Output

      -459.67

      В Go можно выполнять математические операции с числами с плавающей точкой, как и с целыми числами:

      var sum = 564.0 + 365.24
      fmt.Println(sum)
      

      Output

      929.24

      При работе с целыми числами и числами с плавающей точкой важно помнить, что 3 ≠ 3.0, поскольку 3 означает целое число, а 3.0 — число с плавающей точкой.

      Размеры числовых типов

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

      Сегодня большинство системных архитектур представляют собой 32-битные или 64-битные архитектуры. Например, вы можете разрабатывать приложения для современных ноутбуков с 64-битной операционной системой Windows. Однако, если вы разрабатывает приложения для таких устройств, как фитнес-браслеты, вам может потребоваться 32-битная архитектура. Если вы используете архитектурно-независимый тип, например int32, вне зависимости от компилируемой архитектуры, у этого типа будет постоянный размер.

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

      Помимо разных размеров, такие типы данных, как целые числа, могут иметь два базовых типа: со знаком и без знака. int8 — это целое число со знаком, которое может иметь значение от -128 до 127. uint8 — целое число без знака, которое может иметь только положительное значение от 0 до 255.

      Диапазоны зависят от размера в битах. Для двоичных данных 8 бит могут представлять 256 разных значений. Поскольку тип int должен поддерживать как положительные, так и отрицательные значения, 8-битное целое число (int8) будет иметь диапазон от -128 до 127, что соответствует 256 возможных уникальных значений.

      В Go имеются следующие архитектурно-независимые типы целых чисел:

      uint8       unsigned  8-bit integers (0 to 255)
      uint16      unsigned 16-bit integers (0 to 65535)
      uint32      unsigned 32-bit integers (0 to 4294967295)
      uint64      unsigned 64-bit integers (0 to 18446744073709551615)
      int8        signed  8-bit integers (-128 to 127)
      int16       signed 16-bit integers (-32768 to 32767)
      int32       signed 32-bit integers (-2147483648 to 2147483647)
      int64       signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
      

      Числа с плавающей точкой и комплексные числа также могут иметь разные размеры:

      float32     IEEE-754 32-bit floating-point numbers
      float64     IEEE-754 64-bit floating-point numbers
      complex64   complex numbers with float32 real and imaginary parts
      complex128  complex numbers with float64 real and imaginary parts
      

      Также существует несколько типов псевдонимов чисел, которые присваивают полезные имена определенным типам данных:

      byte        alias for uint8
      rune        alias for int32
      

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

      Псевдоним rune немного отличается от вышеописанного. Если byte и uint8 содержат одни и те же данные, rune может представлять собой один байт или четыре байта, определенный в int32 диапазон. rune используется для представления символа Unicode, в то время как символы ASCII может представлять только тип данных int32.

      Кроме того, в Go имеются следующие типы для конркретных реализаций:

      uint     unsigned, either 32 or 64 bits
      int      signed, either 32 or 64 bits
      uintptr  unsigned integer large enough to store the uninterpreted bits of a pointer value
      

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

      Выбор числовых типов данных

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

      Как обсуждалось ранее в этой статье, существуют типы, не зависящие от архитектуры, и типы, зависящие от реализации. Для целочисленных данных в Go обычно используются такие типы реализации, как int или uint вместо int64 или uint64. Обычно это обеспечивает более высокую скорость обработки в целевой архитектуре. Например, если вы используете int64 и выполняете компиляцию до 32-битной архитектуры, обработка этих значений займет в два раза меньше времени, поскольку для перемещения данных по архитектуре требуются дополнительные процессорные циклы. Если вы использовали int, программа определяет 32-битный размер для 32-битной архитектуры, в результате чего обработка будет значительно быстрее.

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

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

      Переполнение и циклический переход

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

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

      package main
      
      import "fmt"
      
      func main() {
          var maxUint32 uint32 = 4294967295 // Max uint32 size
          fmt.Println(maxUint32)
      }
      

      После компиляции и запуска получим следующий результат:

      Output

      4294967295

      Если мы прибавим 1 к значению времени исполнения, произойдет циклический переход на 0:

      Output

      0

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

      package main
      
      import "fmt"
      
      func main() {
          var maxUint32 uint32 = 4294967295 + 1
          fmt.Println(maxUint32)
      
      }
      

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

      Поскольку компилятор может определить переполнение, он выведет сообщение об ошибке:

      Output

      prog.go:6:36: constant 4294967296 overflows uint32

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

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

      Логические операторы

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

      Значения true и false всегда обозначаются символами t и f в нижнем регистре, поскольку эти идентификаторы заранее декларированы в Go.

      Многие математические операции дают ответы, соответствующие значениям «истина» или «ложь»:

      • больше чем
        • 500 > 100 истина
        • 1 > 5 ложь
      • меньше чем
        • 200 < 400 истина
        • 4 < 2 ложь
      • равно
        • 5 = 5 истина
        • 500 = 400 ложь

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

      myBool := 5 > 8
      

      Мы можем распечатать значение логического оператора посредством вызова функции fmt.Println():

      fmt.Println(myBool)
      

      Поскольку 5 не больше 8, мы получим следующий результат:

      Output

      false

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

      Строки

      Строка — это последовательность из одного или нескольких символов (буквы, числа, символы), которая может представлять собой константу или переменную. Строки существуют внутри одинарных кавычек ` или двойных кавычек " в Go и имеют разные характеристики в зависимости от типа кавычек.

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

      Необработанные литералы строк

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

      a := `Say "hello" to Go!`
      fmt.Println(a)
      

      Output

      Say "hello" to Go!

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

      a := `Say "hello" to Go!n`
      fmt.Println(a)
      

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

      Output

      Say "hello" to Go!n

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

      a := `This string is on
      multiple lines
      within a single back
      quote on either side.`
      fmt.Println(a)
      

      Output

      This string is on multiple lines within a single back quote on either side.

      В предыдущих блоках кода новые строчки буквально переносились из входных данных в результаты.

      Интерпретируемые литералы строк

      Интерпретируемые строковые литералы — это последовательность символов внутри двойных кавычек, например, "bar". Внутри кавычек может находиться любой символ, кроме символа новой строчки и незакрытых двойных кавычек. Для отображения двойных кавычек в интерпретируемой строке вы можете использовать обратную косую черту в качестве символа перехода:

      a := "Say "hello" to Go!"
      fmt.Println(a)
      

      Output

      Say "hello" to Go!

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

      Строки с символами UTF-8

      UTF-8 — это схема кодировки, используемая для кодировки символов переменной ширины в 1-4 байтах. Go поддерживает символы UTF-8 без специальных настроек, библиотек или пакетов. Латинские символы, такие как буква A, могут быть представлены значением ASCII, например, числом 65. Однако при использовании специальных символов, таких как международный символ , требуется UTF-8. Go использует тип псевдонима rune для данных UTF-8.

      a := "Hello, 世界"
      

      Вы можете использовать ключевое слово range в цикле for для индексации любых строк в Go, в том числе строк UTF-8. Мы более подробно расскажем о циклах for и о ключевом слове range позднее, а сейчас важно помнить, что мы можем использовать их для подсчета количества байт в строке:

      package main
      
      import "fmt"
      
      func main() {
          a := "Hello, 世界"
          for i, c := range a {
              fmt.Printf("%d: %sn", i, string(c))
          }
          fmt.Println("length of 'Hello, 世界': ", len(a))
      }
      

      В блоке кода выше мы декларировали переменную a и назначили для нее значение Hello, 世界. Назначенный текст содержит символы UTF-8.

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

      С помощью функции fmt.Printf мы выводим строку формата %d: %sn. %d — это печатное обозначение цифры (в данном случае целого числа), а %s — обозначение строки. Затем мы задали значения i или текущего индекса цикла for,а также c, который представляет текущий символ цикла for.

      В заключение мы распечатали полную переменную a с помощью встроенной функции len.

      Мы уже упоминали, что rune является псевдонимом int32 и может состоять из 1-4 байт. Для определения символа требуется три байта, и индекс перемещается соответствующим образом при изменении диапазона в строке UTF-8. По этой причине печать i не выполняется последовательно.

      Output

      0: H 1: e 2: l 3: l 4: o 5: , 6: 7: 世 10: 界 length of 'Hello, 世界': 13

      Как видите, длина превышает количество проходов диапазона строки.

      Вы не всегда будете использовать строки UTF-8, но теперь вы понимаете, почему они относятся к типу rune, а не int32.

      Декларирование типов данных для переменных

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

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

      В следующем примере мы декларируем переменную с именем pi типа float64.

      В первую очередь декларируется ключевое слово var:

      var pi float64
      

      Далее идет имя переменной pi:

      var pi float64
      

      Последним идет тип данных float64:

      var pi float64
      

      При желании мы можем задать начальное значение, например, 3.14:

      var pi float64 = 3.14
      

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

      Например, в Go тип декларируется при декларировании переменной:

      var pi float64 = 3.14
      var week int = 7
      

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

      Этим Go отличается от таких языков как PHP, где тип данных привязывается к значению:

      $s = "sammy";         // $s is automatically a string
      $s = 123;             // $s is automatically an integer
      

      В предыдущем блоке кода первая переменная $s является строкой, поскольку ей присвоено значение "sammy", а вторая являетя целым числом, поскольку ей присвоено значение 123.

      Теперь рассмотрим более сложные типы данных — массивы.

      Массивы

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

      Массивы определяются посредством декларирования размера массива и типа данных с определением значений внутри фигурных скобок { }.

      Массив строк должен выглядеть следующим образом:

      [3]string{"blue coral", "staghorn coral", "pillar coral"}
      

      Мы можем сохранить массив в переменной и распечатать его:

      coral := [3]string{"blue coral", "staghorn coral", "pillar coral"}
      fmt.Println(coral)
      

      Output

      [blue coral staghorn coral pillar coral]

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

      Срезы

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

      Срезы определяются посредством декларирования типа данных, которому предшествуют открывающая и закрывающая квадратные скобки [], а значения указываются в фигурных скобках { }.

      Срез целых чисел выглядит следующим образом:

      []int{-3, -2, -1, 0, 1, 2, 3}
      

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

      []float64{3.14, 9.23, 111.11, 312.12, 1.05}
      

      Срез строк выглядит следующим образом:

      []string{"shark", "cuttlefish", "squid", "mantis shrimp"}
      

      Определим срез строк как seaCreatures:

      seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp"}
      

      Мы можем распечатать его посредством вызова переменной:

      fmt.Println(seaCreatures)
      

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

      Output

      [shark cuttlefish squid mantis shrimp]

      Мы можем использовать ключевое слово append для добавления элементов в срез. Следующая команда добавляет значение строки seahorse в срез:

      seaCreatures = append(seaCreatures, "seahorse")
      

      Вы можете проверить его добавление посредством вывода значений:

      fmt.Println(seaCreatures)
      

      Output

      [shark cuttlefish squid mantis shrimp seahorse]

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

      Карты

      Карта — это встроенный в Go тип хэша или словаря. Карты используют пары ключей и значений для хранения данных. Это полезно в программировании для быстрого просмотра значений по индексу или (в данном случае) по ключу. Например, вам может потребоваться карта пользователей с индексацией по идентификатору пользователя. Ключ может быть идентификатором пользователя, а объект пользователя будет значением. Карта создается с помощью ключевого слова map с типом данных ключа в квадратных скобках [ ], за которым идут пары значение и ключ в фигурных скобках.

      map[key]value{}
      

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

      map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
      

      Обратите внимание, что помимо фигурных скобок карта содержит двоеточия. Слова слева от двоеточий являются ключами. Ключи могут относится к любому *comparable *типу в Go. Сравнимые типы — это типы примитивов, в том числе строки, целые числа и т. д. Тип примитива определяется языком, а не составляется посредством сочетания других типов. Хотя допускается использование определяемых пользователем типов, во избежание ошибок программирования их лучше оставлять простыми. В словаре выше содержатся ключи: name, animal, color и location.

      Слова справа от двоеточий являются значениями. Значения могут состоять из любого типа данных. Значения в словаре выше: Sammy, shark, blue и ocean.

      Давайте сохраним карту внутри переменной и выведем ее:

      sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
      fmt.Println(sammy)
      

      Output

      map[animal:shark color:blue location:ocean name:Sammy]

      Если мы хотим изолировать цвет Sammy, мы можем использовать вызов sammy["color"]. Распечатаем результат:

      fmt.Println(sammy["color"])
      

      Output

      blue

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

      Заключение

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

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



      Source link

      Выражения defer в Go


      Введение

      В Go используется много общих управляющих ключевых слов, которые используются и в других языках программирования. В число этих ключевых слов входят if, switch, for и т. д. Однако некоторые ключевые слова отсутствуют в большинстве языков программирования. Одно из них — ключевое слово defer, и хотя оно используется нечасто, вы быстро поймете, насколько полезно оно может быть для ваших программ.

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

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

      Что представляет собой выражение defer

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

      Посмотрим, как выражение defer работает при выводе текста:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          defer fmt.Println("Bye")
          fmt.Println("Hi")
      }
      

      В функции main два выражения. Первое выражение начинается с ключевого слова defer, за которым идет выражение print, которое выводит текст Bye. Следующая строчка выводит текст Hi.

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

      Output

      Hi Bye

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

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

      main.go

      package main
      
      import "fmt"
      
      func main() {
          // defer statement is executed, and places
          // fmt.Println("Bye") on a list to be executed prior to the function returning
          defer fmt.Println("Bye")
      
          // The next line is executed immediately
          fmt.Println("Hi")
      
          // fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
      }
      

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

      Хотя этот код иллюстрирует порядок запуска defer, это не совсем обычный способ, который использовался бы при написании программы Go. Более вероятно использование defer для очистки ресурса, например дескриптора файла. Далее мы покажем, как это сделать.

      Использование defer для очистки ресурсов

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

      main.go

      package main
      
      import (
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("readme.txt", "This is a readme file"); err != nil {
              log.Fatal("failed to write file:", err)
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          _, err = io.WriteString(file, text)
          if err != nil {
              return err
          }
          file.Close()
          return nil
      }
      

      В этой программе имеется функция write, которая вначале пытается создать файл. При возникновении ошибки функция выводит сообщение об ошибке и закрывается. Затем она пытается записать строку This is a readme file в указанный файл. При возникновении ошибки функция выводит сообщение об ошибке и закрывается. Затем функция пытается закрыть файл и вернуть ресурс в систему. В заключение функция возвращает значение nil, подтверждая выполнение функции без ошибки.

      Хотя этот код работает, в нем есть небольшая ошибка. Если вызов io.WriteString не обрабатывается надлежащим образом, функция прекращает работу без закрытия файла и возврата ресурса в систему.

      Эту проблему можно решить, добавив еще одно выражение file.Close(), которое позволит решить проблему без использования defer:

      main.go

      package main
      
      import (
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("readme.txt", "This is a readme file"); err != nil {
              log.Fatal("failed to write file:", err)
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          _, err = io.WriteString(file, text)
          if err != nil {
              file.Close()
              return err
          }
          file.Close()
          return nil
      }
      

      Теперь программа закроет файл, даже если вызов io.WriteString не будет обработан надлежащим образом. Хотя эту ошибку было относительно легко найти и исправить, в более сложной функции ее можно было и пропустить.

      Вместо добавления второго вызова file.Close() мы можем использовать выражение defer, чтобы вызывать Close() вне зависимости от хода выполнения функции.

      Вот версия, использующая ключевое слово defer:

      main.go

      package main
      
      import (
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("readme.txt", "This is a readme file"); err != nil {
              log.Fatal("failed to write file:", err)
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          defer file.Close()
          _, err = io.WriteString(file, text)
          if err != nil {
              return err
          }
          return nil
      }
      

      В этот раз мы добавили строчку кода: defer file.Close(). Это указывает компилятору, что функцию file.Close нужно выполнить перед выходом из функции write.

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

      Однако, добавив defer, мы внесли в код еще одну ошибку. Мы больше не проверяем ошибки, которые может возвратить метод Close. Это связано с тем, что при использовании defer нет способа передать в функцию какое-либо возвращаемое значение.

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

      Теперь посмотрим, как мы можем отложить вызов Close и при этом сообщить об ошибке, если она возникнет.

      main.go

      package main
      
      import (
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("readme.txt", "This is a readme file"); err != nil {
              log.Fatal("failed to write file:", err)
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          defer file.Close()
          _, err = io.WriteString(file, text)
          if err != nil {
              return err
          }
      
          return file.Close()
      }
      

      Единственное изменение этой программы мы внесли в последнюю строку, где мы возвращаем file.Close(). Если при вызове Close возникает ошибка, она будет возвращена вызывающей функции, как и ожидается. Необходимо помнить, что выражение defer file.Close() также будет выполняться после выражения return. Это означает, что функция file.Close() может быть вызвана дважды. Хотя это не идеально, эта практика является допустимой, поскольку она не создаст никаких побочных эффектов для вашей программы.

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

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

      Использование нескольких выражений defer

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

      main.go

      package main
      
      import "fmt"
      
      func main() {
          defer fmt.Println("one")
          defer fmt.Println("two")
          defer fmt.Println("three")
      }
      

      Если мы запустим программу, результат будет выглядеть следующим образом:

      Output

      three two one

      Обратите внимание, что порядок выполнения противоположен порядку вызова выражений defer. Это связано с тем, что каждое выражение defer помещается в стек поверх предыдущего и вызывается функцией в обратном порядке (Last In, First Out).

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

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

      main.go

      package main
      
      import (
          "fmt"
          "io"
          "log"
          "os"
      )
      
      func main() {
          if err := write("sample.txt", "This file contains some sample text."); err != nil {
              log.Fatal("failed to create file")
          }
      
          if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
              log.Fatal("failed to copy file: %s")
          }
      }
      
      func write(fileName string, text string) error {
          file, err := os.Create(fileName)
          if err != nil {
              return err
          }
          defer file.Close()
          _, err = io.WriteString(file, text)
          if err != nil {
              return err
          }
      
          return file.Close()
      }
      
      func fileCopy(source string, destination string) error {
          src, err := os.Open(source)
          if err != nil {
              return err
          }
          defer src.Close()
      
          dst, err := os.Create(destination)
          if err != nil {
              return err
          }
          defer dst.Close()
      
          n, err := io.Copy(dst, src)
          if err != nil {
              return err
          }
          fmt.Printf("Copied %d bytes from %s to %sn", n, source, destination)
      
          if err := src.Close(); err != nil {
              return err
          }
      
          return dst.Close()
      }
      

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

      Затем мы создаем файл назначения. После этого мы снова проверяем наличие ошибки при создании файла. Если она есть, мы возвращаем эту ошибку и выходим из функции. В противном случае мы также используем defer для функции Close() для закрытия файла. Теперь у нас имеется два выражения defer, которые должны вызываться при выходе из функции в ее области действия.

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

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

      Заключение

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



      Source link