Ссылки:
|
https://go.dev/play/ https://wiki.archlinux.org/title/Go_(%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9) https://github.com/b14esh/c-test/tree/main/02_lesson_golang |
Установка golang на linux
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
0. Установка # debian \ ubuntu apt update && apt upgrade && apt install golang # archlinux pacman -Sy go 1. Компиляция и запуск файлов Go — это компилируемый язык. Первый способ — через команду "go run". Эта команда компилирует исполняемый файл, запускает его и удаляет. Этим способом пользуются, когда нужно разово запустить небольшую программу на Go и забыть. Второй способ — через команду "go build". Она выполняет компиляцию и создает исполняемый файл в текущей директории. go build hello.go # Создаем бинарный файл hello.go В директории с файлом должен появиться новый файл hello. Можно запустить его как обычный исполняемый файл: ./hello Пример: go fmt hello-world.go go build hello-world.go go run build hello-world.go Кросс компиляция под разные платформы: go mod init hello # Создаем Go-модуль GOOS=windows GOARCH=386 go build -o hello_windows.exe # Исполняемый файл для Windows GOOS=windows GOARCH=amd64 go build -o hello_windows64.exe # Файл для Windows с архитектурой x64 GOOS=linux GOARCH=amd64 go build -o hello_linux_amd64 # Файл для linux с архитектурой x64 GOOS=linux GOARCH=arm go build -o hello_linux_arm # Файл для Linux на arm GOOS=darwin GOARCH=arm64 go build -o hello_mac_arm64 # для mac os |
Показать переменные go / рабочее пространство
|
go env В языке Go поиск программ и их зависимостей (например, import "пакет"), сначала выполняется в каталогах, прописанных в переменную $GOPATH, а затем - в переменной $GOROOT (путь установки go, по умолчанию /usr/lib/go). $GOPATH работает как $PATH и может содержать несколько записей. Это может быть полезно для отделения пакетов, скачанных через go get, от вашего кода, например GOPATH=$HOME/go:$HOME/mygo Создать само рабочее пространство: $ mkdir -p ~/go/src каталог ~/go/src предназначен для хранения исходных текстов проектов. При компиляции Go также создаст каталог bin для исполняемых файлов и pkg для кэша отдельных пакетов. Вы можете добавить ~/go/bin в переменную окружения $PATH для запуска установленных Go-программ: export PATH="$PATH:$HOME/go/bin" |
00. Первый пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
0. Открываем сайт https://go.dev/play/ 1. Вводим код package main import "fmt" func main () { fmt.Println("Hello, Go") } 3. Жмем format а потом run 4. Объяснение: Кнопка Format приводит код в порядок, создаются отступы, выставляются пробелы. Когда вы распространяете свой код, другие разработчики ожидают, что он будет оформлен в стандартном формате Go. Это означает стандартное форматирование отступов, пробелов и т. д., чтобы другим людям было проще читать ваш код. Если в других языках программирования разработчикам приходилось вручную переформатировать свой код, чтобы он соответствовал стилевому руководству, в Go достаточно выполнить команду go fmt, которая автоматически сделает все за вас. package main - Эта строка сообщает, что остальной код относится к пакету "main" import "fmt" - Означает что мы будем использовать код форматирования из пакета "fmt" Функция main играет особую роль - именно: func main () { fmt.Println("Hello, Go") - Эта строка выводит сообщение "Hello,Go!" } Для этого она вызывает функцию Println из пакета "fmt" Пакет представляет собой набор блоков кода, выполняющих похожие операции — например, форматирование строк или построение графических, изображений. Директива package задает имя пакета, частью которого станет код этого файла. В нашем случае используется специальный пакет main; это необходимо для того, чтобы код можно было запускать напрямую (чаще всего в терминале). При запуске программа Go ищет функцию с именем main и выполняет ее в первую очередь, поэтому-то мы и присвоили своей функции имя main. Структура типичного файла Go 1. Директива package. package main 2. Директива import. import "fmt" 3. Собственно код программы. function main{} Каждый файл Go должен начинаться с директивы package Каждый файл Go должен импортировать все пакеты, которые в нем используются Файлы Go должны импортировать только те пакеты, которые в них используются. (Это ускоряет компиляцию кода!) Компилятор Go ищет функцию с именем main, чтобы выполнить ее при запуске программы В Go учитывается регистр символов. fmt.Println — действительное имя, но имени fmt.println не существует Функция Println не принадлежит пакету main, поэтому перед вызовом функции необходимо указать имя пакета |
01. Вызов функций
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
В нашем примере вызывается функция Println из пакета fmt. Чтобы вызвать функцию, введите имя функции (в данном случае Println) и пару круглых скобок. Как и многие функции, Println может получать один или несколько аргументов — значений, с которыми должна работать функция. Аргументы перечисляются в круглых скобках после имени функции fmt.Println("First argument", "Second argument") Хотя функции Println и можно передать несколько аргументов, она может вызываться без них. Когда мы будем заниматься другими функциями, вы заметите, что они обычно должны получать строго определенное количество аргументов. Если аргументов будет слишком мало или слишком много, то вы получите сообщение об ошибке, в котором будет указано ожидаемое количество аргументов. В этом случае программу нужно будет исправить. package main import "fmt" func main () { fmt.Println("First argument", "Second argument") } |
02. Функция Println
|
При помощи функции Println можно узнать, как идет выполнение программы. Все аргументы, переданные этой функции, выводятся в терминале (а их значения разделяются пробелами) После вывода всех аргументов Println переходит на следующую строку в терминале. (Отсюда суффикс «ln» — сокращение от «line» — в конце имени.) package main import "fmt" func main() { fmt.Println("One", "Two", "Three") fmt.Println("Another line") fmt.Println("End") } |
03. Использование функций из других пакетов. / Возвращаемые значения функций.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
Весь код нашей первой программы является частью пакета main, но функция Println принадлежит пакету fmt (сокращение от «format»). Чтобы в программе можно было вызвать функцию Println, необходимо сначала импортировать пакет, содержащий эту функцию. // - комментарий package main import "fmt" // Пакет fmt необходимо импортировать чтобы вызвать его функции Println func main() { fmt.Println("Hello, Go!") // Сообщает что вызываемая функция является частью пакета fmt } После того как пакет будет импортирован, вы сможете вызывать функции из этого пакета. Для этого укажите имя пакета, поставьте точку (.) и введите имя нужной функции. fmt.Println() fmt - пакет .Println() - имя функции Следующий пример демонстрирует вызов функций из двух других пакетов. Так как мы собираемся импортировать несколько пакетов, то переходим на альтернативный формат инструкции, который позволяет перечислять в круглых скобках сразу несколько пакетов, по одному имени пакета в строке. package main import ( "math" "strings" ) func main() { math.Floor(2.75) strings.Title("head first go") } !!! Программа ничего не выводит! :) При вызове функции fmt.Println вам не нужно обмениваться с ней дополнительной информацией. Вы передаете Println одно или несколько выводимых значений и ожидаете вывод. Но иногда программа должна вызвать функцию и получить от нее дополнительные данные. Из-за этого в большинстве языков программирования функции имеют возвращаемые значения, которые вычисляются функциями и возвращаются на сторону вызова. Функции math.Floor и strings.Title относятся к числу функций, имеющих возвращаемое значение. Функция math.Floor получает число с плавающей точкой, округляет его до ближайшего меньшего целого и возвращает полученное число. А функция strings.Title получает строку, преобразует первую букву каждого слова к верхнему регистру и возвращает полученную строку. Чтобы увидеть результаты этих вызовов функций, необходимо взять возвращаемые значения и передать их fmt.Println: package main import ( "fmt" "math" "strings" ) func main() { fmt.Println(math.Floor(2.75)) fmt.Println(strings.Title("head first go")) } Что делает: строка: fmt.Println(math.Floor(2.75)) Функция fmt.Println вызывается для возвращаемого значения функции math.Floor. math.Floor получает число, округляет его в меньшую сторону и возвращает полученное значение. строка: fmt.Println(strings.Title("head first go")) Функция fmt.Println вызывается для возвращаемого значения функции strings.Title. strings.Title получает строку и возвращает новую строку, в которой все слова начинаются с буквы верхнего регистра. Результат: 2 Head First Go |
04. шаблон программы GO
|
vim test.go ----------- package main import "fmt" func main() { fmt.Println("hello world") } ----------- Проверка: Производим форматирование скрипта go fmt test.go Производим тестовый запуск go run test.go Производим сборку go build test.go Проверяем еще раз работоспособность |
05. Строки
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
В аргументах Println передавались строки. Строка представляет собой последовательность байтов, которые обычно представляют символы текста. Строки можно определять прямо в программе в виде строковых литералов: компилятор Go интерпретирует текст, заключенный в двойные кавычки, как строку Открывающая двойная кавычка "Hello, Go!" Закрывающая двойная кавычка Результат: Hello, Go! Некоторые управляющие символы, которые неудобно вводить с клавиатуры (символы новой строки, табуляции и т. д.), внутри строк могут представляться в виде служебных последовательностей: символа «обратный слеш», за которым следует другой символ (или символы). \n Символ новой строки \t Символ табуляции \" Двойная кавычка \\ Обратный слеш Пример 0: "Hello,\nGo!" Результат: Hello, Go! Пример 1: "Hello, \tGo!" Результат: Hello, Go! Пример 2: "\"Hello, Go!\"" "Hello, Go!" |
06. Руны
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
Если строки обычно используются для представления последовательностей символов, то руны в языке Go представляют отдельные символы. Строковые литералы заключаются в двойные кавычки ("), а рунные литералы записываются в одиночных кавычках ('). В программах Go могут использоваться практически любые символы любых мировых языков, потому что в Go для хранения рун используется стандарт Юникод. Руны хранятся в виде числовых кодов, а не в виде символов; если передать руну функции fmt.Println, то выведется числовой код, а не исходный символ. В рунных литералах (как и в строковых) можно использовать служебные последовательности для представления символов, которые неудобно вводить с клавиатуры для включения в программу. 'A' - 65 'B' - 66 'Ж' - 1174 '\t' - 9 '\n' - 10 '\\' - 92 Пример кода: package main import "fmt" func main() { fmt.Println('A', 'B', 'Ж', '\t', '\n', '\\') } |
07. Логические значения
|
Логические величины принимают всего два возможных значения: true или false. Они особенно удобны в условных командах, в которых выполнение блока кода зависит от того, истинно или ложно некоторое условие. Чуть позже разберем... |
08. Числа
|
Числа тоже можно определять прямо в программном коде. Это еще проще, чем определять строковые литералы: просто введите нужное число. Как вы вскоре увидите, в языке Go целые числа и числа с плавающей точкой интерпретируются как разные типы. Помните, что целое число можно отличить от числа с плавающей точкой по разделителю дробной части — точке. Пример: 42 Целое число. 3.1415 Число с плавающей точкой Пример в коде: package main import "fmt" func main() { fmt.Println(0, 3.1415) } |
09. Математические операции и сравнения
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
Основные математические операторы Go работают так же, как и в большинстве других языков. оператор + выполняет сложение оператор - выполняет вычитание оператор * — умножение оператор / — деление Пример кода: package main import "fmt" func main() { fmt.Println("\t1 + 2 =", 1+2, "\n\t5.4 - 2.2 =", 5.4-2.2, "\n\t3 * 4 =", 3*4, "\n\t7.5 / 5 =", 7.5/5) } При помощи операторов < и > можно сравнить два значения и проверить, какое из них больше другого. оператор == (два знака равенства) проверяет, что два значения равны оператор != проверяет, что два значения не равны оператор <= проверяет, что второе значение меньше или равно первому оператор >= проверяет, что второе значение больше или равно первому Результатом сравнения является логическое значение (true или false) package main import "fmt" func main() { fmt.Println("\t4 < 6 \t\t\t", 4 < 6, "\n\t4 > 6 \t\t\t", 4 > 6, "\n\t2 + 2 == 5 \t\t", 2+2 == 5, "\n\t2 + 2 !=5 \t\t", 2+2 != 5, "\n\t4 < = 6 \t\t", 4 <= 6, "\n\t4 >= 4\t\t\t", 4 >= 4) } |
10. Типы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
В предыдущем примере кода использовалась функция math.Floor, округляющая число с плавающей точкой с уменьшением, и функция strings.Title, преобразующая первую букву каждого слова в строке к верхнему регистру. Логично ожидать, что в аргументе функции Floor передается число, а в аргументе функции Title передается строка. Но что произойдет, если передать функции Floor строку, а функции Title число? Go выводит два сообщения об ошибках — по одному для каждого вызова функции, а программа даже не запускается! Объекты в окружающем мире часто можно разделить на типы в зависимости от того, для чего они используются. Машину или грузовик нельзя съесть на завтрак, а на омлете или чашке с кукурузными хлопьями не поедешь на работу — они предназначены для другого. В языке Go используется статическая типизация — это означает, что типы всех значений известны еще до запуска программы. Функции ожидают, что их аргументы относятся к конкретным типам, а их возвращаемые значения тоже имеют типы (которые могут совпадать или не совпадать с типами аргументов). Go — язык со статической типизацией. Если вы используете неправильный тип значения в неподходящем месте, Go сообщит вам об этом. Пример не правильного использование типов: package main import ( "fmt" "math" "strings" ) func main() { fmt.Println(math.Floor("head first go")) fmt.Println(strings.Title(2.75)) } Пример ошибки с неправильными типами: # command-line-arguments ./00_type_bad.go:10:25: cannot use "head first go" (type untyped string) as type float64 in argument to math.Floor ./00_type_bad.go:11:28: cannot use 2.75 (type untyped float) as type string in argument to strings.Title |
11. Узнаем типы значений
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
Чтобы узнать тип любого значения, передайте его функции TypeOf из пакета reflect. Давайте узнаем типы некоторых значений, которые уже встречались в примерах программ: package main import ( "fmt" "reflect" ) func main() { fmt.Println(reflect.TypeOf(42)) fmt.Println(reflect.TypeOf(3.1234)) fmt.Println(reflect.TypeOf(true)) fmt.Println(reflect.TypeOf("Hello world")) } Типы: int Целое число (не имеющее дробной части) float64 Число с плавающей точкой. Тип используется для хранения чисел, имеющих дробную часть. (Для хранения числа используются 64 бита данных, отсюда суффикс 64 в имени. Значения типа float64 обеспечивают очень высокую, хотя и не бесконечную точность.) bool Логическое значение (true или false) string Строка— последовательность данных, которые обычно представляют символы текста |
12. Объявление переменных
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
В языке Go переменная представляет собой область памяти, в которой хранится значение. Чтобы к переменной можно было обращаться по имени, используйте объявление переменной. Объявление состоит из ключевого слова var, за которым следует имя и тип значений, которые будут храниться в переменной. var quantity int var length, width float64 var customerName string После того как переменная будет объявлена, ей можно будет присвоить любое значение этого типа оператором = (один знак равенства): quantity = 2 customerName = "Damon Cole" В одной команде можно присвоить значения сразу нескольким переменным. Для этого перечислите имена переменных слева от = и такое же количество значений в правой части, разделяя их запятыми. length, width = 1.2, 2.4 После того как переменной будет присвоено значение, вы сможете использовать ее в любом контексте, где может использоваться исходное значение: package main import "fmt" func main() { var quantity int var length, width float64 var customerName string quantity = 4 length, width = 1.2, 2.4 customerName = "Damon Cole" fmt.Println(customerName) fmt.Println("has ordered", quantity, "sheets") fmt.Println("each with an area of") fmt.Println(length*width, "square meters") } Пример 2: package main import "fmt" func main() { var x1 int var x2 int var sumx int x1 = 500 x2 = 600 sumx = x1+x2 fmt.Println(x1, "+", x2, "=", sumx) } Если значение переменной известно заранее, можно объявить переменную и присвоить ей значение в одной строке: var quantity int = 4 var length, width float64 = 1.2, 2.4 var customerName string = "Damon Cole" Существующим переменным можно присваивать новые значения, но эти значения должны относиться к тому же типу. Статическая типизация в Go гарантирует, что переменной не будет случайно присвоено значение неподходящего типа. Если значение переменной присваивается одновременно с ее объявлением, тип переменной в объявлении обычно не указывают. Тип значения, присвоенного переменной, будет считаться типом этой переменной. var quantity = 4 var length, width = 1.2, 2.4 var customerName = "Damon Cole" fmt.Println(reflect.TypeOf(quantity)) fmt.Println(reflect.TypeOf(length)) fmt.Println(reflect.TypeOf(width)) fmt.Println(reflect.TypeOf(customerName)) Пример: package main import "fmt" func main() { var x1 int = 700 var x2 int = 800 var sumx = x1+x2 fmt.Println(x1, "+", x2, "=", sumx) } |
13. Нулевые значения
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
Если переменная объявляется без присваивания значения, то она будет содержать нулевое значение для этого типа. Для числовых типов нулевое значение равно 0. Пример: package main import "fmt" func main() { var myInt int var myFloat float64 fmt.Println(myInt, myFloat) } Но для других типов значение 0 будет недействительным, поэтому нулевое значение для этого типа будет отличаться. Скажем, для строковых переменных нулевым значением является пустая строка, а для переменных bool — значение false. Пример: package main import "fmt" func main() { var myString string var myBool bool fmt.Println(myString, myBool) } |
14. Короткие объявления переменных
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
Вместо того чтобы явно объявлять тип переменной и позднее присваивать ей значение оператором =, вы совмещаете эти две операции с помощью синтаксиса :=. Обычное объявление переменной выглядит вот так: var quantity int = 4 var length, width float64 = 1.2, 2.4 var customerName string = "Damon Cole" Короткие объявления переменных выглядит так: quantity := 4 length, width := 1.2, 2.4 customerName := "Damon Cole" Пример в коде: package main import "fmt" func main() { quantity := 4 length, width := 1.2, 2.4 customerName := "Damon Cole" fmt.Println(customerName) fmt.Println("has ordered", quantity, "sheets") fmt.Println("each with an area of") fmt.Println(length*width, "square meters") } Явно объявлять тип переменной не обязательно: тип значения, присвоенного переменной, становится типом этой переменной. Поскольку короткие объявления переменных очень удобны и компактны, они используются чаще обычных объявлений. Впрочем, время от времени вам будут встречаться обе формы, поэтому важно знать их. |
15. Возможные ошибки при добавлении переменных
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
00. хз что это было.... // не знаю что это было но назвав файл 05_vats_test.go он не запускался :( // go fmt 05_vats_test.go - выполняло форматирование // go run 05_vats_test.go - приводило к ошибке "go run: cannot run *_test.go files (05_vars_test.go)" // go build 05_vats_test.go - молча выполнялся но бинарник не производился // mv 05_vats_test.go 05_dz.go - но когда я переименовал файл, файл стал выполнятся, сборка также свершилась. Код для тестов: package main import "fmt" func main() { quantity := 4 length, width := 1.2, 2.4 customerName := "Damon Cole" fmt.Println(customerName) fmt.Println("has ordered", quantity, "sheets") fmt.Println("each with an area of") fmt.Println(length*width, "square meters") } 01. Пример, добавить второе объявление для той же переменной: quantity := 4 quantity := 4 Ошибка при выполнении: # command-line-arguments ./07_dd.go:7:11: no new variables on left side of := Переменную можно объявить только один раз. (Хотя при желании ей можно сколько угодно раз присваивать новые значения. Также можно объявлять другие переменные с тем же именем, но для этого они должны принадлежать другой области видимости. 02. Пример, убрать символ «:» из короткого объявления переменной: quantity = 4 Ошибка: # command-line-arguments ./07_dd.go:8:2: undefined: quantity ./07_dd.go:12:29: undefined: quantity Если вы забудете поставить двоеточие, конструкция рассматривается как присваивание, а не как объявление, а переменной, которая не была объявлена, нельзя будет присвоить значение. 03. Пример, присвоить строковое значение переменной int: quantity := 4 quantity = "a" Ошибка: # command-line-arguments ./07_dd.go:10:11: no new variables on left side of := ./07_dd.go:10:11: cannot use "a" (type untyped string) as type int in assignment Переменным могут присваиваться только значения того же типа. 04. Пример, количества переменных и значений не совпадают length, width := 1.2 Ошибка: # command-line-arguments ./07_dd.go:12:23: assignment mismatch: 2 variables but 1 values Вы должны предоставить значение для каждой переменной и переменную для каждого значения 05. Пример, удалить код, в котором используется переменная //fmt.Println(customerName) - например можно просто закомментировать Ошибка: # command-line-arguments ./07_dd.go:14:2: customerName declared but not used Все объявленные переменные должны использоваться в программе. Если вы удаляете код, в котором используется переменная, необходимо также удалить и объявление. |
16. Правила выбора имен для переменных
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
В Go существует один простой набор правил, применяемых к именам переменных, функций и типов: !!! Эти правила должны обязательно выполняться на уровне языка 00. Имя должно начинаться с буквы и может содержать любое количество дополнительных букв и цифр. 01. Если имя переменной, функции или типа начинается с буквы верхнего регистра, оно считается экспортируемым и может использоваться в других пакетах, кроме текущего. (Именно поэтому буква P в fmt.Println имеет верхний регистр: это нужно для того, чтобы его можно было использовать в main или любом другом пакете.) Если имя переменной/функции/типа начинается с буквы нижнего регистра, оно считается не экспортируемым. Такие имена доступны только в текущем пакете. !!! Но сообщество Go также соблюдает ряд дополнительных соглашений: 02. Если имя состоит из нескольких слов, каждое слово после первого должно начинаться с буквы верхнего регистра, и они должны следовать друг за другом без разделения пробелами: topPrice, RetryConnection и т. д. (Первая буква имени имеет верхний регистр только в том случае, если оно должно экспортироваться из пакета.) Этот стиль записи часто называется верблюжьим регистром, потому что буквы верхнего регистра напоминают горбы у верблюда. 03. Если смысл имени очевиден по контексту, в сообществе Go принято сокращать его: использовать i вместо index, max вместо maximum и т. д. Примеры: Нормально: sheetLength TotalUnits i нарушают соглашения: sheetlength - Остальные слова должны начинаться с буквы верхнего регистра! Total_Units - Допустимо, но слова должны записываться подряд! index - Хорошо бы заменить сокращением! !!! Только переменные, функции и типы, имена которых начинаются с буквы верхнего регистра, считаются экспортируемыми, то есть доступными за пределами текущего пакета. |
17. Преобразования
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
Пример не рабочего кода: package main import "fmt" func main() { var length float64 = 1.2 var width int = 2 fmt.Println("Area is", length*width) fmt.Println("length > width?", length > width) } При выполнении математических операций и операций сравнения в Go значения должны относиться к одному типу. Если же типы различаются, то при попытке выполнения кода вы получите сообщение об ошибке. Ошибка: # command-line-arguments ./02_badcode.go:8:31: invalid operation: length * width (mismatched types float64 and int) ./02_badcode.go:9:40: invalid operation: length > width (mismatched types float64 and int) Этот принцип действует и при присваивании новых значений переменным. Если тип присваиваемого значения не соответствует объявленному типу переменной, вы получите сообщение об ошибке. package main import "fmt" func main() { var length float64 = 1.2 var width int = 2 length = width fmt.Println(length) } Ошибка: # command-line-arguments ./03_badcode.go:8:8: cannot use width (type int) as type float64 in assignment Проблема решается преобразованием значений одного типа к другому типу. Для этого следует указать тип, к которому должно быть преобразовано значение, а за ним преобразуемое значение в скобках. var myInt int = 2 float64(myInt) В результате преобразования вы получите новое значение нужного типа. Пример кода: package main //import "fmt" import ( "fmt" "reflect" ) func main() { var myInt int = 2 fmt.Println(reflect.TypeOf(myInt)) fmt.Println(reflect.TypeOf(float64(myInt))) } Ошибка забыл добавить компонент reflect # command-line-arguments ./04_pre.go:7:14: undefined: reflect ./04_pre.go:8:14: undefined: reflect |
18. Преобразование переменных
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
Пример не рабочего кода: package main import "fmt" func main() { var length float64 = 1.2 var width int = 2 fmt.Println("Area is", length*width) fmt.Println("length > width?", length > width) } Исправим код: package main import "fmt" func main() { var length float64 = 1.2 var width int = 2 fmt.Println("Area is", length*float64(width)) fmt.Println("length > width?", length > float64(width)) } Теперь математические операции и операции сравнения работают правильно! Попробуем преобразовать значение int к типу float64 перед присваиванием переменной float64: var length float64 = 1.2 var width int = 2 length = float64(width) - Преобразование int к типу float64 перед присваиванием переменной float64 fmt.Println(length) Всегда держите в голове, как преобразования изменяют выходные значения. Например, переменные float64 могут хранить дробные значения, а переменные int — нет. Когда вы преобразовываете float64 в int, дробная часть просто отбрасывается! Это может внести путаницу в любые операции, выполняемые с результирующим значением. var length float64 = 3.75 var width int = 5 width = int(length) - В результате этого преобразования дробная часть теряется! fmt.Println(width) Но если действовать внимательно, вы поймете, что преобразования исключительно важны для работы с Go. С ними вы сможете совместно использовать несовместимые типы. Пример: var price int = 100 var taxRate float64 = 0.08 var tax float64 = float64(price) * taxRate fmt.Println(tax) |
19. Кратко о чем писал выше
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
go build - Компилирует файлы с исходным кодом в двоичные файлы go run - Компилирует и запускает программу без сохранения в исполняемом файле go fmt - Переформатирует исходные файлы с использованием стандартного форматирования Go go version - Выводит текущую версию Go - Пакет представляет собой группу взаимосвязанных функций и других блоков кода. - Прежде чем использовать функции из пакета в файле Go, необходимо импортировать этот пакет. - Строка — последовательность байтов, обычно представляющих символы текста. - Руна представляет отдельный символ текста. - Два самых распространенных числовых типа Go — int (для хранения целых чисел) и float64 (для хранения чисел с плавающей точкой). - Тип bool используется для хранения логических значений (true или false). - Переменная представляет собой блок памяти для хранения значения заданного типа. - Если переменной не присвоено значение, то она содержит нулевое значение для своего типа. Примеры нулевых значений: 0 для переменных int или float64, "" для строковых переменных. - Объявление переменной можно совместить с присваиванием ей значения при помощи короткого объявления переменной :=. - К переменной, функции или типу можно обращаться из кода других пакетов только в том случае, если ее имя начинается с буквы верхнего регистра. - Команда go fmt автоматически переформатирует исходные файлы по стандартам Go. Всегда выполняйте команду go fmt для любого кода, который вы собираетесь передавать другим разработчикам. - Команда go build компилирует исходный код Go в двоичный формат, который может выполняться компьютером. - Команда go run компилирует и выполняет программу без сохранения в исполняемом файле в текущем каталоге. Вызовы функций Функция представляет собой блок кода, который может вызываться в других местах программы. При вызове функции ей могут передаваться данные в аргументах. Типы У всех значений в Go имеется тип, который определяет, для чего могут использоваться эти значения. Математические операции и сравнения с разными типами запрещены, хотя при необходимости значение можно преобразовать к другому типу. В переменных Go могут храниться значения только того типа, с которым они были объявлены |
20. Вызов методов на примере пакета time
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
В языке Go можно определять методы: функции, связанные со значениями определенного типа. Методы Go похожи на связываемые с "объектами" методы других языков программирования, но в Go все проще. В пакете time определен тип Time, представляющий дату (год, месяц и день) и время (час, минуты, секунды и т. д.). Каждое значение time.Time содержит метод Year, который возвращает год. Приведенный ниже код использует этот метод для вывода текущего года: package main import ( "fmt" "time" //Необходимо импортировать пакет «time», чтобы использовать тип time.Time ) func main() { var now time.Time = time.Now() //Метод time.Now возвращает значение time.Time, представляющее текущую дату и время. var year int = now.Year() //У значений time.Time имеется метод Year, который возвращает текущий год. fmt.Println(year) } Функция time.Now возвращает новое значение Time для текущей даты и времени; это значение сохраняется в переменной now. Затем мы вызываем метод Year для значения, ссылка на которое хранится в now: now.Year() | | | Вызывает метод Year для значения time.Time Содержит значение time.Time Метод Yer возвращает целое значение года, которое выводится программой. Методы — это функции, связанные со значениями конкретного типа |
21. Вызов методов на примере пакета strings
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
Пакет strings содержит тип Replacer, который ищет подстроку в строке и заменяет каждое вхождение этой подстроки в другой строке. Следующий код заменяет каждый символ # в строке буквой o: package main import ( "fmt" "strings" ) func main() { broken := "G# r#cks!" replacer := strings.NewReplacer("#", "o") fixed := replacer.Replace(broken) fmt.Println(fixed) } Функция strings.NewReplacer получает аргументы — заменяемую строку ("#") и заменяющую строку ("o") — и возвращает значение strings.Replacer. Когда мы передаем строку методу Replace значения Replacer, то метод возвращает строку, в которой выполнена указанная замена офф топ: Значение. Имя метода. | | replacer.Replace(broken) now.Year() | | Значение. Имя метода. |
21. Комментарии
|
Самая распространенная форма комментариев обозначается двумя слешами (//). Все символы от // до конца строки рассматриваются как часть комментария. Комментарий // может занимать всю строку или следовать после кода. // Общее количество виджетов в системе. var TotalCount int // Должно быть целым числом. Более редкая форма комментариев занимает несколько строк. Блочные комментарии начинаются с /* и заканчиваются */, а весь текст между этими маркерами (включая символы новой строки) является частью комментария. /* Пакет widget включает все функции, используемые для работы с виджетами. */ |
22. Получение значения от пользователя
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
|
Пример кода: //Внимание: этот код не будет компилироваться в том виде, в котором он здесь приведен // pass_fail сообщает, сдал ли пользователь экзамен. package main import ( "bufio" "fmt" "os" ) func main() { fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) input := reader.ReadString('\n') - Возвращает текст, введенный пользователем до нажатия клавиши Enter. fmt.Println(input) - Вывод введенных данных. } Сначала нужно запросить данные у пользователя, и для вывода приглашения используется функция fmt.Print. (В отличие от Println, функция Print не переходит на новую строку в терминале после вывода сообщения; таким образом, запрос и введенные данные размещаются в одной строке.) Затем нам понадобится механизм чтения (получения и хранения) ввода из стандартного ввода программы, в который поступает весь ввод с клавиатуры. Строка reader := bufio. NewReader(os.Stdin) сохраняет bufio.Reader в переменной reader, которая сделает это за вас. Чтобы получить введенные данные от пользователя, мы вызываем метод ReadString для Reader. Методу ReadString требуется аргумент с руной (символом), отмечающей конец ввода. Мы хотим прочитать весь текст, введенный пользователем до нажатия Enter, поэтому ReadString передается руна новой строки. После того как от пользователя будут получены данные, мы просто выводим их. bufio.Reader - Пока достаточно знать, что это средство позволяет читать текст, введенный с клавиатуры. Возвращает новое значение bufio.Reader. | reader := bufio.NewReader(os.Stdin) | Reader читает данные из стандартного ввода (с клавиатуры). Возвращает данные, введенные пользователем, в виде строки | input := reader.ReadString('\n') | Будет прочитан весь текст до руны новой строки. Ошибка: 0. go build 00_input.go - при сборке # command-line-arguments ./00_input.go:14:8: assignment mismatch: 1 variable but reader.ReadString returns 2 values 2. go run 00_input.go - при запуске без сборки # command-line-arguments ./00_input.go:14:8: assignment mismatch: 1 variable but reader.ReadString returns 2 values 3. из документации / мы должны были увидеть: multiple-value reader.ReadString() in single-value context Мы пытаемся прочитать ввод с клавиатуры, но получаем сообщение об ошибке. Компилятор сообщает о проблеме в следующей строке кода: input := reader.ReadString('\n') В большинстве языков программирования функции и методы могут возвращать только одно значение, но в Go функция может возвращать сколько угодно значений. Чаще всего множественные возвращаемые значения в Go используются для возвращения дополнительного значения ошибки, по которому можно определить, не возникли ли проблемы во время выполнения функции или метода. Несколько примеров: bool, err := strconv.ParseBool("true") - Возвращает ошибку, если строку не удается преобразовать в логическое значение. file, err := os.Open("myfile.txt") - Возвращает ошибку, если файл не удалось открыть. response, err := http.Get("http://golang.org") - Возвращает ошибку, если страницу не удалось загрузить. И в чем проблема? Просто добавьте переменную для хранения ошибки и не используйте ее! Но Go не позволит объявить переменную, если она не используется в программе!!! input, err := reader.ReadString('\n') - не сработает !!! Ошибка "err declared and not used" Go требует, чтобы каждая объявленная переменная также использовалась где-то в программе. Если вы добавите переменную err и не проверите ее, программа не будет компилироваться. Неиспользуемые переменные часто указывают на ошибки в программе; это один из примеров того, как Go помогает вам выявлять и исправлять ошибки!а |
23. Вариант 1. Игнорировать возвращаемое значение ошибки
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
Если имеется значение, которое должно быть присвоено переменной, но вы не собираетесь его использовать, можно воспользоваться пустым идентификатором Go. Присваивание значения пустому идентификатору фактически приводит к тому, что оно теряется (а другим читателям вашего кода становится очевидно, что вы поступаете так намеренно). Чтобы использовать пустой идентификатор, просто введите символ подчеркивания ( _ ) в команде присваивания, где должно использоваться имя переменной. Попробуем использовать пустой идентификатор вместо обычной переменной err: // pass_fail сообщает, сдал ли пользователь экзамен. package main import ( "bufio" "fmt" "os" ) func main() { fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) input, _ := reader.ReadString('\n') fmt.Println(input) } |
24. Вариант 2. Обработка ошибки / пакет log
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
Игнорирование ошибок не является признаком небрежного кода. Из предыдущего примера кода: fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) input, _ := reader.ReadString('\n') | Возвращаемый признак ошибки игнорируется! fmt.Println(input) - Выводит значение, которое может быть недействительным! В данном случае при возникновении ошибки правильнее было бы предупредить пользователя и прервать выполнение программы. Пакет log содержит функцию Fatal, которая выполняет обе операции одновременно: вывод сообщения в терминале и остановку программы. (В этом контексте «фатальной» называется ошибка, «смертельная» для вашей программы.) Давайте избавимся от пустого идентификатора "_" и заменим его переменной err, чтобы ошибка снова сохранялась в программе. Затем воспользуемся функцией Fatal для вывода сообщения об ошибке и прерывания работы программы. Пример кода: // pass_fail сообщает, сдал ли пользователь экзамен. package main import ( "bufio" "fmt" "log" "os" ) func main() { fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') log.Fatal(err) fmt.Println(input) Но при запуске программы обнаружится новая проблема: Enter a grade: 22 2023/07/26 19:53:50 <nil> exit status 1 Такие функции и методы, как ReadString, возвращают значение ошибки nil, что по сути означает «здесь ничего нет». Другими словами, если переменная err равна nil, значит, ошибки не было. Но наша программа написана так, что она просто сообщает об ошибке nil! Что же нужно сделать, чтобы программа завершалась только в том случае, если значение переменной не равно nil? Для этого можно воспользоваться условными командами, в которых блок кода (одна или несколько команд, заключенных в фигурные скобки) выполняется только при истинности заданного условия. if 1 < 2 { fmt.Println("It's true!") } На этом пока стопе нужно разобраться с условными командами |
25. Условные команды
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
Выражение вычисляется, и если полученный результат равен true, то выполняется код в теле условного блока. Если же результат равен false, условный блок пропускается. if true { fmt.Println("I'll be printed!") } if false { fmt.Println("I won't!") } Как и многие другие языки, Go поддерживает множественное ветвление в условных командах. Такие команды записываются в форме if...else if...else. if grade == 100 { fmt.Println("Perfect!") } else if grade >= 60 { fmt.Println("You pass.") } else { fmt.Println("You fail!") } Условные команды используют логическое выражение (результат которого равен true или false), чтобы определить, должен ли выполняться содержащийся в них код. if 1 == 1 { fmt.Println("I'll be printed!") } if 1 >= 2 { fmt.Println("I won't!") } if 1 > 2 { fmt.Println("I won't!") } if 2 <= 2 { fmt.Println("I'll be printed!") } if 1 < 2 { fmt.Println("I'll be printed!") } if 2 != 2 { fmt.Println("I won't!") } Если код должен выполняться только в том случае, когда условие дает результат false, используйте ! — оператор логического отрицания. Этот оператор берет значение true и превращает его в false или же берет значение false и превращает его в true. if !true { fmt.Println("I won't be printed!") } if !false { fmt.Println("I will!") } Если код должен выполняться только в том случае, когда истинны оба условия, используйте оператор && («и»). А если он должен выполняться лишь тогда, когда истинно хотя бы одно из двух условий, используйте оператор || («или»). if true && true { fmt.Println("I'll be printed!") } if false || true { fmt.Println("I'll be printed!") } if true && false { fmt.Println("I won't!") } if false || false { fmt.Println("I won't!") } В: Другой язык программирования требует, чтобы условие команды if заключалось в круглые скобки. В Go это не обязательно? О: Нет. Более того, команда go fmt удалит все круглые скобки, добавленные вами, если только они не используются для определения порядка операций. |
26. Условная выдача фатальной ошибки (пакет log) / продолжение
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
Наша программа сообщает об ошибке и аварийно завершается, хотя данные с клавиатуры были успешно прочитаны. Сохраняет возвращаемое значение ошибки в переменной. | input, err := reader.ReadString('\n') log.Fatal(err) - Сообщаем о возвращаемом значении ошибки Напоминаю ошибка выглядела вот так: Enter a grade: 22 2023/07/26 19:53:50 <nil> exit status 1 Мы знаем, что если значение в переменной err равно nil, это говорит о том, что данные с клавиатуры были прочитаны успешно. Теперь, познакомившись с командами if, попробуем обновить код, чтобы сообщение об ошибке и завершение программы происходило только в том случае, если значение err не равно nil. Пример кода с обработкой ошибки: // Выводит строку которую ввели package main import ( "bufio" "fmt" "log" "os" ) func main() { fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } fmt.Println(input) } Вот еще один пример: // сообщает размер файла my1.txt package main import ( "fmt" "log" "os" ) func main() { fileInfo, err := os.Stat("my1.txt") if err != nil { log.Fatal(err) } fmt.Println(fileInfo.Size()) } |
27. Избегайте замещения имен
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
Пример кода: fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } Ранее говорили, что стараетесь избегать сокращений. А здесь переменной присваивается имя err вместо error! Называть переменную error не рекомендуется, потому что это приведет к замещению типа с именем error Объявляя переменную, проследите за тем, чтобы ее имя не совпадало с именами существующих функций, пакетов, типов или других переменных. Если такое имя уже существует во внешней области видимости (вскоре мы поговорим об областях видимости), ваша переменная заместит его, то есть будет перехватывать все обращения к нему. И часто это нежелательно. В следующем фрагменте объявляется переменная с именем int, которая замещает имя типа, переменная с именем append, которая замещает имя встроенной функции (функция append будет представлена в главе 6), а также переменная с именем fmt, которая замещает имя импортированного пакета. Эти имена создают путаницу, но сами по себе не порождают ошибок... Пример ошибок в коде package main import "fmt" func main() { var int int = 12 //Переменная с именем «int» замещает имя встроенного типа «int»! var append string = "minutes of bonus footage" //Переменная с именем «append» замещает имя встроенной функции «append»! var fmt string = "DVD" //Переменная с именем «fmt» замещает имя импортированного пакета «fmt»! var count int //int» теперь относится к переменной, объявленной выше, а не к числовому типу var languages = append([]string{}, "Español") //Имя «append» теперь обозначает переменную, а не функцию! fmt.Println(int, append, "on", fmt, languages) //Имя «fmt» теперь обозначает переменную, а не пакет } # command-line-arguments ./06_varerrorcode.go:3:8: imported and not used: "fmt" ./06_varerrorcode.go:9:6: int is not a type ./06_varerrorcode.go:10:24: cannot call non-function append (type string), declared at ./06_varerrorcode.go:7:6 ./06_varerrorcode.go:11:5: fmt.Println undefined (type string has no field or method Println) Чтобы не путаться самому и не путать коллег, старайтесь по возможности избегать замещения имен. В данном случае проблема решается простым выбором неконфликтующих имен для переменных: Исправили код: package main import "fmt" func main() { var count int = 12 var suffix string = "minutes of bonus footage" var format string = "DVD" var languages = append([]string{}, "Español") fmt.Println(count, suffix, "on", format, languages) } Вот почему при объявлении переменных, предназначенных для хранения ошибок, мы присваивали им имя err вместо error, мы хотели предотвратить замещение имени типа ошибки именем переменной. package main import "fmt" fmt.Print("Enter a grade: ") reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') // err а не error! if err != nil { log.Fatal(err) } Если вы все же назовете свою переменную error, возможно, ваш код будет работать. По крайней мере до того момента, как вы забудете, что имя типа ошибки было замещено, попробуете воспользоваться типом и получите вместо него переменную. Лучше не рисковать; используйте имя err для своих переменных со значениями ошибок. |
28. Преобразование строк в числа
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
|
Условные команды также могут быть использованы для проверки введенного значения. Давайте воспользуемся командой if/else для определения того, прошел ли пользователь экзамен или нет. Если введенный процент правильных ответов равен 60 и выше, переменной status присваивается строка "passing". В противном случае переменной будет присвоена строка "failing". Пример кода: package main import "fmt" func main() { fmt.Print("Enter a grade:") reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } if input >= 60 { status := "passing" } else { status := "failing" } } !!! Код не заработает в текущем варианте этой программы выдается ошибка компиляции. Дело в том, что ввод с клавиатуры читается как строка. Go может сравнивать числа только с другими числами, сравнить число со строкой не удастся. А прямого преобразования типа из строки в число не существует: float64("2.6") - Ошибка cannot convert "2.6" (type string) to type float64 Мы должны решить две задачи: 1. В конце строки input находится символ новой строки, появившийся в результате нажатия клавиши Enter в процессе ввода. Его необходимо удалить. 2. Остальные символы строки необходимо преобразовать в число с плавающей точкой. Удалить символ новой строки из конца входного текста несложно. Пакет strings содержит функцию TrimSpace, которая удаляет все символы-пропуски (символы новой строки, табуляции и обычные пробелы) в начале и в конце строки. s := "\t formerly surrounded by space \n" fmt.Println(strings.TrimSpace(s)) Таким образом, чтобы убрать символ новой строки из входной строки, следует передать ее TrimSpace, а затем снова присвоить возвращенное значение переменной input. input = strings.TrimSpace(input) После этого в строке input должно остаться только число, введенное пользователем. Мы воспользуемся функцией ParseFloat из пакета strconv, чтобы преобразовать его в значение float64. В аргументах передается преобразуемая строка | grade, err := strconv.ParseFloat(input, 64) | | | | Возможная ошибка Количество битов точности для результата Возвращаемые значения - flat64 Функции ParseFloat передается строка, которую необходимо преобразовать в число, а также количество битов точности для результата. Поскольку строка преобразуется в значение float64, мы передаем число 64. (Кроме float64, в Go также поддерживается менее точный тип float32, однако им лучше не пользоваться, если только у вас нет на это веских причин.) Функция ParseFloat преобразует число в строку и возвращает его в форме float64. Как и ReadString, она также имеет второе возвращаемое значение — значение ошибки. Оно должно быть равно nil, если только в ходе преобразования строки не возникли какие-то проблемы. (Например, переданная строка не может быть преобразована в число. Да и какой может быть числовой эквивалент у строки "hello"...) Оффтоп: Все эти «биты точности» сейчас не так уж важны. По сути это просто размер компьютерной памяти, используемой для хранения числа с плавающей точкой. Если вы уверены в том, что вам нужно число float64, всегда передавайте 64 во втором аргументе ParseFloat, и все будет хорошо. package main import ( "bufio" "log" "os" "strconv" "strings" "fmt" ) func main() { fmt.Print("Enter a grage: ") reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } input = strings.TrimSpace(input) grade, err := strconv.ParseFloat(input, 64) if err != nil { log.Fatal(err) } if grade >= 60 { status := "passing" } else { status := "failing" } fmt.Println("A grade of", grade, "is", status) } Сначала нужные пакеты включаются в директиву import. Мы добавляем код удаления символа новой строки из входного текста, после чего передаем ввод функции ParseFloat и сохраняем полученное значение float64 в новой переменной grade. Как и в случае с ReadString, мы проверяем, возвращает ли ParseFloat значение ошибки. В этом случае программа сообщает об ошибке и аварийно завершается. Наконец, мы обновляем условную команду, чтобы она проверяла число в grade, а не строку в input. На этом все ошибки, происходящие от сравнения строки с числом, должны быть исправлены. При запуске обновленной программы мы уже не получаем сообщение о несовпадении типов string и int. Но тут есть и еще ошибки :) Код Go делится на блоки (сегменты). Блоки обычно заключаются в фигурные скобки ({}) и могут существовать как на уровне файлов с исходным кодом, так и на уровне пакетов. Блоки могут вкладываться друг в друга. Тела функций и условных команд тоже являются блоками. Понимание этого — ключ к решению нашей проблемы с переменной status |
29. Блоки и область видимости переменной
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
Каждая объявленная переменная обладает областью видимости: частью кода, в которой она «видна». К объявленной переменной можно обращаться в любой точке ее области видимости, однако при попытке обратиться к ней за пределами этой области видимости вы получите сообщение об ошибке. Область видимости переменной состоит из блока, в котором она была объявлена, и всех блоков, вложенных в этот блок. Пример кода: package main import "fmt" var packageVar = "package" func main() { var functionVar = "function" if true { var conditionalVar = "conditional" fmt.Println(packageVar) //все еще в области видимости fmt.Println(functionVar) //все еще в области видимости fmt.Println(conditionalVar) //все еще в области видимости _ область видимости conditionalVar } //Область видимости conditionalVar fmt.Println(packageVar) //Все еще в области видимости fmt.Println(functionVar) //Все еще в области видимости fmt.Println(conditionalVar) //не определенно не в области видимости }//Область видимости functionVar) Ошибка: # command-line-arguments ./02_x123.go:17:14: undefined: conditionalVar Области видимости переменных в приведенном выше коде: - Областью видимости packageVar является весь пакет main. К packageVarможно обращаться в любой точке любой функции, определенной в пакете. - Областью видимости functionVar является вся функция, в которой объявлена переменная, включая блок if, вложенный в эту функцию. - Область видимости conditionalVar ограничивается блоком if. При попытке обратиться к conditionalVar после закрывающей фигурной скобки } блока if вы получите сообщение об ошибке, в котором говорится, что переменная conditionalVar не определена! |
30. Блоки и области видимости переменной:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
|
Пример кода: package main import ( "bufio" "log" "os" "strconv" "strings" "fmt" ) func main() { fmt.Print("Enter a grage: ") reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { log.Fatal(err) } input = strings.TrimSpace(input) grade, err := strconv.ParseFloat(input, 64) if err != nil { log.Fatal(err) } if grade >= 60 { status := "passing" } else { status := "failing" } fmt.Println("A grade of", grade, "is", status) } Ошибка: undefined: status Рассмотрим проблему: func main() { // Начало блока фунции // Omitting code up here... if grade >= 60 { status := "passing" //Блок if } else { status := "failing" // Блок else } fmt.Println("A grade of", grade, "is", status) // В этой области видимости переменная "status" НЕ ОПРЕДЕЛЕНА! //Конец блока функции Проблема решается перемещением объявления переменной status из блоков условных команд в блок функции. После этого переменная status будет находиться в области видимости как во вложенных условных блоках, так и в конце блока функции. Решение: Пример кода: package main import ( "bufio" "log" "os" "strconv" "strings" "fmt" ) func main() { //Функция «main» вызывается при запуске программы. fmt.Print("Enter a grage: ") //Запрашиваем у пользователя значение. reader := bufio.NewReader(os.Stdin) //Создаем bufio.Reader для чтения ввода с клавиатуры. input, err := reader.ReadString('\n') //Читает данные, вводимые пользователем до нажатия клавиши Enter // Если произошла ошибка, вывести сообщение и прервать работу программы. if err != nil { log.Fatal(err) } input = strings.TrimSpace(input) //Удалить символ новой строки из введенных данных. grade, err := strconv.ParseFloat(input, 64) //Преобразовать введенную строку в значение float64 (число). //Если произошла ошибка, вывести сообщение и прервать выполнение программы. if err != nil { log.Fatal(err) } var status string //Переменная «status» объявляется здесь, чтобы она находилась в области видимости в границах функции. //Если значение grade равно 60 и более, переменной status присваивается строка «passing». В противном случае переменной присваивается строка «failing». if grade >= 60 { status = "passing" // так как перемену мы задали выше тут мы уже используем присвоение } else { status = "failing" // так как перемену мы задали выше тут мы уже используем присвоение } fmt.Println("A grade of", grade, "is", status) } | | | и результат сдачи экзамена Выводим введенное значение... Вы можете запускать программу сколько угодно раз. Введите значение меньше 60, и получите сообщение о том, что экзамен не сдан. Введите значение больше 60, и программа сообщит, что экзамен сдан успешно. Кажется, все работает! |
31. Только одна переменная в коротком объявлении должна быть новой?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
|
Пример: input, err := reader.ReadString('\n') grade, err := strconv.ParseFloat(input, 64) В этом коде есть странность. До этого говорили, что переменную нельзя объявить дважды. Тем не менее переменная err встречается в двух разных коротких объявлениях! Действительно, если дважды объявить одно имя переменной в одной области видимости, компилятор выдаст сообщение об ошибке: a := 1 a := 2 Ошибка компиляции: # command-line-arguments ./05_x1.go:7:11: no new variables on left side of := Но если хотя бы одно имя переменной в коротком объявлении является новым, такая запись допустима. Новые имена переменных интерпретируются как объявление, а существующие — как присваивание. У этого специального подхода есть причина: многие функции Go возвращают несколько значений. Было бы неприятно объявлять отдельно все переменные только потому, что вы захотели повторно использовать одну из них. Вместо этого Go позволяет использовать короткие объявления переменных, даже если для одной из переменных в действительности выполняется присваивание Пример: Вариант с отдельными объявлениями всех переменных работает, но, к счастью, поступать так не обязательно... var a, b float64 var err error a, err = strconv.ParseFloat("1.23", 64) b, err = strconv.ParseFloat("4.56", 64) Можно просто воспользоваться синтаксисом короткого объявления переменных a, err := strconv.ParseFloat("1.23", 64) - Объявляем «a» и «err». b, err := strconv.ParseFloat("4.56", 64) - Объявляем «b» и присваиваем «err». fmt.Println(a, b, err) |
32. Генерация случайного числа / пакет («math/rand»)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
Пакет math/rand содержит функцию Intn, которая сгенерирует случайное число за нас, поэтому в программу следует импортировать math/rand. После этого можно будет вызвать функцию rand.Intn для генерирования случайного числа Пример кода: package main import ( "fmt" "math/rand" ) func main() { target := rand.Intn(100) + 1 fmt.Println(target) } Передайте число функции rand.Intn, и функция вернет случайное число в диапазоне от 0 до переданного числа. Другими словами, если передать аргумент 100, будет получено случайное число в диапазоне 0–99. Так как нам требуется число в диапазоне 1–100, остается прибавить 1 к полученному случайному значению. Результат сохраняется в переменной target. Пока мы ограничимся простым выводом переменной target. Попытавшись запустить программу в таком виде, вы получите случайное число. Однако вы будете получать одно и то же случайное число раз за разом! Чтобы получать разные случайные числа, необходимо передать значение функции rand.Seed. Тем самым вы «инициализируйте» генератор случайных чисел, то есть предоставляете значение, которое будет использоваться для генерирования других случайных чисел. Но если передавать одно и то же значение инициализации, то и случайные значения будут теми же и мы снова вернемся к тому, с чего начинали. Ранее было показано, что функция time.Now выдает значение Time, представляющее текущую дату и время. Его можно использовать для того, чтобы получать разное значение инициализации при каждом запуске программы. Пример кода: package main import ( "fmt" "math/rand" "time" ) func main() { seconds := time.Now().Unix() rand.Seed(seconds) target := rand.Intn(100) + 1 fmt.Println(target) } Функция rand.Seed ожидает получить целое число, поэтому передать ей значение Time напрямую не удастся. Вместо этого для Time следует вызвать метод Unix, который преобразует его в целое число. (А конкретно значение будет преобразовано в формат времени Unix — целое количество секунд, прошедших с 1 января 1970 года. Впрочем, запоминать это не нужно.) Это число передается rand.Seed. |
33. Путь импортирования пакета
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
Пакет math/rand содержит функцию Intn, которая сгенерирует случайное число за нас, поэтому в программу следует импортировать math/rand. После этого можно будет вызвать функцию rand.Intn для генерирования случайного числа Упоминая math/rand, мы имеем в виду путь импортирования пакета, а не его имя. Путь импортирования — всего лишь уникальная строка, которая идентифицирует пакет и используется в директиве import. После того как пакет будет импортирован, к нему можно обращаться по имени пакета. Для всех пакетов, которые использовались до сих пор, путь импортирования совпадал с именем пакета. Несколько примеров: Путь импортирования Имя пакета "fmt" fmt "log" log "strings" strings Однако путь импортирования и имя пакета могут различаться. Многие пакеты Go классифицируются по категориям — например, «сжатие» или «комплексные вычисления». По этой причине они часто группируются по префиксам пути импортирования (например, "archive/" или "math/"). (Их можно рассматривать как аналоги путей каталогов на жестком диске.) Несколько примеров: Путь импортирования Имя пакета "archive" archive "archive/tar" tar "archive/zip" zip "math" math "math/cmplx" cmplx "math/rand" rand Язык Go не требует, чтобы имя пакета было как-то связано с путем импортирования. Но по соглашению последний (или единственный) сегмент пути импортирования также используется в качестве имени пакета. Таким образом, для пути импортирования "archive" именем пакета также будет archive, а для пути импортирования "archive/zip" будет использоваться имя пакета zip. Именно по этой причине в директиве import используется путь "math/rand", а в функции main имя пакета — rand. |
34. Циклы / Цикл for
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
|
Пример кода: package main import "fmt" func main() { for x := 4; x <= 6; x++ { fmt.Println("x is now", x) } } Разберем: for x := 4; x <= 6; x++ { fmt.Println("x is now", x) } for - ключевое слово x:=4 - команда инициализации x < 6 - Условное выражение x++ - Операция приращения { - начало блока цикла }конец блока цикла fmt.Println("x is now", x) - тело блока цикла Циклы всегда начинаются с ключевого слова for. В одной из стандартных разновидностей циклов за for следуют три сегмента кода, которые управляют циклом: - Команда инициализации, обычно используемая для инициализации переменной. - Условное выражение, которое определяет, когда следует прервать выполнение цикла. - Оператор приращения, который выполняется после каждой итерации цикла. Команда инициализации часто используется для инициализации переменной; условное выражение обеспечивает выполнение цикла до того, как переменная достигнет определенного значения, и оператор приращения обновляет значение этой переменной. Например, в приведенном фрагменте переменная t инициализируется значением 3, условие обеспечивает выполнение цикла, пока t > 0, а оператор приращения уменьшает t на 1 при каждом выполнении цикла. В конечном итоге t уменьшается до 0 и цикл завершается. package main import "fmt" func main() { for t := 3; t > 0; t-- { fmt.Println(t) } fmt.Println("Blastoff!") } Операторы ++ и -- часто встречаются в командах приращения циклов. ++ увеличивает значение переменной на 1, а -- уменьшает его на 1. Примеры кода: x := 0 x++ fmt.Println(x) x++ fmt.Println(x) x-- fmt.Println(x) for x := 1; x <= 3; x++ { fmt.Println(x) } for x := 3; x >= 1; x-- { fmt.Println(x) } В языке Go также поддерживаются операторы присваивания += и -=. Они получают значение в переменной, добавляют или вычитают другое значение, а затем присваивают результат той же переменной. Примеры кода: x := 0 x += 2 fmt.Println(x) x += 5 fmt.Println(x) x -= 3 fmt.Println(x) Операторы += и -= также могут использоваться в циклах для изменения переменной на величину, отличную от 1. for x := 1; x <= 5; x += 2 { fmt.Println(x) } for x := 15; x >= 5; x -= 5 { fmt.Println(x) } Когда цикл завершается, выполнение программы продолжится с команды, следующей за блоком цикла. При этом цикл продолжает выполняться, пока условное выражение остается истинным. Этот факт может иметь нежелательные последствия; ниже приведены примеры циклов, которые выполняются бесконечно или не выполняются ни одного раза: бесконечный цикл: for x := 1; true; x++ { fmt.Println(x) } Цикл не выполнится никогда: for x := 1; false; x++ { fmt.Println(x) } Будьте осторожны! Цикл может выполняться бесконечно. В этом случае ваша программа никогда не остановится сама. Если это случится, в активном окне терминала нажмите клавишу Control одновременно с клавишей C, чтобы прервать выполнение программы. Операторы инициализации и приращения необязательны. При желании операторы инициализации и приращения в заголовке цикла for можно опустить, оставив только условное выражение (хотя вы должны проследить за тем, чтобы условие в какой-то момент становилось ложным, иначе в программе возникнет бесконечный цикл). x := 1 for x <= 3 { fmt.Println(x) x++ } x := 3 for x >= 1 { fmt.Println(x) x-- } |
35. Циклы и области видимости
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
Как и в случае с условными командами, область видимости любых переменных, объявленных в блоке цикла, ограничивается этим блоком (хотя команда инициализации, условное выражение и оператор приращения также могут считаться частью этой области видимости). Пример: for x := 1; x <= 3; x++ { y := x + 1 fmt.Println(y) //Остается в области видимости... } fmt.Println(y) //Ошибка: вне области видимости! (undefined: y) Ошибка: # command-line-arguments ./13_loop.go:11:14: undefined: y Пример: for x := 1; x <= 3; x++ { fmt.Println(x) //Остается в области видимости... } fmt.Println(x) //Ошибка: вне области видимости! (undefined: x) Ошибка: # command-line-arguments ./15_loop.go:9:14: undefined: x Как и в случае с условными командами, любые переменные, объявленные до начала цикла, находятся в области видимости в заголовке и блоке цикла и остаются в области видимости после выхода из цикла. var x int //Объявляется за пределами цикла for x = 1; x <= 3; x++ { //Объявлять x здесь не нужно, просто присвойте значение fmt.Println(x) //Остается в области видимости. } fmt.Println(x) //Остается в области видимости. |
36. Сломай и изучи!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
Пример кода: package main import "fmt" func main() { for x := 1; x <= 3; x++ { fmt.Println(x) } } 0. Добавить круглые скобки после ключевого слова for for (x := 1; x <= 3; x++) Ошибка: # command-line-arguments ./17_dz.go:7:9: syntax error: unexpected :=, expecting ) Что: Другие языки требуют, чтобы управляющая часть цикла for заключалась в круглые скобки. Однако язык Go не только этого не требует, но и запрещает. 1. Удалить : из команды инициализации x = 1 Ошибка: # command-line-arguments ./17_dz.go:9:6: undefined: x ./17_dz.go:10:15: undefined: x Что: Если только вы не присваиваете значение переменной, уже объявленной во внешней области видимости (а это бывает довольно редко), команда инициализации должна быть объявлением, а не присваиванием. 2. Удалить = из условного выражения x < 3 Выражение x<3 становится ложным , когда значение x становится равным 3 (тогда как выражение x<=3 все еще остается истинным). Таким образом, цикл будет вести отсчет только до 2. 3. Использовать противоположный оператор сравнения в условном выражении x >= 3 Так как условие будет ложным уже в самом начале цикла (x инициализируется 1, что меньше 3), цикл не будет выполнен ни одного раза 4. Заменить оператор приращения x++ на x-- Переменная x начинает отсчет с 1 (1, 0, -1, -2 и т. д.). Так как она никогда не станет больше 3, цикл будет выполняться бесконечно 5. Переместить команду fmt.Println(x) за пределы блока цикла Переменные, объявленные в команде инициализации или в блоке цикла, остаются в области видимости только в пределах блока цикла # command-line-arguments ./17_dz.go:15:15: undefined: x |
37. Пропуск частей цикла командами continue и break
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
В Go предусмотрены два ключевых слова для управления циклом. Первое — continue — осуществляет немедленный переход к следующей итерации цикла; при этом дальнейший код текущей итерации в блоке цикла пропускается. for x := 1; x <= 3; x++ { fmt.Println("before continue") continue fmt.Println("after continue") } В приведенном выше примере строка "after continue" никогда не выводится, потому что ключевое слово continue всегда выполняет переход к началу цикла — до того, как отработает второй вызов Println. Второе ключевое слово break приводит к немедленному выходу из цикла. Дальнейший код в блоке цикла не отрабатывается, другие итерации цикла не выполняются. Управление передается первой команде, следующей за циклом. for x := 1; x <= 3; x++ { fmt.Println("before break") break fmt.Println("after break") } fmt.Println("after loop") Здесь при первой итерации цикла выводится сообщение "before break", после чего команда break немедленно прерывает цикл; сообщение "after break" не выводится, и цикл не выполняется повторно (хотя без break он бы выполнился еще два раза). Управление передается команде, следующей за циклом. |
38. Комментарии (guess)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
|
//guess - игра, в которой игрок должен отгадать случайное число. package main import ( "bufio" // пакет отвечает за input keyboard "fmt" // вывод в консоль "log" // работает с ошибками err "math/rand" // отвечает за генерацию чисел "os" // использование возможностей ОС "strconv" // работа со строками, преобразование "strings" // работа со строками "time" // работает со временем, используется ) func main() { seconds := time.Now().Unix() //Получает текущую дату и время в виде целого числа. rand.Seed(seconds) // Инициализируем генератор случайных чисел. target := rand.Intn(100) + 1 // Генерируем целое число 1 от 100 fmt.Println("I've chosen a random number between 1 and 100.") fmt.Println("Can you guess it?") reader := bufio.NewReader(os.Stdin) // Создаем bufio.Reader для чтения ввода с клавиатуры. success := false // Настроить программу, чтобы по умолчанию выводилось сообщение о проигрыше. for guesses := 0; guesses < 10; guesses++ { fmt.Println("You have", 10-guesses, "guesses left.") fmt.Print("Make a guess: ") // Запрашиваем число. input, err := reader.ReadString('\n') // Прочитать данные, введенные пользователем до нажатия Enter. if err != nil { // Если произошла ошибка программа выводит сообщение и завершается log.Fatal(err) } input = strings.TrimSpace(input) // Удаляем символ новой строки. guess, err := strconv.Atoi(input) // Введенная строка преобразуется в целое число if err != nil { // Если произошла ошибка программа выводит сообщение и завершается log.Fatal(err) } if guess < target { // если введенное значение меньше загаданного, сообщить об этом. fmt.Println("Oops. Your guess was LOW") } else if guess > target { // Если введено сообщение больше загаданного, сообщить об этом fmt.Println("Oops.Your guss was HIGH") } else { // В противном случае введенное значение должно быть правильным... success = true //Предотвращает вывод сообщения о проигрыше. fmt.Println("Good job! You guessed it") break //Выход из цикла } } if !success { // Если переменная «success» равна false, сообщить игроку загаданное число. fmt.Println("Sorry, you diddn't guess my number. It was:", target) } } |
39. Проведем промежуточные итоги
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
0. Метод — разновидность функций, связываемых со значениями конкретного типа. 1. Go интерпретирует все символы от // до конца строки как комментарий и игнорирует их. 2. Многострочные комментарии начинаются с /* и завершаются */. Все символы между этими маркерами, включая символы новой строки, игнорируются. 3. Традиционно в начало любой программы включается комментарий, который объясняет, что делает программа. 4. В отличие от большинства языков программирования, Go допускает множественные возвращаемые значения из вызова функции или метода. 5. Одно из стандартных применений множественных возвращаемых значений — возвращение основного результата функции и второго значения, которое сообщает, произошла ли ошибка при вызове. 6. Чтобы проигнорировать значение без реального использования в программе, воспользуйтесь пустым идентификатором _. Пустой идентификатор может использоваться вместо любой переменной в любой команде присваивания. 7. Постарайтесь не присваивать переменным имена, совпадающие с именами типов, функций или пакетов; это приведет к замещению (переопределению) элемента с тем же именем 8. Функции, условные команды и циклы содержат блоки кода, заключенные в фигурные скобки {}. 9. Файлы и пакеты также образуют блоки, хотя содержащийся в них код и не заключается в фигурные скобки {}. 10. Область видимости переменной ограничивается блоком, в котором она определяется, а также всеми блоками, вложенными в этот блок. 11. Кроме имени, пакет может иметь путь импортирования, который должен указываться при импортировании. 12. Ключевое слово continue осуществляет переход к следующей итерации цикла. 13. Ключевое слово break полностью прерывает выполнение цикла. 14. Условные команды Условные команды обеспечивают выполнение блока кода только в случае истинности заданного условия. Вычисляется результат выражения, и если его результат равен true, то выполняется тело условного блока. Go поддерживает множественное ветвление в условиях. Такие команды записываются в форме if...else if...else. 15. Циклы Циклы предназначены для многократного выполнения блока кода. Одна из распространенных разновидностей циклов начинается с ключевого слова «for», за которым следует команда инициализации переменной; условное выражение, которое определяет, когда цикл должен прерваться; и оператор приращения, выполняемый после каждой итерации цикла. |
40. Функции вступление
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
Допустим есть код: /*Допустим, вы хотите вычислить, сколько краски потребуется для покрытия нескольких стен. Производитель указывает, что одного литра краски хватает на 10 квадратных метров. Следовательно, для определения количества литров необходимо вычислить площадь каждой стены, умножив ее ширину (в метрах) на высоту, а затем разделить результат на 10.*/ package main import "fmt" func main() { var width, height, area float64 //Вычислить расход краски для первой стены. width = 4.2 height = 3.0 area = width * height //Вычисляем площадь стены. fmt.Println(area/10.0, "liters needed") //Вычисляем, сколько краски понадобится для этой площади. //Сделать то же для этой площади. самое для второй стены. width = 5.2 height = 3.5 area = width * height fmt.Println(area/10.0, "liters needed") //Вычисляем, сколько краски понадобится для этой площади. } Такое решение работает, но у него есть пара недостатков: 0. Похоже, произведение вычисляется с небольшой погрешностью, поэтому выводимые значения выглядят немного странно. Хватило бы пары цифр в дробной части. 1. В этой версии присутствует большое количество повторяющегося кода. Если мы добавим новые стены, все станет еще хуже. |
41. Форматирование вывода функциями Printf и Sprintf
|
Числа с плавающей точкой в языке Go хранятся с высокой степенью точности. Если вам потребуется вывести такое число, результат может быть громоздким и неудобным: fmt.Println("About one-third:", 1.0/3.0) Для решения подобных проблем форматирования в пакете fmt имеется функция Printf (сокращение от «print, with formatting», то есть «вывод с форматированием»). Функция получает строку и вставляет в нее одно или несколько значений, отформатированных заданным способом. После этого функция выводит полученную строку. fmt.Printf("About one-third: %0.2f\n", 1.0/3.0) Функция Sprintf (также из пакета fmt) в целом похожа на Printf, но она возвращает отформатированную строку, а не выводит ее. resultString := fmt.Sprintf("About one-third: %0.2f\n", 1.0/3.0) fmt.Printf(resultString) |
42. Глаголы форматирования
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
|
В первом аргументе Printf передается строка, которая будет использоваться для форматирования вывода. Большая часть вывода форматируется так, как он выглядит в строке. Однако знаки процента (%) рассматриваются как начало глагола форматирования — части строки, которая будет заменена значением в определенном формате. Остальные аргументы определяют значения, которые будут форматироваться с этими глаголами. Глагол Глагол Значение Значение | | | | fmt.Printf("The %s cost %d cents each.\n", "gumballs", 23) fmt.Printf("That will be $%f please.\n", 0.23 * 5) | -------- Глагол | Значение Глагол Вывод %f Число с плавающей точкой %d Десятичное целое число %s Строка %t Логическое значение (true или false) %v Произвольное значение (подходящий формат выбирается на основании типа передаваемого значения) %#v Произвольное значение, отформатированное в том виде, в котором оно отображается в коде Go %T Тип переданного значения (int, string и т. п.) %% Знак процента (литерал) fmt.Printf("A float: %f\n", 3.1415) fmt.Printf("An integer: %d\n", 15) fmt.Printf("A string: %s\n", "hello") fmt.Printf("A boolean: %t\n", false) fmt.Printf("Values: %v %v %v\n", 1.2, "\t", true) fmt.Printf("Values: %#v %#v %#v\n", 1.2, "\t", true) fmt.Printf("Types: %T %T %T\n", 1.2, "\t", true) fmt.Printf("Percent sign: %%\n") Обратите внимание: в конец каждой форматной строки включается символ новой строки в виде служебной последовательности \n. Дело в том, что в отличие от Println, Printf не добавляет символ новой строки автоматически. Особого внимания заслуживает глагол формата %#v. Так как он выводит значения в том виде, в котором они отображаются в коде Go, %#v позволяет выводить значения, которые обычно остаются скрытыми в выводе. Например, в следующем примере %#v показывает пустую строку, символ табуляции и символ новой строки — все эти символы остаются невидимыми при выводе с %v. %v выводит все значения... …но только с %#v вы можете фактически их увидеть! fmt.Printf("%v %v %v", "", "\t", "\n") fmt.Printf("%#v %#v %#v", "", "\t", "\n") Форматирование значений ширины Глагол форматирования %f предназначен для чисел с плавающей точкой. Используем его для форматирования количества краски. fmt.Printf("%f liters needed\n", 1.8199999999999998) Предположим, вы хотите отформатировать данные в форме текстовой таблицы. Необходимо позаботиться о том, чтобы отформатированное значение занимало минимальное количество столбцов, а столбцы правильно выравнивались. Минимальная ширина может задаваться после знака процента в глаголе форматирования. Если аргумент, соответствующий этому глаголу, короче минимальной длины, значение дополняется пробелами до достижения минимальной ширины. Пример кода таблицы: fmt.Printf("%12s | %s\n", "Product", "Cost in Cents") fmt.Println("-----------------------------") fmt.Printf("%12s | %2d\n", "Stamps", 50) fmt.Printf("%12s | %2d\n", "Paper Clips", 5) fmt.Printf("%12s | %2d\n", "Tape", 99) Форматирование с дробными значениями ширины Минимальная ширина всего числа. | Ширина дробной части. | | %5.3f | | | Тип глагола форматирования. Начало спецификатора форматирования. Минимальная ширина всего числа включает дробную часть и точку-разделитель. Если она указана, то более короткие числа будут дополняться пробелами в начале до достижения указанной ширины. Если она не указана, то пробелы не добавляются. Ширина после точки определяет количество цифр в дробной части. Если выводимое число имеет больше разрядов, оно округляется (в большую или меньшую сторону) до заданного количества разрядов. fmt.Printf("%%7.3f: %7.3f\n", 12.3456) fmt.Printf("%%7.2f: %7.2f\n", 12.3456) fmt.Printf("%%7.1f: %7.1f\n", 12.3456) fmt.Printf("%%.1f: %.1f\n", 12.3456) fmt.Printf("%%.2f: %.2f\n", 12.3456) Последний формат "%.2f" позволяет взять число с плавающей точкой с произвольной точностью и округлить его до двух цифр в дробной части. (Также при этом число не дополняется лишними пробелами.) |
43. Функции
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
Объявление функций ключевое слово | Имя функции | | func sayHi() { - начало блока функции fmt.Println("Hi!") - тело блока функции } | Конец блока функции. После того как функция будет объявлена, вы сможете вызывать ее в пакете — для этого достаточно указать ее имя и пару круглых скобок. При этом будет выполнен код функции. Обратите внимание: при вызове sayHi перед именем функции не ставится имя пакета и точка. При вызове функции, определенной в текущем пакете, указывать имя пакета не нужно. (А вызов main.sayHi() приведет к ошибке компиляции.) Пример использования: package main import "fmt" func sayHi() { fmt.Println("Hi!") } func main() { for x := 4; x > 1; x-- { sayHi() } } Имена функций подчиняются тем же правилам, что и имена переменных: 1.Имя должно начинаться с буквы, за которой следует произвольное количество букв и цифр. (При нарушении этого правила выдается ошибка компиляции.) 2. Функция, имя которой начинается с буквы верхнего регистра, экспортируется и может использоваться вне текущего пакета. Если функция должна использоваться только внутри текущего пакета, начните ее имя с буквы нижнего регистра. 3. Имена, состоящие из нескольких слов, должны записываться в верблюжьем регистре. Допускается: double addPart - Если имя состоит из нескольких слов, используется верблюжий регистр. Publish - Если функция должна использоваться другими пакетами, ее имя начинается с буквы верхнего регистра. Недопустимо: 2times - Имя не может начинаться с цифры. addpart - Нарушает соглашения; следует использовать верблюжий регистр posts.publish - Недопустимо; к функции нельзя обращаться из другого пакета, если ее имя не начинается с буквы верхнего регистра. |
44. Объявление параметров функции
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
Если вы хотите, чтобы при вызове ваших функций передавались аргументы, необходимо объявить один или несколько параметров. Параметром называется переменная, локальная по отношению к функции, значение которой задается при вызове функции. Имя параметра | тип Имя тип | | | | func repeatLine(line string, times int) { for i := 0; i < times; i++ { fmt.Println(line) } Параметр — переменная, локальная по отношению к функции, значение которой задается при вызове функции. В объявлении функций в круглых скобках можно объявить один или несколько параметров, разделенных запятыми. Как и с любыми другими переменными, для каждого объявляемого параметра должно быть указано имя, за которым следует тип (float64, bool и т. д.). Если функция имеет определенные параметры, нужно будет передать соответствующий набор аргументов при ее вызове. Когда функция запускается, каждому параметру присваивается копия значения в соответствующем аргументе. Эти значения параметров затем используются в функциональном блоке кода Пример: package main import "fmt" func repeatLine(line string, times int) { for i := 0; i < times; i++ { fmt.Println(line) } } func main() { repeatLine("helo", 10) } |
45. И вот мы научились использовать функции исправим код про краску:
|
package main import "fmt" func paintNeeded(width float64, height float64) { area := width * height fmt.Printf("%.2f liters needed\n", area/10.0) } func main() { paintNeeded(4.2, 3.0) paintNeeded(5.2, 3.5) paintNeeded(5.0, 3.3) |
46. Функции и области видимости переменных
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
Функция paintNeeded объявляет переменную area в блоке функции: func paintNeeded(width float64, height float64) { area := width * height // Объявляем переменную area fmt.Printf("%.2f liters needed\n", area/10.0) // Обращение к переменной area } Как и в случае с блоками условных команд и циклов, переменные, объявленные в блоке функции, остаются в области видимости только в границах блока функции. Таким образом, если вы попытаетесь обратиться к переменной area вне функции paintNeeded, компилятор сообщит об ошибке: func paintNeeded(width float64, height float64) { area := width * height fmt.Printf("%.2f liters needed\n", area/10.0) } func main() { paintNeeded(4.2, 3.0) fmt.Println(area) } Ошибка: # command-line-arguments ./11_func_err.go:11:14: undefined: area Но как и в случае с блоками условных команд и циклов переменные, объявленные за пределами функции, будут находиться в области видимости внутри этого блока. Это означает, что переменную можно объявить на уровне пакета и обращаться к ней из любой функции в этом пакете. package main import "fmt" var metersPerLiter float64 // Если переменная объявляется на уровне пакета... func paintNeeded(width, height float64) float64 { area := width * height return area / metersPerLiter //здесь она остается в области видимости. } func main() { metersPerLiter = 10.0 //и здесь остается в области видимости. fmt.Printf("%.2f", paintNeeded(4.2, 3.0)) } |
47. Возвращаемые значения функций
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
|
Допустим, мы хотим вывести количество краски, необходимой для покрытия всех стен, которые мы собираемся покрасить. Сделать это с текущей версией функции paintNeeded не удастся; она лишь выводит результат, а потом отбрасывает его! func paintNeeded(width float64, height float64) { area := width * height fmt.Printf("%.2f liters needed\n", area/10.0) //Выводит расход краски, но вычисленное значение будет потеряно! } Итак, пересмотрим функцию paintNeeded, чтобы она возвращала значение. Тогда при ее вызове можно будет вывести полученное значение, провести дополнительные вычисления или сделать что-то еще. Функция всегда возвращает значение конкретного типа (и только этого типа). Чтобы объявить, что функция возвращает значение, добавьте тип возвращаемого значения после параметров в объявлении функции. Затем добавьте в блок функции ключевое слово return, за которым следует возвращаемое значение. Тип возвращаемого значения. | func double(number float64) float64 { return number * 2 } | ---------- | | | Возвращаемое значение. | Ключевое слово return. После этого результат вызова функции можно присвоить переменной, передать другой функции или сделать что-то еще. package main import "fmt" func double(number float64) float64 { return number * 2 } func main() { dozen := double(6.0) //Возвращаемое значение присваивается переменной fmt.Println(dozen) fmt.Println(double(4.2)) //Возвращаемое значение передается другой функции. } При выполнении команды return функция немедленно возвращает управление, а следующий за ней код не выполняется. Ее можно использовать в сочетании с командой if для выхода из функции в том случае, если выполнение остального кода стало бессмысленным (из-за ошибки или другого условия). func status(grade float64) string { if grade < 60.0 { return "failing" // Если экзамен провален, немедленно вернуть. } return "passing" // Выполняется только в том случае, если grade >= 60. } func main() { fmt.Println(status(60.1)) fmt.Println(status(59)) } Это означает, что при включении команды return, не являющейся частью блока if, какой-то код может не выполняться ни при каких условиях. Такая ситуация почти наверняка свидетельствует об ошибке в коде, поэтому Go помогает обнаруживать подобные ситуации: компилятор требует, чтобы любая функция с объявленным возвращаемым типом завершалась командой return. Если функция завершается любой другой командой, это приведет к ошибке компиляции. Пример: func double(number float64) float64 { return number * 2 //Функция всегда должна завершаться здесь... fmt.Println(number * 2) //А эта строка никогда не должна выполняться! } Ошибка: # command-line-arguments ./16_err_return.go:8:1: missing return at end of function Ошибка компиляции произойдет и в том случае, если тип возвращаемого значения не соответствует объявленному возвращаемому типу. func double(number float64) float64 { //Должно возвращаться число с плавающей точкой... return int(number * 2) //...а возвращается целое число! } Ошибка: # command-line-arguments ./17_err_return.go:4:12: cannot use int(number * 2) (type int) as type float64 in return argument |
48. Использование возвращаемого значения в программе
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
Изменим функцию paintNeeded так, чтобы она возвращала необходимый объем. Возвращаемое значение будет использоваться в функции main как для вывода расхода краски для текущей стены, так и для обновления переменной total, в которой накапливается общий расход краски. package main import "fmt" func paintNeeded(width float64, height float64) float64 { //объявляет, что paintNeeded возвращает число с плавающей точкой area := width * height return area / 10.0 //Функция возвращает расход краски вместо того, чтобы выводить его. } func main() { var amount, total float64 //Объявляем переменные для хранения расхода краски для текущей сметы, а также для общего расхода по всем сменам. amount = paintNeeded(4.2, 3.0) //Вызываем paintNeeded и сохраняем возвращаемое значение fmt.Printf("%0.2f liters needed\n", amount) //Выводим расход для первой сметы total += amount //Прибавляем расход для текущий смены к total amount = paintNeeded(5.2, 3.5) fmt.Printf("%0.2f liters\n", total) total += amount fmt.Printf("Total: %0.2f litters\n", total) //Выводим общий расход по всем сменам } |
49. Ломаем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
Пример кода: package main import "fmt" func paintNeeded(width float64, height float64) float64 { area := width * height return area / 10.0 func main() { var amount, total float64 amount = paintNeeded(4.2, 3.0) fmt.Printf("%0.2f liters needed\n", amount) total += amount amount = paintNeeded(5.2, 3.5) fmt.Printf("%0.2f liters\n", total) total += amount fmt.Printf("Total: %0.2f litters\n", total) } 0. Удалить команду return: func paintNeeded(width float64, height float64) float64 { area := width * height //return area / 10.0 } Ошибка: # command-line-arguments ./19_break.go:8:1: missing return at end of function Если функция объявляет возвращаемый тип, Go требует, чтобы она содержала команду return 1. Добавить строку после команды return: Добавить строку после команды return: func paintNeeded(width float64, height float64) float64 { area := width * height return area / 10.0 fmt.Println(area / 10.0) } Ошибка: # command-line-arguments ./19_break.go:10:1: missing return at end of function Если функция объявляет возвращаемый тип, Go требует, чтобы ее последней командой была команда return 2. Удалить объявление возвращаемого типа: func paintNeeded(width float64, height float64) /*float64*/ { area := width * height return area / 10.0 } Ошибка: # command-line-arguments ./19_break.go:9:2: too many arguments to return have (float64) want () ./19_break.go:14:22: paintNeeded(4.2, 3) used as value ./19_break.go:17:22: paintNeeded(5.2, 3.5) used as value Go не позволяет вернуть значение, тип которого не был объявлен 3. Изменить тип возвращаемого значения: func paintNeeded(width float64, height float64) float64 { area := width * height return int(area / 10.0) } Ошибка: # command-line-arguments ./19_break.go:11:12: cannot use int(area / 10) (type int) as type float64 in return argument Go требует, чтобы тип возвращаемого значения соответствовал объявленному типу |
50. Функции paintNeeded нужна обработка ошибок
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
Ваша функция paintNeeded отлично работает... чаще всего. Но один из пользователей недавно случайно передал ей отрицательное число и получил отрицательный расход краски! Пример: func main() { amount := paintNeeded(4.2, -3.0) fmt.Printf("%0.2f liters needed\n", amount) } func paintNeeded(width float64, height float64) float64 { area := width * height return area / 10.0 } Похоже, функция paintNeeded и не подозревает, что переданный ей аргумент недействителен. Она просто идет напролом, использует этот аргумент в своих вычислениях и возвращает недействительный результат. Это создает проблемы — даже если вы знаете, где можно приобрести отрицательный объем краски, захочется ли вам использовать такую краску у себя дома? Необходимо обнаруживать недопустимые значения аргументов и сообщать об ошибке. Ранее были продемонстрированы функции, которые в дополнение к основному возвращаемому значению также возвращают второе значение — признак ошибки. Например, функция strconv.Atoi пытается преобразовать строку в целое число. Если преобразование прошло успешно, возвращается значение ошибки nil, означающее, что программа может продолжать работу. Но если значение ошибки не равно nil, значит, строку невозможно преобразовать в число. В таком случае мы решили вывести значение ошибки и прервать выполнение программы. Пример: guess, err := strconv.Atoi(input) //Входная строка преобразуется в целое число if err != nil { //Если обнаружена ошибка, вывести сообщение и прервать работу программы log.Fatal(err) } Если мы хотим, чтобы при вызове функции paintNeeded происходило то же самое, необходимо реализовать две возможности: - Создание значения, представляющего ошибку. - Возвращение дополнительного значения из функции paintNeeded. Нужно сначала разобраться с пакетом errors. |
51. Ошибки и пакет erros
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
Значение ошибки представляет собой любое значение с методом Error, который возвращает строку. Чтобы создать такое значение, проще всего передать строку функции New из пакета errors — функция создаст и вернет новое значение ошибки. Если вызвать для полученного значения метод Error, вы получите строку, переданную errors.New Пример: package main import ( "errors" "fmt" ) func main() { err := errors.New("height can't be negative") //Создаем новое значение ошибки fmt.Println(err.Error()) //Возвращает сообщение об ошибке } Но если значение ошибки передается функции из пакета fmt или log, скорее всего, вам не нужно будет вызвать его метод Error. Функции пакетов fmt и log были написаны так, что они проверяют, содержит ли переданное им значение метод Error, и если содержит — выводят возвращаемое значение Error. err := errors.New("height can't be negative") fmt.Println(err) //Выводит сообщение об ошибке. log.Fatal(err) //Тоже выводит сообщение об ошибке, после чего завершает программу. Но если значение ошибки передается функции из пакета fmt или log, скорее всего, вам не нужно будет вызвать его метод Error. Функции пакетов fmt и log были написаны так, что они проверяют, содержит ли переданное им значение метод Error, и если содержит — выводят возвращаемое значение Error. err := errors.New("height can't be negative") fmt.Println(err) //Выводит сообщение об ошибке log.Fatal(err) //Тоже выводит сообщение об ошибке, после чего завершает программу. Если вам потребуется отформатировать числа или другие значения для использования в сообщениях об ошибках, воспользуйтесь функцией fmt.Errorf. Функция вставляет значения в форматную строку так же, как это делает fmt.Printf или fmt.Sprintf, но вместо вывода или возвращения строки она возвращает значение ошибки. err := fmt.Errorf("a height of %0.2f is invalid", -2.33333) fmt.Println(err.Error()) fmt.Println(err) |
52. Объявление нескольких возвращаемых значений
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
|
Чтобы объявить функцию с несколькими возвращаемыми значениями, заключите типы возвращаемых значений во второй набор круглых скобок в объявлении функции (после круглых скобок с параметрами), разделяя их запятыми. Круглые скобки вокруг возвращаемых значений можно опустить, если функция возвращает всего одно значение, но с несколькими возвращаемыми значениями они обязательны. package main import "fmt" func manyReturns() (int, bool, string) { return 1, true, "hello" } func main() { myInt, myBool, myString := manyReturns() fmt.Println(myInt, myBool, myString) } Вы также можете задать имя для каждого возвращаемого значения (по аналогии с именами параметров), если это поможет лучше понять их предназначение. Именованные возвращаемые значения в основном служат документацией для программистов, читающих код package main import ( "fmt" "math" ) func floatParts(number float64) (integerPart int, fractionalPart float64) { wholeNumber := math.Floor(number) return int(wholeNumber), number - wholeNumber } func main() { cans, remainder := floatParts(1.26) fmt.Println(cans, remainder) } |
53. Использование множественных возвращаемых значений с функцией paintNeeded
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
Функция может возвращать несколько значений любых типов. Впрочем, чаще всего эта возможность используется для возвращения основного значения, за которым следует дополнительное значение, указывающее, обнаружила ли функция ошибку. Дополнительному значению обычно присваивается nil, если выполнение прошло без проблем, или значение ошибки, если возникла ошибка. Мы также будем следовать этим соглашениям в своей функции paintNeeded. В объявлении функции будет указано, что она возвращает два значения, float64 и error. (Значения ошибок имеют тип error.) В блоке функции мы прежде всего проверяем параметры. Если хотя бы один из параметров width или height меньше 0, функция возвращает расход краски 0 (это значение бессмысленно, но что-то надо вернуть) и значение ошибки, сгенерированное вызовом fmt.Errorf. Проверка ошибок в начале функции позволяет легко пропустить остальной код функции вызовом return при возникновении проблем. Если значения параметров допустимы, мы переходим к вычислению и возвращению расхода краски, как и прежде. В коде функции встречается только одно отличие: наряду с расходом краски возвращается второе значение nil, указывающее на отсутствие ошибок. package main import "fmt" func paintNeeded(width float64, height float64) (float64, error) { if width < 0 { return 0, fmt.Errorf("a width of %0.2f is invalid", width) //Если ширина имеет недопустимое значение, вернуть 0 и признак ошибки. } if height < 0 { return 0, fmt.Errorf("a height of %0.2f is invalid", height) //Если высота имеет недопустимое значение, вернуть 0 и признак ошибки. } area := width * height return area / 10.0, nil //Возвращает расход краски и значение «nil», которое указывает на отсутствие ошибок. } func main() { amount, err := paintNeeded(4.2, -3.0) fmt.Println(err) //Выводим значение ошибки (или «nil», если ошибки не было). fmt.Printf("%0.2f liters needed\n", amount) } |
54. Всегда обрабатывайте ошибки!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
При передаче paintNeeded недопустимого аргумента вы получаете значение ошибки, которое выводится для просмотра пользователем. Но при этом мы также получаем (недействительный) расход краски, который тоже выводится программой! func main() { amount, err := paintNeeded(4.2, -3.0) //err Этой переменной присваивается значение ошибки. // amount Присваивается 0 (бессмысленное значение). fmt.Println(err) // Выводит ошибку fmt.Printf("%0.2f liters needed\n", amount) // Выводит бессмысленное значение! } Наряду со значением ошибки функция также обычно должна возвращать основное значение. Но любые другие значения, возвращаемые вместе со значением ошибки, следует считать ненадежными и игнорировать. Когда вы вызываете функцию, возвращающую значение ошибки, прежде всего необходимо убедиться в том, что это значение равно nil. Если значение отлично от nil, значит, в программе возникла ошибка, которую необходимо обработать. Как именно должна обрабатываться ошибка — зависит от ситуации. Возможно, в случае с функцией paintNeeded лучше всего будет пропустить текущее вычисление и продолжить выполнение программы: func main() { amount, err := paintNeeded(4.2, -3.0) if err != nil { //Если значение ошибки не равно nil, в программе возникли проблемы... fmt.Println(err) //...поэтому выводим ошибку. } else { // В противном случае значение ошибки будет равно nil... fmt.Printf("%0.2f liters needed\n", amount) //и можно спокойно вывести полученный расход краски. } // Дальнейшие вычисления... } Но в такой короткой программе вместо этого можно вызвать log.Fatal, чтобы вывести сообщение об ошибке и прервать выполнение. func main() { amount, err := paintNeeded(4.2, -3.0) if err != nil { //Если значение ошибки не равно nil, в программе возникли проблем... log.Fatal(err) //...выводим ошибку и прерываем выполнение программы. } fmt.Printf("%0.2f liters needed\n", amount) .Этот код никогда не будет выполняться при возникновении ошибки. } Важно помнить, что возвращаемое значение всегда следует проверять, чтобы знать, произошла ли ошибка. А что делать с ошибкой, зависит от вас! |
55. Ломаем и изучаем
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
|
Пример кода: package main import ( "fmt" "math" ) func squareRoot(number float64) (float64, error) { if number < 0 { return 0, fmt.Errorf("can't get square root of negative number") } return math.Sqrt(number), nil } func main() { root, err := squareRoot(-9.3) if err != nil { fmt.Println(err) } else { fmt.Printf("%0.3f", root) } } 0. Удалить один из аргументов return: return math.Sqrt(number) // например nil Ошибка: # command-line-arguments ./03_prim.go:18:2: not enough arguments to return have (float64) want (float64, error) Количество аргументов return всегда должно соответствовать количеству возвращаемых значений в объявлении функции. 1. Удалить одну из переменных, которым присваиваются возвращаемые значения: root := squareRoot(-9.3) // удалили err Ошибка: # command-line-arguments ./03_prim.go:21:7: assignment mismatch: 1 variable but squareRoot returns 2 values ./03_prim.go:23:5: undefined: err ./03_prim.go:24:15: undefined: err Если вы используете одно из возвращаемых значений функции, Go требует, чтобы вы использовали их все. 2. Удалить код, использующий одно из возвращаемых значений: root, err := squareRoot(-9.3) //if err != nil { //fmt.Println(err) //} else { fmt.Printf("%0.3f", root) //} Ошибка: # command-line-arguments ./05_dz.go:20:7: assignment mismatch: 1 variable but squareRoot returns 2 values Go требует, чтобы каждая объявленная переменная использовалась в программе. И эта особенность весьма полезна для работы с возвращаемыми значениями, потому что она предотвращает случайное игнорирование ошибок. |
56. В параметрах функций хранятся копии аргументов
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
Как уже говорилось, при вызове функции с объявленными параметрами необходимо передать аргументы при вызове. Значение каждого аргумента копируется в соответствующую переменную-параметр. (Этот механизм в языках программирования иногда называют «передачей по значению».) Во многих случаях этого достаточно. Но если вы хотите передать значение переменной функции, чтобы функция каким-то образом изменила его, возникает проблема. Функция может изменить только копию значения параметра, но не оригинал. Таким образом, любые изменения, внесенные внутри функции, не будут видны за ее пределами! Ниже приведена обновленная версия функции double, приведенной выше. Она получает число, умножает его на 2 и выводит результат. (Функция использует оператор *= , который работает аналогично += , но умножает значение из переменной вместо того, чтобы выполнять сложение.) package main import "fmt" func main() { amount := 6 //Аргумент передается функции double(amount) } func double(number int) { //В параметрах сохраняется копия аргумента number *= 2 fmt.Println(number) //Выводит удвоенное значение } Но предположим, вы хотите переместить команду вывода удвоенного значения из функции double в функцию, из которой она вызывается. Такое решение работать не будет, потому что double изменяет только копию значения. При попытке вывести значение в вызывающей функции будет выведено исходное значение, а не удвоенное! package main import "fmt" func main() { amount := 6 double(amount) //Функция передает аргумент fmt.Println(amount) //Выводится исходное значение } func double(number int) { //Параметру присваивается копия аргумента number *= 2 // Измен } Как же добиться того, чтобы функция изменяла исходное значение, хранящееся в переменной, вместо его копии? Чтобы понять, как это делается, необходимо познакомиться с указателями. |
57. Указатели
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
Оператор & (амперсанд) используется в Go для получения адреса переменной. Например, следующий код инициализирует переменную, сначала выводит ее значение, а затем адрес переменной... Пример кода: package main import "fmt" func main() { amount := 6 fmt.Println(amount) //Выводит значение переменной fmt.Println(&amount) //Выводит адрес переменной } вывод кода: 6 //Значение переменной. 0x400011c010 //Адрес переменной. Адрес можно получить для переменной любого типа. Обратите внимание, все переменные имеют разные адреса. Еще пример кода: package main import "fmt" func main() { var myInt int fmt.Println(&myInt) var myFloat float64 fmt.Println(&myFloat) var myBool bool fmt.Println(&myBool) } Вывод кода: 0x40000ba010 0x40000ba018 0x40000ba030 И что же собой представляют эти «адреса»? Чтобы найти конкретный дом в плотно застроенном городе, вам нужно знать его адрес... Память, выделяемая компьютером программе, так же переполнена, как и городские улицы. Она забита значениями переменных: логическими значениями, целыми числами, строками и т. д. Зная адрес переменной, вы сможете воспользоваться им для получения значения, хранящегося в переменной. Адрес | 0x1040a100 0x1040a108 0x1040a110 0x1040a118 0x1040a120 0x1040a128 true 6 ... ... ... 3.1415 | значение, хранящееся по этому адресу! !!! Значения, представляющие адреса переменных, называются указателями, потому что они указывают на область памяти, в которой хранится переменная. |
58. Типы указателей
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
|
Тип указателя состоит из знака * и типа переменной, на которую ссылается указатель. Например, тип указателя на переменную int записывается в виде *int (читается «указатель на int»). С помощью функции reflect.TypeOf можно вывести типы указателей из приведенной ранее программы: package main import ( "fmt" "reflect" ) func main() { var myInt int fmt.Println(reflect.TypeOf(&myInt)) //Получает указатель на myInt и выводит тип указателя. var myFloat float64 fmt.Println(reflect.TypeOf(&myFloat)) //Получает указатель на myFloat и выводит тип указателя. var myBool bool fmt.Println(reflect.TypeOf(&myBool)) //Получает указатель на myBool и выводит тип указателя.* } В программе можно объявлять переменные, содержащие указатели. В таких переменных могут храниться только указатели на один конкретный тип переменной, так что переменная может содержать только указатели *int, только указатели *float64 и т. д. package main import "fmt" func main() { myInt := 4 //Значение myIntPointer := &myInt //Указатель fmt.Println(myIntPointer) //Выводится сам указатель. fmt.Println(*myIntPointer) //Выводится значение, на которое ссылается указатель. myFloat := 98.6 //Значение myFloatPointer := &myFloat //Указатель fmt.Println(myFloatPointer) //Выводит сам указатель fmt.Println(*myFloatPointer) //Выводится значение, на которое ссылается указатель. myBool := true //Значение myBoolPointer := &myBool ////Указатель fmt.Println(myBoolPointer) //Выводит сам указатель fmt.Println(*myBoolPointer) //Выводится значение, на которое ссылается указатель. Как и с другими типами, при немедленном присваивании исходного значения переменной-указателю можно воспользоваться коротким объявлением переменной. package main import "fmt" func main() { var myBool bool myBoolPointer := &myBool //Короткое объявление переменной-указателя. fmt.Println(myBoolPointer) } |
59. Чтение или изменение значения по указателю
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
!!! Оператор * может использоваться для обновления значения по указателю. package main import "fmt" func main() { myInt := 4 //Значение fmt.Println(myInt) //Вывести значение myIntPointer := &myInt //Создаем указатель *myIntPointer = 8 //Новое значение присваивается переменной, на которую ссылается указатель (myInt). fmt.Println(*myIntPointer) //Выводится значение переменной, на которую ссылается указатель. fmt.Println(myInt) //Значение переменной выводится напрямую. } /* 4 Исходное значение myInt. 8 Результат обновления *myIntPointer. 8 Обновление значения myInt (то же, что *myIntPointer). */ В приведенном коде команда *myIntPointer = 8 обращается к переменной, на которую ссылается указатель myIntPointer (то есть переменной myInt) и присваивает ей новое значение. Таким образом, обновляется не только значение *myIntPointer, но и myInt |
60. Использование указателей с функциями
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
Указатели также можно возвращать из функций; просто объявите возвращаемый тип функции как тип указателя. Пример кода: package main import "fmt" func createInt() *int { var myInt = 100 return &myInt } func createPointer() *float64 { var myFloat = 98.5 return &myFloat } func crS() *string { var xX = "hello all" return &xX } func main() { var myFloatPointer *float64 = createPointer() fmt.Println(*myFloatPointer) var x *int = createInt() fmt.Println(*x) var x1 *string = crS() fmt.Println(*x1) } Кстати говоря, в отличие от некоторых других языков, в Go можно вернуть указатель на переменную, локальную по отношению к функции. И хотя эта переменная уже не находится в области видимости, пока у вас есть указатель, Go предоставит вам доступ к значению этой переменной. package main import "fmt" func printPointer(myBoolPointer *bool) { fmt.Println(*myBoolPointer) } func main() { var myBool bool = true printPointer(&myBool) } |
61. Проведем промежуточные итоги
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
|
Функции Типы Условные команды Циклы 0. Объявление функций Чтобы вызвать объявленную вами функцию в другой точке того же пакета, введите имя функции и пару круглых скобок со списком аргументов (если он есть) Функция может возвращать одно или несколько значений. 1. Указатели Чтобы получить указатель на переменную, поставьте оператор & прямо перед именем переменной: &myVaruable Имена типов-указателей состоят из символа * и типа значения, на которое ссылается указатель (*int, *bool и т.д.) 2. Функция fmt.Printf и fmt.Sprintf форматируют переданные им значения. В первом аргументе передается форматная срока с глаголами (%d, %f, %s и т.д.), на место которых подставляется отформатированные значения. 2.1 Глагол форматирования может содержать ширину: минимальное количество символов, которые будет занимать отформатированное значение. Например, глагол %12s определяет строку из 12 символов (дополненную пробелами), %2d — целое число из двух цифр, а %.3f — число с плавающей точкой, округленное до трех цифр в дробной части. 3. Если вы хотите, чтобы при вызовах вашей функции передавались аргументы, необходимо объявить один или несколько параметров (с указанием типа каждого параметра) в объявлении функции. Количество и тип аргументов всегда должны соответствовать количеству и типу параметров — в противном случае вы получите ошибку компиляции. 4. Если вы хотите, чтобы ваша функция возвращала одно или несколько значений, объявите типы возвращаемых значений в объявлении функции. 5. К переменным, объявленным внутри функции, невозможно обратиться вне этой функции. С другой стороны, внутри функции можно обращаться к переменным, объявленным за пределами функции (обычно на уровне пакета). 6. Если функция возвращает несколько значений, последнее значение обычно имеет тип error. Значения ошибок содержат метод Error(), который возвращает строку с описанием ошибки. 7. По умолчанию функции возвращают значение ошибки nil, указывающее на отсутствие ошибок. 8. Чтобы обратиться к значению, на которое ссылается указатель, поставьте * перед именем: *myPointer. 9. Если функция получает указатель в параметре и обновляет значение, на которое ссылается указатель, такое изменение будет видимо за пределами функции. |
62. Пакеты \ Хранение кода пакетов
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
Пакеты нужны для хранения взаимосвязанного кода в одном месте. Но пакеты нужны не только для организации кода. Пакеты предоставляют простые средства для повторного использования кода в разных программах. А еще это простой способ распространения кода среди разработчиков. Инструменты Go ищут код пакетов в специальном каталоге (папке) на вашем компьютере, который называется рабочей областью. По умолчанию рабочей областью является каталог с именем go в домашнем каталоге текущего пользователя. Каталог рабочей области содержит три подкаталога: - bin для хранения откомпилированных двоичных исполняемых программ. - pkg для хранения откомпилированных двоичных файлов пакетов. - src для хранения исходного кода Go. В каталоге src код каждого пакета размещается в отдельном подкаталоге. По соглашениям имя подкаталога должно совпадать с именем пакета (так что код пакета gizmo должен храниться в подкаталоге с именем gizmo). Подкаталог каждого пакета должен содержать один или несколько файлов с исходным кодом. Имена файлов могут быть любыми, но они должны иметь расширение .go. домашний каталог пользователя | |_ go //каталог рабочей области. | |_ bin //Исполняемые программы | |_ pkg //Откомпилированный код пакетов | |_ src //Исходный код | |__ package1 //код пакета находится в отдельном каталоге. | |__ package2 //код пакета находится в отдельном каталоге. | |_ package2.go //каждый каталог пакета содержит один или несколько файлов с исходным кодом. |_ plugin.go //каждый каталог пакета содержит один или несколько файлов с исходным кодом. Вопрос: Вы говорите, что в каталоге пакета могут находиться несколько файлов. Что должен содержать каждый файл? Ответ: Все что угодно! Весь код пакета можно хранить в одном файле, а можно разбить его на несколько файлов. В любом случае они станут частью одного пакета. |
63. Создание нового пакета
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
Попробуем создать пакет в рабочей области. Это будет простой пакет с именем greeting, который выводит приветствия на разных языках. При установке Go каталог рабочей области не создается по умолчанию, так что вам придется создать его самостоятельно. Для начала перейдите в свой домашний каталог. (Путь имеет вид C:\Users\<имя_пользователя> в большинстве систем Windows, /Users/<имя_пользователя> на Mac и /home/<имя_пользователя> в большинстве систем Linux.) Linux: cd ~ mkdir -p go/src/greeting vim go/src/greeting/greeting.go ------------------------------- package greeting import "fmt" func Hello() { fmt.Println("Hello!") } func Hi() { fmt.Println("Hi!") } ------------------------------- Как и все файлы с исходным кодом, встречавшиеся вам до этого, файл начинается с директивы package. Но в отличие от других файлов, этот код не является частью пакета main; он принадлежит пакету с именем greeting. |
64. Импорт пакета в программу
|
cd ~ mkdir go/src/hi vim go/src/hi/main.go ------------------- package main import "greeting" func main() { greeting.Hello() greeting.Hi() } ---------------------- |
65. Файлы пакетов имеют одинаковую структуру
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
1. Директива package. 2. Директива import. 3. Собственно код программы Пример в коде: package main //Директива package. import "fmt" //Директива import. //Собственно код программы. func main() { fmt.Println("Hello, Go!") } 0. Изменить имя каталога greeting на salutation Ошибка: main.go:3:8: cannot find package "greeting" in any of: /usr/lib/go-1.15/src/greeting (from $GOROOT) /home/pi/go/src/greeting (from $GOPATH) Программа не будет работать, потому что: Инструменты Go используют имя в пути импорта как имя каталога, из которого должен загружаться исходный код пакета. Если имена не совпадают, то код не загрузится. 1. Изменить имя в строке package файла greeting \ на "package salutation" Ошибка: main.go:4:8: cannot find package "salutation" in any of: /usr/lib/go-1.15/src/salutation (from $GOROOT) /home/pi/go/src/salutation (from $GOPATH) Программа не будет работать, потому что: Содержимое каталога greeting загрузится как пакет с именем salutation. Но поскольку в вызовах функций в main.go упоминается пакет greeting, вы получите сообщения об ошибке 2. Преобразовать имена функций в файлах greeting.go и main.go к нижнему регистру func hello() func hi() greeting.hello() greeting.hi() Ошибка: # command-line-arguments ./main.go:6:2: cannot refer to unexported name greeting.hello ./main.go:6:2: undefined: greeting.hello ./main.go:7:2: cannot refer to unexported name greeting.hi ./main.go:7:2: undefined: greeting.hi Программа не будет работать, потому что: Функции, имена которых начинаются с букв нижнего регистра, не экспортируются — это означает, что они могут использоваться только в своем пакете. Чтобы в программе можно было использовать функцию из другого пакета, эта функция должна экспортироваться, а для этого ее имя должно начинаться с буквы верхнего регистра |
66. Соглашения по выбору имен пакетов
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
Разработчикам, использующим пакет, придется вводить его имя каждый раз, когда они вызывают функцию из этого пакета. (Вспомните fmt.Printf, fmt.Println, fmt.Print и т. д.) Чтобы не возникало проблем, при выборе имен пакетов следует соблюдать несколько правил: - Имя пакета должно быть записано только символами нижнего регистра. - Имя следует сокращать, если его смысл очевиден (например, fmt). - По возможности имя должно состоять из одного слова. Если необходимы два слова, они не должны разделяться символами подчеркивания, а второе слово не должно начинаться с буквы верхнего регистра (пример — пакет strconv). - Импортированные имена пакетов могут конфликтовать с именами локальных переменных, поэтому не используйте имя, которое с большой вероятностью может быть выбрано пользователями пакета. (Например, если бы пакет fmt назывался format, то импорт этого пакета создавал бы риск конфликта с локальной переменной format.) Уточнение имен При обращении к функции, переменной или чему-то еще, экспортируемому из другого пакета, необходимо уточнить имя функции или переменной, поставив перед именем функции или переменной имя пакета. Но при обращении к функции или переменной, определенной в текущем пакете, не следует уточнять имя пакета. |
67. Константы
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
Многие пакеты экспортируют константы: именованные значения, которые не изменяются за время работы программы. Объявление константы очень похоже на объявление переменной: в нем также указывается имя, необязательный тип и значение. Тем не менее правила несколько отличаются: - Вместо ключевого слова var используется ключевое слово const. - Значение константы должно быть задано в момент ее объявления. Вы не сможете присвоить его позднее, как с переменными. - Для переменных доступен синтаксис короткого объявления :=, а у констант аналогичной конструкции не существует. Имя константы Значение | | const TriangleSides int = 3 | | | Тип | Ключевое слово «const» Как и при объявлении переменных, тип можно опустить, он будет автоматически определен по присваиваемому значению: const SquareSides = 4 //Присваивается целое число, поэтому для константы будет выбран тип «int» Значение переменной может изменяться, но значение константы должно оставаться постоянным. Попытка присвоить новое значение константе приведет к ошибке компиляции. Эта особенность констант обеспечивает безопасность: константы должны использоваться для значений, которые не должны изменяться. Пример кода: package main import "fmt" func main() { const PentagonSides = 5 PentagonSides = 7 fmt.Println(PentagonSides) } Ошибка: # command-line-arguments ./00_constanta.go:8:16: cannot assign to PentagonSides Если ваша программа включает «фиксированные» значения литералов (особенно если эти значения используются в нескольких местах), подумайте о том, чтобы заменить их константами (даже если программа не разбита на пакеты). Ниже приведен пакет с двумя функциями, в обеих функциях целочисленный литерал 7 представляет количество дней в неделе: |
68. Вложенные каталоги и пути импорта пакетов
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
home └── user └── go └── src └── __a__ | │ └── a.go //Код пакета "a" | | | └────── b.go //Код пакета "b" | └─ main.go //Наша программа, использующая пакеты "a" и "b" Пример кода b: package deutsch import "fmt" func Hallo() { fmt.Println("Hallo!") } func Hue() { fmt.Println("Guten Tag!") } Пример код main: package main import ( "a" "a/b" ) func main() { a.Hello() a.Hi() b.Hallo() b.Hue() } |
69. Установка исполняемых файлов командой «go install»
|
При использовании команды "go run" программа должна быть сначала откомпилирована, как и все пакеты, от которых она зависит. И весь откомпилированный код будет потерян после завершения программы. Ранее была описана команда go build, которая компилирует и сохраняет исполняемый двоичный файл (файл, который может выполняться даже без установки Go). Но если вы будете использовать ее слишком часто, ваша рабочая область будет забита исполняемыми файлами в случайных и неподходящих местах. Команда go install также сохраняет откомпилированные бинарные версии исполняемых программ, но в четко определенном и легкодоступном месте: в каталоге bin вашей рабочей области Go. Просто передайте go install имя каталога из src, содержащего код исполняемой программы (то есть файлов .go, начинающихся с package main). Программа будет откомпилирована, а исполняемый файл будет сохранен в стандартном каталоге. !!! неважно, из какого каталога вы запустите "go install name_package" — компилятор будет искать каталог внутри каталога src Когда компилятор Go видит, что файл в каталоге name_package содержит объявление "package main", он понимает, что это код исполняемой программы. Он компилирует исполняемый файл и сохраняет результат в каталоге с именем bin в рабочей области Go. Каталог bin будет создан автоматически, если он до этого не существовал.) |
70. Переменная GOPATH и смена рабочих областей
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
На разных веб-сайтах можно увидеть, как разработчики говорят о "настройке GOPATH" при обсуждении рабочей области Go. GOPATH — переменная среды, к которой инструменты Go обращаются за информацией о местонахождении рабочей области. Большинство разработчиков хранит весь свой код Go в одной рабочей области и не меняет ее местонахождения по умолчанию. Но при желании можно использовать GOPATH для перемещения рабочей области в другой каталог. Переменная среды предназначена для хранения и чтения значений (как и переменные Go), но управляет ею операционная система, а не Go. Некоторые программы настраиваются при помощи переменных среды; к их числу относится и компилятор Go. Допустим, вместо домашнего каталога вы разместили свой пакет greeting в каталоге code в корневом каталоге своего жесткого диска. И теперь вы хотите запустить файл main.go, который зависит от greeting. Пример: . └── code ├── main.go └── src └── greeting └── greeting.go Код: cat code/main.go package main import "greeting" ifunc main() { greeting.Hello() greeting.Hi() } cat code/src/greeting/greeting.go package greeting import "fmt" func Hello() { fmt.Println("hrllo") } func Hi() { fmt.Print("Hi") } Допустим, вместо домашнего каталога вы разместили свой пакет greeting в каталоге code в корневом каталоге своего жесткого диска. И теперь вы хотите запустить файл main.go, который зависит от greeting. Но вы получаете сообщение об ошибке. В нем говорится, что пакет greeting не найден, так как компилятор продолжает искать пакет в подкаталоге go вашего домашнего каталога: Ошибка: main.go:2:8: cannot find package "greeting" in any of: /usr/lib/go-1.15/src/greeting (from $GOROOT) /home/pi/go/src/greeting (from $GOPATH) |
71. Настройка GOPATH
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
Если ваш код хранится в другом каталоге (вместо каталога по умолчанию), вы должны настроить компилятор Go и передать информацию о местонахождении кода. Для этого можно воспользоваться переменной среды GOPATH, а конкретный способ зависит от операционной системы. Системы Mac и Linux systems: Для настройки переменной среды используется команда export. В приглашении терминала введите команду: export GOPATH="/code" Для каталога с именем code в корневом каталоге жесткого диска используется путь "/code". Если вы храните код в другом месте, укажите нужный путь. Системы Windows: Для настройки переменной среды используется команда set. В приглашении командной строки введите команду: set GOPATH="C:\code" Для каталога с именем code в корневом каталоге жесткого диска используется путь «C:\code». Если вы храните код в другом месте, укажите нужный путь. Обратите внимание: описанные выше способы настраивают переменную среды GOPATH только для текущего окна терминала/командной строки. Вам придется настраивать ее заново для каждого нового окна. Впрочем, есть способы выполнить постоянную настройку переменной среды. |
72. Публикация пакетов / конфликт подключения пакетов с одним именем
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
!!! В каталоге src рабочего пространства Go может быть только один каталог keyboard. Похоже, может быть только один пакет с именем keyboard! Пример: home/user └── go └── src ├── keyboard │ └── keyboard.go ├── script │ └── main.go └── git.net └── keyboard └── keyboard.go Пример кода: package main import ( "fmt" "git.net/keyboard" //Обновляем путь импорта. "log" ) func main() { fmt.Print("Enter a grade: ") grade, err := keyboard.GetFloat() //Здесь ничего не изменилось: имя пакета осталось прежним. if err != nil { og.Fatal(err) } //... еще какой то код } |
73. Загрузка и установка пакетов командой «go get»
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
У использования URL-адреса размещения пакета в качестве пути импорта есть еще одно преимущество. У команды "go" существует еще одна субкоманда "go get", которая может автоматически загружать и устанавливать пакеты за вас. Создадим репозиторий "Git" для описанного выше пакета "greeting" по следующему URL-адресу: https://github.com/headfirstgo/greeting Это означает, что на любом компьютере с установленным экземпляром Go необходимо ввести в терминале следующую команду: go get github.com/headfirstgo/greeting После команды "go get" следует URL-адрес репозитория с отсеченным сегментом «схемы» («https://»). Команда подключается к "github.com", загружает репозиторий "Git" по пути "/headfirstgo/greeting" и сохраняет его в каталоге "src" вашей рабочей области "Go". (Примечание: если в вашей системе не установлена поддержка "Git", вам будет предложено установить ее при выполнении команды "go get". Просто следуйте инструкциям на экране. Команда "go get" также работает с репозиториями Subversion, Mercurial и Bazaar.) Команда "go get" автоматически создает подкаталоги, необходимые для настройки подходящего пути импорта (каталог github.com, каталог headfirstgo и т. д.). Пакеты, сохраненные в рабочей области Go, готовы к использованию в программах. Например, если вы хотите использовать пакеты greeting, dansk и deutsch в программе, включите в нее директиву импорта, которая выглядит примерно так: import ( "github.com/headfirstgo/greeting" "github.com/headfirstgo/greeting/dansk" "github.com/headfirstgo/greeting/deutsch" ) Команда go get работает также и с другими пакетами. 0. Переходим нужный каталог: cd /home/pi/githabmegafolder/c-test/02_lesson_golang/23_goget/go 1. Экспортируем GOPATH / каталог по умолчанию для go: export GOPATH=$(pwd) 2. Проверить переменную GOPATH: env | grep GOPATH 3. скачиваем пакеты: go get github.com/headfirstgo/keyboard go get github.com/headfirstgo/greeting |
74. Чтение документации пакетов командой «go doc»
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
Введите команду "go doc", чтобы вывести документацию по любому пакету или функции. Чтобы вывести документацию по пакету, передайте его путь импорта команде "go doc". Например, информация о пакете "strconv" выводится командой "go doc strconv". go doc -h go doc -u http go doc strconv go doc strconv ParseFloat go doc github.com/headfirstgo/keyboard Команда go doc старается получить полезную информацию на основании анализа кода. Имена пакетов и пути импорта включаются автоматически, как и имена функций, параметры и возвращаемые типы. И все же команда go doc не умеет творить чудеса. Если вы хотите, чтобы пользователи смогли узнать из документации о предназначении пакета или функции, придется добавить эту информацию самостоятельно. К счастью, это делается просто: нужно добавить в код документирующие комментарии. Обычные комментарии Go, размещенные непосредственно перед директивой package или объявлением функции, считаются документирующими комментариями и включаются в вывод go doc. При добавлении документирующих комментариев следует соблюдать ряд правил: - Комментарии должны состоять из полноценных предложений. - Комментарии для пакетов должны начинаться со слова "Package", за которым следует имя пакета: // Package mypackage enables widget management. - Комментарии для функций должны начинаться с имени функции, которую они описывают: // MyFunction converts widgets to gizmos. - В комментарии также можно включать примеры кода, которые должны снабжаться отступами. - Не включайте дополнительные символы для выразительности или форматирования (кроме отступов в примерах кода). Документирующие комментарии будут выводиться в виде простого текста и должны форматироваться соответствующим образом. |
75. go help
|
go help go help gopath go help list |
76. Запуск сервера документации HTML командой «godoc»
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
WEB поиск: https://pkg.go.dev/ https://pkg.go.dev/fmt Программа, которая обслуживает раздел документации сайта golang.org, на самом деле доступна и на вашем компьютере. Эта программа называется godoc (не путайте с командой go doc), и она автоматически устанавливается вместе с Go. Программа godoc генерирует документацию HTML на основании кода основной установки Go и вашей рабочей области. Она включает веб-сервер, который может передавать полученные веб-страницы браузеру. (Не беспокойтесь, с настройками по умолчанию godoc не будет принимать подключения с других компьютеров, кроме вашего.) Чтобы запустить godoc в режиме веб-сервера, введите в терминале команду godoc (еще раз: не перепутайте с go doc) со специальным параметром -http=:6060. godoc -http=:6060 После того как сервер godoc будет запущен, введите URL-адрес http://localhost:6060/pkg Debian apt-get install golang-golang-x-tools Ubuntu apt-get install golang-golang-x-tools Arch Linux pacman -S golang-godoc-1 pacman -S go-tools Kali Linux apt-get install golang-golang-x-tools Fedora dnf install golang-godoc-1 Raspbian apt-get install golang-golang-x-tools Display help for package "fmt": godoc fmt Display help for the function "Printf" of "fmt" package: godoc fmt Printf Serve documentation as a web server on port 6060: godoc -http=:6060 Create an index file: godoc -write_index -index_files=path/to/file Use the given index file to search the docs: godoc -http=:6060 -index -index_files=path/to/file |
77. Проведем промежуточные итоги
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
Функции Типы Условные команды Циклы Объявления decloracion Указатели Пакеты 00. По умолчанию рабочей областью является каталог с именем go в домашнем каталоге пользователя. 01. Чтобы использовать в качестве рабочей области другой каталог, настройте переменную среды GOPATH. 02. Go использует три подкаталога в рабочей области: в каталоге bin хранятся откомпилированные исполняемые программы, в каталоге pkg — откомпилированный код пакетов, а в каталоге src — исходный код Go. 03. Имена подкаталогов каталога src формируют путь импорта пакета. Имена вложенных каталогов разделяются символами / в пути импорта. 04. Имя пакета определяется директивами package в начале файла с исходным кодом в каталоге пакета. За исключением пакета main, имя пакета должно совпадать с именем каталога, в котором он находится. 05. Имена пакетов должны записываться в нижнем регистре. В идеале они состоят из одного слова. 06. Функции пакета могут вызываться за пределами пакета только в том случае, если они экспортированы. Функция экспортируется, если ее имя начинается с буквы верхнего регистра. 07. Константой называется имя для обращения к значению, которое никогда не изменяется. 08. Команда "go install" компилирует код пакета и сохраняет его в каталоге pkg для пакетов общего назначения или в каталоге bin для исполняемых программ. 09. В качестве пути импорта пакета принято использовать URL-адрес размещения пакета. В этом случае команда "go get" может находить, загружать и устанавливать пакеты, зная только их путь импорта. 10. Команда "go doc" выводит документацию пакетов. В выходные данные "go doc" включаются документирующие комментарии в коде. |
78. Массивы / arrays
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
|
Многие программы работают со списками. Списки адресов. Списки телефонных номеров. Списки товаров. Массив представляет собой набор значений, относящихся к одному типу. Представьте себе таблетницу — вы можете класть и доставать таблетки в любое отделение, но при этом ее удобно переносить как единое целое. Значения, хранящиеся в массиве, называются элементами. Вы можете создать массив строк, массив логических значений или массив любого другого типа Go (даже массив массивов). Весь массив можно сохранить в одной переменной, а затем обратиться к любому нужному элементу. В массивах хранятся наборы значений. Массив содержит заранее заданное количество элементов, а его размер не может увеличиваться или уменьшаться. Чтобы объявить переменную для хранения массива, следует указать количество хранящихся в нем элементов в квадратных скобках ([]), а затем тип элементов в массиве. Чтобы присвоить значения элементам массива или прочитать их позднее, необходимо каким-то образом указать, какой элемент вам нужен. Элементы в массиве нумеруются, начиная с 0. Номер массива называется его индексом. Пример массив нот: var notes [7]string notes[0] = "do" notes[1] = "re" notes[2] = "mi" fmt.Println(notes[0]) fmt.Println(notes[1]) Массив целых чисел: var primes [5]int primes[0] = 2 primes[1] = 3 fmt.Println(primes[0]) Массив значений time.Time: var dates [3]time.Time dates[0] = time.Unix(1257894000, 0) dates[1] = time.Unix(1447920000, 0) dates[2] = time.Unix(1508632200, 0) fmt.Println(dates[1]) Нулевые значения в массивах Как и в случае с переменными, при создании массивов все содержащиеся в них значения инициализируются нулевым значением для типа, содержащегося в массиве. Так массив значений int по умолчанию заполняется нулями. С другой стороны, нулевым значением для строк является пустая строка, так что массив строковых значений по умолчанию заполняется пустыми строками. Нулевые значения позволяют безопасно выполнять операции с элементами массивов, даже если им не были присвоены значения. Например, в следующем массиве хранятся целочисленные счетчики. Любой элемент можно увеличить на 1 даже без предварительного присваивания значения, потому что мы знаем, что все значения счетчиков начинаются с 0. var counters [3]int counters[0]++ // Первый элемент увеличивается с 0 до 1. counters[0]++ // Первый элемент увеличивается с 1 до 2. counters[2]++ // Третий элемент увеличивается с 0 до 1. fmt.Println(counters[0], counters[1], counters[2]) Вывод: 2 0 1 При создании массива все содержащиеся в нем элементы инициализируются нулевым значением для типа, хранящегося в массиве. |
79. Литералы массивов
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
Если вам заранее известны значения, которые должны храниться в массиве, вы можете инициализировать массив этими значениями в форме литерала массива. Литерал массива начинается как тип массива — с количества элементов в квадратных скобках, за которым следует тип элементов. Далее в фигурных скобках идет список исходных значений элементов массива. Значения элементов должны разделяться запятыми. [3]int{9, 18, 27} | | | | | Список значений, разделенных запятыми. | Тип элементов в массиве. | Количество элементов в массиве. var notes [7]string = [7]string{"do", "re", "mi", "fa", "so", "la", "ti"} fmt.Println(notes[3], notes[6], notes[0]) var primes [5]int = [5]int{2, 3, 5, 7, 11} fmt.Println(primes[0], primes[2], primes[4]) Литералы массивов также позволяют использовать короткие объявления переменных с помощью :=. Короткое объявление переменной. notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"} primes := [5]int{2, 3, 5, 7, 11} Литералы массивов могут распространяться на несколько строк, но перед каждым переносом строки в коде должна стоять запятая. Запятая даже должна стоять после последнего элемента в литерале массива, если за ним следует перенос строки. (На первый взгляд этот синтаксис выглядит неуклюже, но он упрощает последующее добавление новых элементов в коде.) text := [3]string{ // Все это один массив. "This is a series of long strings", "which would be awkward to place", "together on a single line", // Запятая в конце обязательна. } Когда вы занимаетесь отладкой кода, вам не нужно передавать элементы массивов Println и другим функциям пакета fmt один за одним. Просто передайте весь массив. Пакет fmt содержит логику форматирования и вывода массивов. (Пакет fmt также умеет работать с сегментами, картами и другими структурами данных, которые будут описаны позднее.) var notes [3]string = [3]string{"do", "re", "mi"} var primes [5]int = [5]int{2, 3, 5, 7, 11} fmt.Println(notes) fmt.Println(primes) Возможно, вы также помните глагол "%#v", используемый функциями Printf и Sprintf, — он форматирует значения так, как они отображаются в коде Go. При форматировании с "%#v" массивы отображаются в форме литералов массивов Go. fmt.Printf("%#v\n", notes) fmt.Printf("%#v\n", primes) |
80. Обращение к элементам массива в цикле
|
Вы не обязаны явно записывать целочисленные индексы элементов массивов, к которым обращаетесь в своем коде. В качестве индекса также можно использовать значение целочисленной переменной. notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"} index := 1 fmt.Println(index, notes[index]) // Выводит элемент массива с индексом 1. index = 3 fmt.Println(index, notes[index]) // Выводит элемент массива с индексом 3. for i := 0; i <= 2; i++ { fmt.Println(i, notes[i]) } При обращении к элементам массивов через переменную необходимо действовать внимательно и следить за тем, какие значения индексов используются в программе. Как упоминалось ранее, массивы содержат конкретное число элементов. Попытка обратиться к индексу за пределами массива приводит к панике — ошибке, происходящей во время выполнения программы (а не на стадии компиляции). |
81. Проверка длины массива функцией «len»
|
Написание циклов, которые ограничиваются только правильными индексами, сопряжено с определенным риском ошибок. К счастью, есть пара приемов, которые упрощают этот процесс. Во-первых, вы можете проверить фактическое количество элементов в массиве перед обращением к элементу. Для этого можно воспользоваться встроенной функцией len, которая возвращает длину массива (количество содержащихся в нем элементов). notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"} for i := 0; i < len(notes); i++ { fmt.Println(i, notes[i]) } Впрочем, и здесь существует некоторый риск ошибок. Хотя len(notes) возвращает наибольший индекс, к которому вы можете обращаться, равен 6 (потому что индексирование массивов начинается с 0, а не с 1). При попытке обратиться по индексу 7 возникнет ситуация паники. |
82. Безопасный перебор массивов в цикле «for…range».
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
В другом, еще более безопасном способе обработки всех элементов массива используется специальный цикл for...range. В форме с range указывается переменная для хранения целочисленного индекса каждого элемента, другая переменная для хранения значения самого элемента и перебираемый массив. Цикл выполняется по одному разу для каждого элемента в массиве; индекс элемента присваивается первой переменной, а значение элемента — второй переменной. В блок цикла включается код для обработки этих значений. for index, value := range myArray { // Блок цикла. } Эта форма цикла for не содержит запутанных выражений инициализации, условия и завершения. А поскольку значение элемента автоматически присваивается переменной, риск обращения к недействительному индексу массива исключен. Форма цикла for с range читается безопаснее и проще, поэтому именно она чаще всего встречается при работе с массивами и другими коллекциями. notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"} for index, note := range notes { fmt.Println(index, note) } Цикл выполняется семь раз, по одному разу для каждого элемента в массиве notes. Для каждого элемента переменной index присваивается индекс элемента, а переменной note присваивается значение элемента. После этого мы выводим индекс и значение. Помните, как при вызове функции с несколькими возвращаемыми значениями мы хотели проигнорировать одно из них? Это значение присваивалось пустому идентификатору ( _ ), чтобы компилятор Go просто отбросил это значение без выдачи сообщения об ошибке... То же самое можно проделать со значениями из циклов "for...range". Если вам не нужен индекс каждого элемента массива, присвойте его пустому идентификатору: notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"} for index, note := range notes { fmt.Println(index, note) } А если вам не нужна переменная для значения, замените ее пустым идентификатором: |
83. Чтение текстового файла
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
Ранее мы использовали пакеты os и bufio стандартной библиотеки для чтения данных по строкам с клавиатуры. Те же пакеты могут использоваться и для построчного чтения данных из текстовых файлов. В своем любимом текстовом редакторе создайте новый файл с именем data.txt. Запишите в файле три наших значения с плавающей точкой, по одному числу в строке. cat > data.txt << "EOF" 71.8 56.2 89.5 EOF Пример программы: package main import ( "bufio" "fmt" "log" "os" ) func main() { file, err := os.Open("data.txt") //Файл данных открыва if err != nil { log.Fatal(err) } scanner := bufio.NewScanner(file) //Цикл выполняется до того, как будет достигнут конец файла, а scanner.Scan вернет false for scanner.Scan() { //Читает строку из файла fmt.Println(scanner.Text()) //Выводит строку } // Если при закрытии файла произошла ошибка то сообщить о ней и завершить работу err = file.Close() //Закрывает файл для освобождения ресурсов. if err != nil { log.Fatal(err) } if scanner.Err() != nil { log.Fatal(scanner.Err()) } } Наша тестовая программа readfile.go успешно читает данные из файла data.txt и выводит их. А теперь разберемся, как же она работает. Сначала строка с именем открываемого файла передается функции os.Open. Эта функция возвращает два значения: указатель os.File, представляющий открытый файл, и значение ошибки. Как и в случае с другими функциями, если значение ошибки равно nil, это означает, что файл был открыт успешно, но любое другое значение указывает на то, что произошла ошибка (например, если файл отсутствует или не читается). В таком случае программа выводит сообщение об ошибке и завершается. |
84. Чтение текстового файла в массив
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
Пример кода: // Пакет datafile предназначен для чтения данных из файлов. //package datafile package main import ( "fmt" "bufio" "os" "strconv" ) // GetFloats читает значение float64 из каждой строки файла. ошибку. func GetFloats(fileName string) ([3]float64, error) { var numbers [3]float64 // Объявление возвращаемого массива. file, err := os.Open(fileName) //Открывает файл с переданным именем. if err != nil { return numbers, err } i := 0 //Переменная для хранения индекса, по которому должно выполняться присваивание scanner := bufio.NewScanner(file) for scanner.Scan() { numbers[i], err = strconv.ParseFloat(scanner.Text(), 64) if err != nil { return numbers, err } i++ //Переход к следующему индексу массива. } err = file.Close() if err != nil { return numbers, err } if scanner.Err() != nil { return numbers, scanner.Err() } return numbers, nil //Если выполнение дошло до этой точки, значит, ошибок не было, поэтому программа возвращает массив чисел и значение ошибки «nil ». } func main() { fileName := "data.txt" fmt.Println("Hello readfile") fmt.Println(GetFloats(fileName)) } |
85. Чтение файла и выполнение программы average. Еще один пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
|
//average вычисляет среднее значение package main import ( "bufio" "fmt" "os" "strconv" "log" ) // GetFloats читает значение float64 из каждой строки файла. ошибку. func GetFloats(fileName string) ([3]float64, error) { var numbers [3]float64 // Объявление возвращаемого массива. file, err := os.Open(fileName) //Открывает файл с переданным именем. if err != nil { return numbers, err } i := 0 //Переменная для хранения индекса, по которому должно выполняться присваивание scanner := bufio.NewScanner(file) for scanner.Scan() { numbers[i], err = strconv.ParseFloat(scanner.Text(), 64) if err != nil { return numbers, err } i++ //Переход к следующему индексу массива. } err = file.Close() if err != nil { return numbers, err } if scanner.Err() != nil { return numbers, scanner.Err() } return numbers, nil //Если выполнение дошло до этой точки, значит, ошибок не было, поэтому программа возвращает массив чисел и значение ошибки «nil». } func main() { filename := "data.txt" numbers, err := GetFloats(filename) if err != nil { log.Fatal(err) } var sum float64 = 0 for _, number := range numbers { sum += number } sampleCount := float64(len(numbers)) fmt.Printf("Average: %0.2f\n", sum/sampleCount) } Наша программа может обрабатывать только три значения! cat > data1.txt << "EOF" 71.8 56.2 89.5 99.3 EOF Пример ошибки: panic: runtime error: index out of range [3] with length 3 goroutine 1 [running]: main.GetFloats(0xd5676, 0x9, 0x0, 0x0, 0x0, 0x4d458, 0x162060) /home/pi/githabmegafolder/c-test/02_lesson_golang/25_file/02_averange.go:25 +0x2e4 main.main() /home/pi/githabmegafolder/c-test/02_lesson_golang/25_file/02_averange.go:46 +0x34 exit status 2 |
86. Промежуточные итоги:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
Массивы: Массив является списком значений определенного типа. Каждое значение, хранимое в массиве, называется элементом массива. Массив содержит фиксированное количество элементов. Не существует возможности легко добавить новые элементы в массив. Ключевые моменты: - Чтобы объявить переменную-массив, укажите длину массива в квадратных скобках и тип хранящихся в нем элементов: var myArray [3]int - Чтобы прочитать или присвоить значение элемента массива, укажите его индекс в квадратных скобках. Индексы начинаются с 0, поэтому первый элемент myArray обозначается myArray[0]. - Как и переменные, по умолчанию все элементы массива инициализируются нулевым значением для типа элемента. - Элементы массива можно инициализировать в момент создания; для этого используется литерал массива: [3]int{4, 9, 6} - Если сохранить недопустимый индекс массива в переменной, а потом попытаться обратиться к элементу с использованием этой переменной в качестве индекса, возникнет ситуация паники — ошибка времени выполнения. - Для получения количества элементов в массиве используется встроенная функция len. Все элементы массива можно удобно обработать в специальном синтаксисе цикла "for... range". Этот цикл перебирает все элементы и присваивает индекс и значение каждого элемента указанным вами переменным. - При использовании цикла "for...range" можно игнорировать индекс или значение каждого элемента при помощи пустого идентификатора _. - Функция os.Open открывает файл. Она возвращает указатель на значение os.File, представляющее открытый файл. - При передаче значения os.File функции bufio.NewScanner возвращается значение bufio.Scanner. Его методы Scan и Text используются для последовательного чтения файла по строкам. |
87. Сегменты
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
Вы уже знаете, что в массив нельзя добавить новые элементы. В нашей программе это создает настоящие проблемы, потому что количество значений данных в файле неизвестно заранее. На помощь приходят сегменты Go. Сегменты — разновидность коллекций, которые могут расширяться для хранения дополнительных элементов; а это как раз то, что нужно! Оказывается, в Go существует структура данных, в которую можно добавлять новые значения, — она называется сегментом. Как и массив, сегмент состоит из нескольких элементов, относящихся к одному типу. В отличие от массивов, существуют функции, позволяющие добавлять новые элементы в конец сегмента. Отличие сегмента от массива: Фактически это уже знакомый синтаксис объявления массива, только без указания размера. var myArray [5]int // Массив — обратите внимание на размер. var mySlice []int // Сегмент — размер не задан В отличие от переменных для массивов, объявление переменной для сегмента не приводит к автоматическому созданию сегмента. Для этого следует вызвать встроенную функцию make. Функции передается тип создаваемого сегмента (он должен соответствовать типу переменной, которой вы собираетесь присвоить сегмент) и длина сегмента при создании. Пример: var notes []string notes = make([]string, 7) notes[0] = "do" notes[1] = "re" notes[2] = "mi" fmt.Println(notes[2]) fmt.Println(notes[0]) Пример: pr := make([]int, 5) pr[0] = 99 pr[4] = 200 fmt.Println("pr[4] - pr[0] = ", pr[4]-pr[0]) Встроенная функция len для сегментов работает так же, как и для массивов. Передайте len сегмент, и функция вернет его длину в виде целого числа. Пример: notes := make([]string, 7) primers := make([]int, 5) fmt.Println(len(notes)) fmt.Println(len(primers)) Циклы "for" и "for...range" работают с сегментами точно так же, как и с массивами: Пример: letters := []string{"a", "b", "c"} for i := 0; i < len(letters); i++ { fmt.Println(letters[i]) } fmt.Println(" ") for _, letter := range letters { fmt.Println(letter) } |
88. Литералы сегментов
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
|
Как и с массивами, если вы заранее знаете, какими значениями должен быть заполнен сегмент в исходном состоянии, то можете инициализировать сегмент этими значениями при помощи литерала сегмента. Литерал сегмента очень похож на литерал массива, но если литерал массива содержит длину массива в квадратных скобках, у литерала сегмента квадратные скобки пусты. За пустыми скобками следует тип элементов, которые будут храниться в сегменте, и список исходных значений всех элементов, заключенный в фигурные скобки. Вызывать функцию make необязательно, при использовании литерала сегмента ваш код создаст сегмент и заполнит его. Сегмент: []int{9, 18, 27} | | | | | Список значений, разделенных запятыми. | Тип элементов в сегменте Пустая пара квадратных скобок Пример: notes := []string{"do", "re", "mi", "fa", "so", "la", "ti"} //Значения присваиваются с помощью литерала сегмента. fmt.Println(notes[3], notes[6], notes[0]) primes := []int{ // Многострочный литерал сегмента. 2, 3, 5, } fmt.Println(primes[0], primes[1], primes[2]) Погодите! Похоже, что сегменты могут делать все, что делают массивы, и в них можно добавлять элементы. Тогда почему бы не ограничиться сегментами и не забыть про эту ерунду с массивами? !!! Потому что сегменты построены на основе массивов. !!! И вы не сможете понять, как работают сегменты, не понимая массивы. Каждый массив существует на основе базового массива. Данные сегмента на самом деле хранятся в базовом массиве, а сегмент всего лишь предоставляет «окно» для работы с некоторыми (или всеми) элементами массива. Когда вы используете функцию make или литерал сегмента для создания сегмента, базовый массив при этом создается автоматически (и вы не можете обратиться к нему иначе как через сегмент). Но вы также можете создать массив самостоятельно, а затем создать сегмент на основе этого массива при помощи оператора сегмента. Пример: underlyingArray := [5]string{"a", "b", "c", "d", "e"} slice1 := underlyingArray[0:3] fmt.Println(slice1) Пример: underlyingArray := [5]string{"a", "b", "c", "d", "e"} i, j := 1, 4 slice2 := underlyingArray[i:j] fmt.Println(slice2) У оператора сегмента предусмотрены значения по умолчанию как для начального, так и для конечного индексов. Если начальный индекс не указан, будет использовано значение 0 позиции. Пример: underlyingArray := [5]string{"a", "b", "c", "d", "e"} slice4 := underlyingArray[:3] fmt.Println(slice4) А если не указан конечный индекс, то в сегмент включаются элементы от начального индекса и до конца базового завершается. Пример: underlyingArray := [5]string{"a", "b", "c", "d", "e"} slice5 := underlyingArray[1:] fmt.Println(slice5) |
89. Базовые массивы
|
Как упоминалось ранее, сам сегмент не содержит данных, это всего лишь «окно» для просмотра элементов базового массива. Сегмент можно представить себе как микроскоп, направленный на определенную часть предметного стекла (базовый массив). Когда вы берете сегмент базового массива, то «видите» только ту часть элементов массива, которая видна через этот сегмент. Несколько сегментов могут существовать на основе одного базового массива. В этом случае каждый сегмент становится «окном» для отдельного подмножества элементов массива. Сегменты даже могут перекрываться! Присваивание нового значения элементу сегмента приводит к изменению соответствующего элемента в базовом массиве. Если на один и тот же базовый массив указывают несколько сегментов, то и изменения элементов массива будут видны во всех сегментах. Из-за этих потенциальных проблем обычно рекомендуется создавать сегменты с использованием make или литерала сегмента (вместо того, чтобы создать массив и применять к нему оператор сегмента). С make и литералами сегментов вам никогда не приходится иметь дела с базовым массивом. |
90. Расширение сегментов функцией «append»
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
В Go существует встроенная функция append, которая получает сегмент и одно или несколько значений, которые присоединяются в конец сегмента. Функция возвращает но вый, расширенный сегмент со всеми элементами исходного сегмента и новыми элементами, добавленными в его конец. Пример: slice := []string{"a", "b"} fmt.Println(slice, len(slice)) slice = append(slice, "c") fmt.Println(slice, len(slice)) slice = append(slice, "d", "e") fmt.Println(slice, len(slice)) Вам не нужно следить за тем, по какому индексу присваиваются новые значения, или за чем-нибудь еще! Просто вызовите функцию append и передайте ей сегмент со значениями, которые добавляются в конец сегмента, и вы получите новый расширенный сегмент. Да, так просто! Обратите внимание: возвращаемое значение append во всех случаях присваивается той же переменной сегмента, которая передается append. Таким образом предотвращается возможность непоследовательного поведения сегментов, возвращаемых append. Базовый массив сегмента не может увеличиваться в размерах. Если в массиве не остается места для добавления элементов, все элементы копируются в новый, больший массив, а сегмент обновляется, чтобы он базировался на новом массиве. Но поскольку все это происходит где-то за кулисами внутри функции append, невозможно простым способом определить, имеет ли возвращенный сегмент тот же базовый массив, как и переданный сегмент, или другой. Если в программе будут оставаться оба сегмента, это может привести к непредсказуемому поведению. Пример: s1 := []string{"s1", "s1"} s2 := append(s1, "s2", "s2") s3 := append(s2, "s3", "s3") s4 := append(s3, "s4", "s4") fmt.Println(s1, s2, s3, s4) s4[0] = "XX" fmt.Println(s1, s2, s3, s4) По этой причине при вызове append возвращаемое значение обычно присваивается той же переменной сегмента, которая была передана append. Если в программе хранится только один сегмент, то вам не придется беспокоиться о том, используют ли два сегмента один базовый массив! Пример: s1 := []string{"s1", "s1"} s1 = append(s1, "s2", "s2") s1 = append(s1, "s3", "s3") s1 = append(s1, "s4", "s4") fmt.Println(s1) |
91. Сегменты и нулевые значения
|
Как и в случае с массивами, при обращении к элементу сегмента, которому не было присвоено значение, вы получите нулевое значение для этого типа. Пример: floatSlice := make([]float64, 10) boolSlice := make([]bool, 10) fmt.Println(floatSlice[9], boolSlice[5]) Пример: var intSlice []int var stringSlice []string fmt.Printf("intSlice: %#v, stringSlice: %#v\n", intSlice, stringSlice) |
92. Аргументы командной строки
|
cd /home/user | | | аргумент команда cd -l /home/user | | | | | второй аргумент | первый аргумент команда |
93. Получение аргументов командной строки из сегмента os.Args
|
Пример: package main import ( "fmt" "os" ) func main() { fmt.Println(os.Args) } |
94. Использование аргументов командной строки в программе
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
// average2 вычисляет среднее значение. // use: go run 01_average2.go 50 10 55 66 99 45 22 33 100 package main import ( "fmt" "log" "os" "strconv" ) func main() { arguments := os.Args[1:] var sum float64 = 0 for _, argument := range arguments { number, err := strconv.ParseFloat(argument, 64) if err != nil { log.Fatal(err) } sum += number //Число прибавляется к сумме } sampleCount := float64(len(arguments)) fmt.Printf("Average: %0.2f\n", sum/sampleCount) //Вычисление среднего значения } |
95. Функции с переменным количеством аргументов
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
|
После знакомства с сегментами мы можем рассмотреть одну возможность Go, которая до сих пор явно не упоминалась. Вы заметили, что при некоторых вызовах функций может передаваться разное количество аргументов? Например, взгляните на функцию fmt.Println или append. fmt.Println(1) fmt.Println(1, 2, 3, 4, 5) letters := []string{"a"} letters = append(letters, "b") letters = append(letters, "c", "d", "e", "f", "g") fmt.Println(letters) Как же это делают функции Println и append? Они объявляются как функции с переменным количеством параметров. Таким функциям при вызове может передаваться разное количество аргументов. Чтобы функция могла получать переменное количество аргументов, поставьте многоточие (...) перед типом последнего (или единственного) параметра функции в ее объявлении. Пример: func myFunc(param1 int, param2 ...string) { // код функции | | } | тип многоточие Пример: package main import "fmt" func severalInts(numbers ...int) { fmt.Println(numbers) } func main() { severalInts(1) severalInts(1, 2, 3) } Функция также может получать один или несколько фиксированных аргументов. Если при вызове переменную часть аргументов можно опускать (что приведет к созданию пустого сегмента), фиксированные аргументы всегда обязательны, если опустить их, произойдет ошибка компиляции. Переменным может быть только последний параметр в определении функции, он не может предшествовать обязательным параметрам. func mix(num int, flag bool, strings ...string) { fmt.Println(num, flag, strings) } func main() { mix(1, true, "a", "b") mix(2, false, "a", "b", "c", "d") } |
96. Использование функций с переменным количеством аргументов (inRange, maximum)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
Пример: package main import ( "fmt" "math" ) func maximum(numbers ...float64) float64 { //Получаем любое количесво аргументов float64 max := math.Inf(-1) //начинаем с очень низского значения for _, number := range numbers { if number > max { max = number } } return max } func main() { fmt.Println(maximum(71.8, 56.2, 89.5)) fmt.Println(maximum(90.7, 89.8, 98.3, 99.2)) |