Golang
Оглавление
- Строки
- Флаги
- Ввод / Вывод
- Файлы и папки
- Ошибки
- Контейнеры и сортировки
- Список
- Сортировка
- Хэши и криптография
- Серверы
- HTTP
- RPC
- Получение аргументов из командной строки
- Синхронизация примитивов
- Мьютексы
Вместо того, чтобы каждый раз писать всё с нуля, реальный мир программирования требует от нас умения взаимодействовать с уже существующими библиотеками. В этой главе мы рассмотрим самые часто используемые пакеты, включенные в Go.
Предупреждаю: некоторые библиотеки достаточно очевидны (или были объяснены в предыдущих главах), многие из библиотек, включённых в Go требуют специальных знаний (например: криптография). Объяснение этих технологий выходит за рамки этой книги.
Строки
Go содержит большое количество функций для работы со строками в пакете
package main import ( "fmt" "strings" ) func main() { fmt.Println( // true strings.Contains("test", "es"), // 2 strings.Count("test", "t"), // true strings.HasPrefix("test", "te"), // true strings.HasSuffix("test", "st"), // 1 strings.Index("test", "e"), // "a-b" strings.Join([]string{"a","b"}, "-"), // == "aaaaa" strings.Repeat("a", 5), // "bbaa" strings.Replace("aaaa", "a", "b", 2), // []string{"a","b","c","d","e"} strings.Split("a-b-c-d-e", "-"), // "test" strings.ToLower("TEST"), // "TEST" strings.ToUpper("test"), ) }
Иногда нам понадобится работать с бинарными данными. Чтобы преобразовать строку в набор байт (и наоборот), выполните следующие действия:
arr := []byte("test") str := string([]byte{'t','e','s','t'})
Флаги
Базовые определения флагов доступны для типов string, integer и boolean.
Также флаги можно определять, используя существующую переменную, объявленную ранее в программе. Обратите внимание, что мы передаем в функцию указатель.
Как только все флаги определены, вызываем flag.Parse()
, чтобы выполнить непосредственно разбор командной строки.
Используйте флаги -h
или --help
, чтобы вывести на экран автоматически сгенерированное текст с описанием флагов.
package main import "flag" import "fmt" func main() { wordPtr := flag.String("word", "foo", "a string") numbPtr := flag.Int("numb", 42, "an int") boolPtr := flag.Bool("fork", false, "a bool") var svar string flag.StringVar(&svar, "svar", "bar", "a string var") flag.Parse() fmt.Println("word:", *wordPtr) fmt.Println("numb:", *numbPtr) fmt.Println("fork:", *boolPtr) fmt.Println("svar:", svar) fmt.Println("tail:", flag.Args()) }
Ввод / Вывод
Прежде чем мы перейдем к работе с файлами, нужно узнать про пакет io
. Пакет io
состоит из нескольких функций, но в основном, это интерфейсы, используемые в других пакетах. Два основных интерфейса — это Reader
и Writer
. Reader
занимается чтением с помощью метода Read
. Writer
занимается записью с помощью метода Write
. Многие функции принимают в качестве аргумента Reader
или Writer
. Например, пакет io
содержит функцию Copy
, которая копирует данные из Reader
во Writer
:
func Copy(dst Writer, src Reader) (written int64, err error)
Чтобы прочитать или записать []byte
или string
, можно использовать структуру Buffer
из пакета bytes
:
var buf bytes.Buffer buf.Write([]byte("test"))
Buffer
не требует инициализации и поддерживает интерфейсы Reader
и Writer
. Вы можете конвертировать его в []byte
вызвав buf.Bytes()
. Если нужно только читать строки, можно так же использовать функцию strings.NewReader()
, которая более эффективна, чем чтение в буфер.
Файлы и папки
Для открытия файла Go использует функцию Open
из пакета os
. Вот пример того, как прочитать файл и вывести его содержимое в консоль:
package main import ( "fmt" "os" ) func main() { file, err := os.Open("test.txt") if err != nil { // здесь перехватывается ошибка return } defer file.Close() // получить размер файла stat, err := file.Stat() if err != nil { return } // чтение файла bs := make([]byte, stat.Size()) _, err = file.Read(bs) if err != nil { return } str := string(bs) fmt.Println(str) }
Мы используем defer file.Close()
сразу после открытия файла, чтобы быть уверенным, что файл будет закрыт после выполнения функции. Чтение файлов является частым действием, так что вот самый короткий способ сделать это:
package main import ( "fmt" "io/ioutil" ) func main() { bs, err := ioutil.ReadFile("test.txt") if err != nil { return } str := string(bs) fmt.Println(str) }
А вот так мы можем создать файл:
package main import ( "os" ) func main() { file, err := os.Create("test.txt") if err != nil { // здесь перехватывается ошибка return } defer file.Close() file.WriteString("test") }
Чтобы получить содержимое каталога, мы используем тот же os.Open()
, но передаём ему путь к каталогу вместо имени файла. Затем вызывается функция Readdir
:
package main import ( "fmt" "os" ) func main() { dir, err := os.Open(".") if err != nil { return } defer dir.Close() fileInfos, err := dir.Readdir(-1) if err != nil { return } for _, fi := range fileInfos { fmt.Println(fi.Name()) } }
Иногда мы хотим рекурсивно обойти каталоги (прочитать содержимое текущего и всех вложенных каталогов). Это делается просто с помощью функции Walk
, предоставляемой пакетом path/filepath
:
package main import ( "fmt" "os" "path/filepath" ) func main() { filepath.Walk(".", func(path string, info os.FileInfo, err error) error { fmt.Println(path) return nil }) }
Функция, передаваемая вторым аргументом, вызывается для каждого файла и каталога в корневом каталоге (в данном случае).
Ошибки
Go имеет встроенный тип для сообщений об ошибках, который мы уже рассматривали (тип error
). Мы можем создать свои собственные типы сообщений об ошибках используя функцию New
из пакета errors
.
package main import "errors" func main() { err := errors.New("error message") }
Контейнеры и сортировки
В дополнение к спискам и картам, Go предоставляет еще несколько видов коллекций, доступных в пакете container
. В качестве примера рассмотрим container/list
.
Список
Пакет container/list
реализует двусвязный список. Структура типа данных связного списка выглядит следующим образом:
Каждый узел списка содержит значение (в нашем случае: 1, 2 или 3) и указатель на следующий узел. Но так как это двусвязный список, узел так же содержит указатель на предыдущий. Такой список может быть создан с помощью следующей программы:
package main import ("fmt" ; "container/list") func main() { var x list.List x.PushBack(1) x.PushBack(2) x.PushBack(3) for e := x.Front(); e != nil; e=e.Next() { fmt.Println(e.Value.(int)) } }
Пустым значением List
(вероятно, опечатка и имелось ввиду x
— прим. пер.) является пустой список (*List
создаётся при вызове list.New
). Значения добавляются в список при помощи PushBack
. Далее, мы перебираем каждый элемент в списке, получая ссылку на следующий, пока не достигнем nil
.
Сортировка
Пакет sort
содержит функции для сортировки произвольных данных. Есть несколько предопределённых функций (для срезов, целочисленных значений и чисел с плавающей точкой). Вот пример, как отсортировать ваши данные:
package main import ("fmt" ; "sort") type Person struct { Name string Age int } type ByName []Person func (this ByName) Len() int { return len(this) } func (this ByName) Less(i, j int) bool { return this[i].Name < this[j].Name } func (this ByName) Swap(i, j int) { this[i], this[j] = this[j], this[i] } func main() { kids := []Person{ {"Jill",9}, {"Jack",10}, } sort.Sort(ByName(kids)) fmt.Println(kids) }
Хэши и криптография
Функция хэширования принимает набор данных и уменьшает его до фиксированного размера. Хэши используются в программировании повсеместно, начиная от поиска данных, заканчивая быстрым детектированием изменений. Хэш-функции в Go подразделяются на две категории: криптографические и некриптографические.
Некриптографические функции можно найти в пакете hash
, который включает такие алгоритмы как adler32
, crc32
, crc64
и fnv
. Вот пример использования crc32
:
package main import ( "fmt" "hash/crc32" ) func main() { h := crc32.NewIEEE() h.Write([]byte("test")) v := h.Sum32() fmt.Println(v) }
Объект crc32
реализует интерфейс Writer
, так что мы можем просто записать в него набор байт, как и в любой другой Writer
. После записи мы вызываем Sum32()
, который вернёт uint32
. Обычным применением crc32
является сравнение двух файлов. Если значение Sum32()
для обоих файлов одинаковы, то, весьма вероятно (не со стопроцентной гарантией), содержимое этих файлов идентично. Если же значения отличаются, значит файлы, безусловно, разные:
package main import ( "fmt" "hash/crc32" "io/ioutil" ) func getHash(filename string) (uint32, error) { bs, err := ioutil.ReadFile(filename) if err != nil { return 0, err } h := crc32.NewIEEE() h.Write(bs) return h.Sum32(), nil } func main() { h1, err := getHash("test1.txt") if err != nil { return } h2, err := getHash("test2.txt") if err != nil { return } fmt.Println(h1, h2, h1 == h2) }
Криптографические хэш-функции аналогичны их некриптографическим коллегам, однако у них есть одна особенность: их сложно обратить вспять. Очень сложно определить, что за набор данных содержится в криптографическом хэше, поэтому такие хэши часто используются в системах безопасности.
Одним из криптографических хэш-алгоритмов является SHA-1. Вот как можно его использовать:
package main import ( "fmt" "crypto/sha1" ) func main() { h := sha1.New() h.Write([]byte("test")) bs := h.Sum([]byte{}) fmt.Println(bs) }
Этот пример очень похож на пример использования crc32
, потому что оба они реализуют интерфейс hash.Hash
. Основное отличие в том, что в то время как crc32
вычисляет 32-битный хэш, sha1
вычисляет 160-битный хэш. В Go нет встроенного типа для хранения 160-битного числа, поэтому мы используем вместо него срез размером 20 байт.
Серверы
На Go очень просто создавать сетевые серверы. Сначала давайте взглянем, как создать TCP сервер:
package main import ( "encoding/gob" "fmt" "net" ) func server() { // слушать порт ln, err := net.Listen("tcp", ":9999") if err != nil { fmt.Println(err) return } for { // принятие соединения c, err := ln.Accept() if err != nil { fmt.Println(err) continue } // обработка соединения go handleServerConnection(c) } } func handleServerConnection(c net.Conn) { // получение сообщения var msg string err := gob.NewDecoder(c).Decode(&msg) if err != nil { fmt.Println(err) } else { fmt.Println("Received", msg) } c.Close() } func client() { // соединиться с сервером c, err := net.Dial("tcp", "127.0.0.1:9999") if err != nil { fmt.Println(err) return } // послать сообщение msg := "Hello World" fmt.Println("Sending", msg) err = gob.NewEncoder(c).Encode(msg) if err != nil { fmt.Println(err) } c.Close() } func main() { go server() go client() var input string fmt.Scanln(&input) }
Этот пример использует пакет encoding/gob
, который позволяет легко кодировать выходные данные, чтобы другие программы на Go (или конкретно эта программа, в нашем случае) могли их прочитать. Дополнительные способы кодирования доступны в пакете encoding
(например encoding/json
), а так-же в пакетах сторонних разработчиков (например, можно использовать labix.org/v2/mgo/bson
для работы с BSON).
HTTP
HTTP-серверы еще проще в настройке и использовании:
package main import ("net/http" ; "io") func hello(res http.ResponseWriter, req *http.Request) { res.Header().Set( "Content-Type", "text/html", ) io.WriteString( res, `<doctype html> <html> <head> <title>Hello World</title> </head> <body> Hello World! </body> </html>`, ) } func main() { http.HandleFunc("/hello", hello) http.ListenAndServe(":9000", nil) }
HandleFunc
обрабатывает URL-маршрут (/hello
) с помощью указанной функции. Мы так же можем обрабатывать статические файлы при помощи FileServer
:
http.Handle( "/assets/", http.StripPrefix( "/assets/", http.FileServer(http.Dir("assets")), ), )
RPC
Пакеты net/rpc
(remote procedure call — удаленный вызов процедур) и net/rpc/jsonrpc
обеспечивают простоту вызова методов по сети (а не только из программы, в которой они используются).
package main import ( "fmt" "net" "net/rpc" ) type Server struct {} func (this *Server) Negate(i int64, reply *int64) error { *reply = -i return nil } func server() { rpc.Register(new(Server)) ln, err := net.Listen("tcp", ":9999") if err != nil { fmt.Println(err) return } for { c, err := ln.Accept() if err != nil { continue } go rpc.ServeConn(c) } } func client() { c, err := rpc.Dial("tcp", "127.0.0.1:9999") if err != nil { fmt.Println(err) return } var result int64 err = c.Call("Server.Negate", int64(999), &result) if err != nil { fmt.Println(err) } else { fmt.Println("Server.Negate(999) =", result) } } func main() { go server() go client() var input string fmt.Scanln(&input) }
Эта программа похожа на пример использования TCP-сервера, за исключением того, что теперь мы создали объект, который содержит методы, доступные для вызова, а затем вызвали Negate
из функции-клиента. Посмотрите документацию по net/rpc
для получения дополнительной информации.
Получение аргументов из командной строки
При вызове команды в консоли, есть возможность передать ей определенные аргументы. Мы видели это на примере вызова команды go
:
go run myfile.go
run
и myfile.go
являются аргументами. Мы так же можем передать команде флаги:
go run -v myfile.go
Пакет flag
позволяет анализировать аргументы и флаги, переданные нашей программе. Вот пример программы, которая генерирует число от 0 до 6. Но мы можем изменить максимальное значение, передав программе флаг -max=100
.
package main import ("fmt";"flag";"math/rand") func main() { // Определение флагов maxp := flag.Int("max", 6, "the max value") // Парсинг flag.Parse() // Генерация числа от 0 до max fmt.Println(rand.Intn(*maxp)) }
Любые дополнительные не-флаговые аргументы могут быть получены с помощью flag.Args()
, которая вернет []string
.
Синхронизация примитивов
Предпочтительный способ справиться с параллелизмом и синхронизацией в Go, с помощью горутин и каналов уже описан в главе 10. Однако, Go предоставляет более традиционные способы работать с процедурами в отдельных потоках - в пакетах sync
и sync/atomic
.
Мьютексы
Мьютекс (или взаимная блокировка) единовременно блокирует часть кода в одном потоке, а так же используется для защиты общих ресурсов из не-атомарных операций. Вот пример использования мьютекса:
package main import ( "fmt" "sync" "time" ) func main() { m := new(sync.Mutex) for i := 0; i < 10; i++ { go func(i int) { m.Lock() fmt.Println(i, "start") time.Sleep(time.Second) fmt.Println(i, "end") m.Unlock() }(i) } var input string fmt.Scanln(&input) }
Когда мьютекс (m
) заблокирован из одного процесса, любые попытки повторно блокировать его из других процессов приведут к блокировке самих процессов до тех пор, пока мьютекс не будет разблокирован. Следует проявлять большую осторожность при использовании мьютексов или примитивов синхронизации из пакета sync/atomic
.
Традиционное многопоточное программирование является достаточно сложным: сделать ошибку просто, а обнаружить её трудно, поскольку она может зависеть от специфичных и редких обстоятельств. Одна из сильных сторон Go в том, что он предоставляет намного более простой и безопасный способ распараллеливания задач, чем потоки и блокировки.