Сегодня мы собираемся написать простой пакет, который позволит нам сохранять объекты Go на диск и читать их обратно.

Мы раскроем две основные функции:

func Save(path string, v interface{}) error
func Load(path string, v interface{}) error

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

Делаем код безопасным для одновременного использования

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

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

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

Создайте файл с именем persist.go и добавьте мьютекс:

package persist
import "sync"
var lock sync.Mutex

Сохранение данных

Давайте начнем с написания нашей функции сохранения:

// Save saves a representation of v to the file at path.
func Save(path string, v interface{}) error {
  lock.Lock()
  defer lock.Unlock()
}

Сначала мы блокируем наш мьютекс и откладываем разблокировку до выхода из функции.

Затем мы собираемся создать файл с помощью os.Create и отложить его закрытие.

// Save saves a representation of v to the file at path.
func Save(path string, v interface{}) error {
  lock.Lock()
  defer lock.Unlock()
  f, err := os.Create(path)
  if err != nil {
    return err
  }
  defer f.Close()
}

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

Маршалинг данных

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

Добавьте следующий код в начало persist.go:

// Marshal is a function that marshals the object into an
// io.Reader.
// By default, it uses the JSON marshaller.
var Marshal = func(v interface{}) (io.Reader, error) {
  b, err := json.MarshalIndent(v, "", "\t")
  if err != nil {
    return nil, err
  }
  return bytes.NewReader(b), nil
}

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

Здесь используется json.MarshalIndent, который для нас превратит объект v в хорошо отформатированный JSON. «Интерфейс» функции принимает объект и возвращает io.Reader, из которого будут считываться байты.

Давайте обновим функцию Save, чтобы использовать наш новый метод Marshal:

// Save saves a representation of v to the file at path.
func Save(path string, v interface{}) error {
  lock.Lock()
  defer lock.Unlock()
  f, err := os.Create(path)
  if err != nil {
    return err
  }
  defer f.Close()
  r, err := Marshal(v)
  if err != nil {
    return err
  }
  _, err = io.Copy(f, r)
  return err
}

Здесь мы вызываем Marshal, передавая объект и возвращая io.Reader, который затем записываем в файл с помощью io.Copy.

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

io.Copy - это вспомогательная функция, которая копирует все из io.Reader в io.Writer.

Демаршаллинг

Как вы уже поняли, демаршаллинг - это процесс обратного преобразования байтов в объект. Мы воспользуемся тем же трюком:

// Unmarshal is a function that unmarshals the data from the
// reader into the specified value.
// By default, it uses the JSON unmarshaller.
var Unmarshal = func(r io.Reader, v interface{}) error {
  return json.NewDecoder(r).Decode(v)
}

Unmarshal принимает io.Reader (откуда берутся байты) и целевой объект и просто передает их в json-пакет стандартной библиотеки.

Загрузка данных

Теперь мы можем сохранять данные - давайте реализуем функцию Load:

// Load loads the file at path into v.
// Use os.IsNotExist() to see if the returned error is due
// to the file being missing.
func Load(path string, v interface{}) error {
  lock.Lock()
  defer lock.Unlock()
  f, err := os.Open(path)
  if err != nil {
    return err
  }
  defer f.Close()
  return Unmarshal(f, v)
}

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

Мы используем os.Open, чтобы открыть путь для чтения перед передачей файла в Unmarshal. Закрытие файла откладывается, поэтому он будет приведен в порядок, как только Unmarshal закончит с ним.

Используя наш пакет

type obj struct {
  Name string
  Number int
  When time.Time
}
func main() {
  o := &obj{
    Name:   "Mat",
    Number: 47,
    When:   time.Now(),
  }
  if err := Save("./file.tmp", o); err != nil {
    log.Fatalln(err)
  }
  // load it back
  var o2 obj
  if err := Load("./file.tmp", o2); err != nil {
    log.Fatalln(err)
  }
  // o and o2 are now the same
  // and check out file.tmp - you'll see the JSON file
}

Домашнее задание

Я один из тех учителей в школе, которые дают вам домашнее задание, даже если сейчас каникулы.

Посмотрите, сможете ли вы придумать новую пару функций Marshal / Unmarshal, которые будут записывать данные в каком-то другом формате.