Стандартная библиотека
Вместо того, чтобы каждый раз писать всё с нуля, реальный мир программирования требует от нас умения взаимодействовать с уже существующими библиотеками. В этой главе мы рассмотрим самые часто используемые пакеты, включенные в Go.
Предупреждаю: некоторые библиотеки достаточно очевидны (или были объяснены в предыдущих главах), многие из библиотек, включённых в Go требуют специальных знаний (например: криптография). Объяснение этих технологий выходит за рамки этой книги.
Строки
Go содержит большое количество функций для работы со строками в пакетеstrings
:
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'})
Ввод / Вывод
Прежде чем мы перейдем к работе с файлами, нужно узнать про пакет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 {
// handle the error here
return
}
defer file.Close()
// get the file size
stat, err := file.Stat()
if err != nil {
return
}
// read the file
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 {
// handle the error here
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() {
// listen on a port
ln, err := net.Listen("tcp", ":9999")
if err != nil {
fmt.Println(err)
return
}
for {
// accept a connection
c, err := ln.Accept()
if err != nil {
fmt.Println(err)
continue
}
// handle the connection
go handleServerConnection(c)
}
}
func handleServerConnection(c net.Conn) {
// receive the message
var msg string
err := gob.NewDecoder(c).Decode(
&
msg)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("Received", msg)
}
c.Close()
}
func client() {
// connect to the server
c, err := net.Dial("tcp", "127.0.0.1:9999")
if err != nil {
fmt.Println(err)
return
}
// send the message
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() {
// Define flags
maxp := flag.Int("max", 6, "the max value")
// Parse
flag.Parse()
// Generate a number between 0 and 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 в том, что он предоставляет намного более простой и безопасный способ распараллеливания задач, чем потоки и блокировки.