среда, 31 марта 2021 г.

Базовое руководство по gRPC в Go

Это руководство представляет собой базовое введение в работу с gRPC в Golang.

Изучив этот пример, вы:

  • Определите службу в файле .proto.
  • Сгенерируйте серверный и клиентский код с помощью protocol buffer компилятора.
  • Используйте Go gRPC API, чтобы написать простой клиент и сервер для вашей службы.

Обратите внимание, что в примере в этом руководстве используется версия proto3 языка protocol buffers.

Зачем использовать gRPC?

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

С помощью gRPC мы можем один раз определить нашу службу в файле .proto и создать клиентов и серверы на любом из поддерживаемых языков gRPC, которые, в свою очередь, могут быть запущены в различных средах, от серверов в большом центре обработки данных до вашего собственного планшета - вся сложность общение между разными языками и средами осуществляется за вас через gRPC. Мы также получаем все преимущества работы с protocol buffers, включая эффективную сериализацию, простой IDL и легкое обновление интерфейса.

Настройка

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

Получите пример кода

Код примера является частью репозитория grpc-go.

Загрузите репозиторий в виде zip-файла и разархивируйте его или клонируйте репозиторий:

$ git clone -b v1.35.0 https://github.com/grpc/grpc-go

Перейдите в каталог с примером:

$ cd grpc-go/examples/route_guide

Определение сервисы

Нашим первым шагом является определение службы gRPC и типов запросов и ответов метода с использованием protocol buffers. Полный файл .proto находится в routeguide/route_guide.proto.

Чтобы определить службу, вы указываете именованную службу в вашем файле .proto:

service RouteGuide {
   ...
}

Затем вы определяете методы rpc внутри определения службы, указывая их типы запроса и ответа. gRPC позволяет определить четыре вида методов службы, все из которых используются в службе RouteGuide:

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

// Получает объект в заданной позиции.
rpc GetFeature(Point) returns (Feature) {}

2. RPC с потоковой передачей на стороне сервера, при котором клиент отправляет запрос на сервер и получает поток для обратного чтения последовательности сообщений. Клиент читает из возвращенного потока, пока не кончатся сообщения. Как вы можете видеть в нашем примере, вы указываете метод потоковой передачи на стороне сервера, помещая ключевое слово stream перед типом ответа.

// Получает Features, доступные в данном Rectangle. 
// Результаты передается в потоковом режиме, 
// а не возвращается сразу (например, в ответном сообщении с
// повторяющимся полем), так как прямоугольник 
// может покрывать большую площадь и содержать
// огромное количество функций.
rpc ListFeatures(Rectangle) returns (stream Feature) {}

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

// Принимает поток Points по пройденному маршруту, возвращая
// RouteSummary по завершении обхода.
rpc RecordRoute(stream Point) returns (RouteSummary) {}

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

// Принимает поток RouteNotes, 
// отправленный во время прохождения маршрута,
// при получении других RouteNotes 
// (например, от других пользователей).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

Наш файл .proto также содержит определения типов сообщений protocol buffer для всех типов запросов и ответов, используемых в наших методах сервиса - например, вот тип сообщения Point:

// Точки представлены парами широта-долгота в представлении E7
// (градусы умножены на 10**7 и 
// округлены до ближайшего целого числа).
// Широта должна быть в диапазоне +/- 90 градусов, 
// а долгота должна быть в диапазоне
// диапазон +/- 180 градусов (включительно).
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

Генерация клиентского и серверного кода

Затем нам нужно сгенерировать клиентский и серверный интерфейсы gRPC из нашего определения службы .proto. Мы делаем это с помощью protocol buffer компилятора protoc со специальным плагином gRPC Go.

В каталоге examples/route_guide выполните следующую команду:

$ protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    routeguide/route_guide.proto

Выполнение этой команды создает следующие файлы в каталоге routeguide:

  • route_guide.pb.go, который содержит весь protocol buffer код для заполнения, сериализации и извлечения типов сообщений запроса и ответа.
  • route_guide_grpc.pb.go, который содержит следующее:
    • Тип интерфейса (или заглушка) для вызовов клиентов с помощью методов, определенных в службе RouteGuide.
    • Тип интерфейса для реализации серверами, также с методами, определенными в службе RouteGuide.

Создание сервера

Сначала давайте посмотрим, как мы создаем сервер RouteGuide. Если вас интересует только создание клиентов gRPC, вы можете пропустить этот раздел и сразу перейти к созданию клиента (хотя вам все равно это может показаться интересным!).

Чтобы наша служба RouteGuide выполняла свою работу, нужно сделать две части:

  • Реализация интерфейса службы, созданного на основе нашего определения службы: выполнение фактической "работы" нашей службы.
  • Запуск сервера gRPC для прослушивания запросов от клиентов и их отправки в нужную реализацию службы.

Вы можете найти пример сервера RouteGuide в server/server.go. Давайте подробнее рассмотрим, как он работает.

Реализация RouteGuide

Как видите, у нашего сервера есть тип структуры routeGuideServer, который реализует сгенерированный интерфейс RouteGuideServer:

type routeGuideServer struct {
        ...
}
...

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
        ...
}
...

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
        ...
}
...

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
        ...
}
...

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
        ...
}
...

Простой RPC

RouteGuideServer реализует все наши методы сервиса. Давайте сначала рассмотрим простейший тип, GetFeature, который просто получает Point от клиента и возвращает информацию о соответствующей функции из своей базы данных в Feature.

func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _, feature := range s.savedFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }
  // Функция не найдена, возвращаем безымянную функцию
  return &pb.Feature{Location: point}, nil
}

В метод передается объект контекста для RPC и клиентский protocol buffer запрос Point. Он возвращает объект protocol buffer Feature с ответной информацией и ошибкой. В этом методе мы заполняем Feature соответствующей информацией, а затем возвращаем ее вместе с нулевой ошибкой, чтобы сообщить gRPC, что мы закончили работу с RPC и что Feature может быть возвращен клиенту.

RPC потоковой передачи на стороне сервера

Теперь давайте посмотрим на один из наших потоковых RPC. ListFeatures - это потоковый RPC на стороне сервера, поэтому нам нужно отправить обратно несколько функций нашему клиенту.

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
  for _, feature := range s.savedFeatures {
    if inRange(feature.Location, rect) {
      if err := stream.Send(feature); err != nil {
        return err
      }
    }
  }
  return nil
}

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

В этом методе мы заполняем столько объектов Feature, сколько нам нужно вернуть, записывая их в RouteGuide_ListFeaturesServer, используя его метод Send(). Наконец, как и в нашем простом RPC, мы возвращаем нулевую ошибку, чтобы сообщить gRPC, что мы закончили писать ответы. Если в этом вызове произойдет какая-либо ошибка, мы вернем ошибку, отличную от нуля; уровень gRPC преобразует его в соответствующий статус RPC для отправки по сети.

Клиентская потоковая передача RPC

Теперь давайте посмотрим на нечто более сложное: на клиентский метод потоковой передачи RecordRoute, где мы получаем поток Points от клиента и возвращаем один RouteSummary с информацией об их поездке. Как видите, на этот раз у метода вообще нет параметра запроса. Вместо этого он получает поток RouteGuide_RecordRouteServer, который сервер может использовать как для чтения, так и для записи сообщений - он может получать клиентские сообщения с помощью метода Recv() и возвращать свой единственный ответ с помощью метода SendAndClose().

func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error {
  var pointCount, featureCount, distance int32
  var lastPoint *pb.Point
  startTime := time.Now()
  for {
    point, err := stream.Recv()
    if err == io.EOF {
      endTime := time.Now()
      return stream.SendAndClose(&pb.RouteSummary{
        PointCount:   pointCount,
        FeatureCount: featureCount,
        Distance:     distance,
        ElapsedTime:  int32(endTime.Sub(startTime).Seconds()),
      })
    }
    if err != nil {
      return err
    }
    pointCount++
    for _, feature := range s.savedFeatures {
      if proto.Equal(feature.Location, point) {
        featureCount++
      }
    }
    if lastPoint != nil {
      distance += calcDistance(lastPoint, point)
    }
    lastPoint = point
  }
}

В теле метода мы используем метод Recv() RouteGuide_RecordRouteServer для многократного чтения в запросах нашего клиента к объекту запроса (в данном случае Point) до тех пор, пока больше не будет сообщений: серверу необходимо проверить ошибку, возвращаемую из Read() после каждого вызова. Если это nil, поток все еще в порядке, и он может продолжить чтение; если это io.EOF, поток сообщений закончился и сервер может вернуть свой RouteSummary. Если он имеет любое другое значение, мы возвращаем ошибку "как есть", чтобы уровень gRPC преобразовал ее в статус RPC.

Двунаправленный потоковый RPC

Наконец, давайте посмотрим на наш двунаправленный потоковый RPC RouteChat().

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
  for {
    in, err := stream.Recv()
    if err == io.EOF {
      return nil
    }
    if err != nil {
      return err
    }
    key := serialize(in.Location)
                ... // ищем заметки для отправки клиенту
    for _, note := range s.routeNotes[key] {
      if err := stream.Send(note); err != nil {
        return err
      }
    }
  }
}

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

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

Запуск сервера

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

flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
if err != nil {
  log.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption
...
grpcServer := grpc.NewServer(opts...)
pb.RegisterRouteGuideServer(grpcServer, newServer())
grpcServer.Serve(lis)

Чтобы собрать и запустить сервер:

1. Укажите порт, который мы хотим использовать для прослушивания клиентских запросов, используя:

lis, err := net.Listen(...).

2. Создайте экземпляр сервера gRPC с помощью grpc.NewServer(...).

3. Зарегистрируйте нашу реализацию сервиса на сервере gRPC.

4. Вызовите Serve() на сервере с данными нашего порта, чтобы выполнить блокирующее ожидание, пока процесс не будет убит или не будет вызван Stop().

Создание клиента

В этом разделе мы рассмотрим создание клиента Go для нашей службы RouteGuide. Вы можете увидеть полный пример клиентского кода в examples/route_guide/client/client.go.

Создание заглушки

Чтобы вызвать методы сервиса, нам сначала нужно создать канал gRPC для связи с сервером. Мы создаем его, передавая адрес сервера и номер порта в grpc.Dial() следующим образом:

var opts []grpc.DialOption
...
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
  ...
}
defer conn.Close()

Вы можете использовать DialOptions для установки учетных данных аутентификации (например, учетных данных TLS, GCE или JWT) в grpc.Dial, когда они требуются службе. Сервис RouteGuide не требует никаких учетных данных.

После настройки канала gRPC нам понадобится клиентская заглушка для выполнения RPC. Мы получаем ее с помощью метода NewRouteGuideClient, предоставляемого пакетом pb, сгенерированным из примера файла .proto.

client := pb.NewRouteGuideClient(conn)

Вызов сервисных методов

Теперь давайте посмотрим, как мы вызываем наши методы обслуживания. Обратите внимание, что в gRPC-Go RPC работают в блокирующем/синхронном режиме, что означает, что вызов RPC ожидает ответа сервера и либо возвращает ответ, либо ошибку.

Простой RPC

Вызов простого RPC GetFeature почти так же прост, как вызов локального метода.

feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
if err != nil {
  ...
}

Как видите, мы вызываем метод полученной ранее заглушки. В параметрах нашего метода мы создаем и заполняем объект protocol buffer запроса (в нашем случае Point). Мы также передаем объект context.Context, который позволяет нам при необходимости изменять поведение нашего RPC, например таймаут/отмену RPC в полете. Если вызов не возвращает ошибку, мы можем прочитать ответную информацию с сервера по первому возвращаемому значению.

log.Println(feature)

RPC потоковой передачи на стороне сервера

Здесь мы вызываем серверный метод потоковой передачи ListFeatures, который возвращает поток географических объектов. Если вы уже прочитали "Создание сервера", некоторые из них могут показаться вам очень знакомыми - потоковые RPC реализованы одинаковым образом с обеих сторон.

rect := &pb.Rectangle{ ... }  // инициализируем pb.Rectangle
stream, err := client.ListFeatures(context.Background(), rect)
if err != nil {
  ...
}
for {
    feature, err := stream.Recv()
    if err == io.EOF {
        break
    }
    if err != nil {
        log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
    }
    log.Println(feature)
}

Как и в простом RPC, мы передаем методу контекст и запрос. Однако вместо того, чтобы вернуть объект ответа, мы возвращаем экземпляр RouteGuide_ListFeaturesClient. Клиент может использовать поток RouteGuide_ListFeaturesClient для чтения ответов сервера.

Мы используем метод Recv() RouteGuide_ListFeaturesClient для многократного чтения ответов сервера на объект protocol buffer ответа (в данном случае Feature) до тех пор, пока больше не будет сообщений: клиенту необходимо проверять ошибку err, возвращаемую из Recv() после каждого вызов. Если nil, поток все еще в порядке, и он может продолжить чтение; если это io.EOF, значит, поток сообщений закончился; в противном случае должна быть ошибка RPC, которая пропускается через err.

Клиентская потоковая передача RPC

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

// Создаем случайное количество случайных точек
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pointCount := int(r.Int31n(100)) + 2 // Пройдите не менее двух точек
var points []*pb.Point
for i := 0; i < pointCount; i++ {
  points = append(points, randomPoint(r))
}
log.Printf("Traversing %d points.", len(points))
stream, err := client.RecordRoute(context.Background())
if err != nil {
  log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)
}
for _, point := range points {
  if err := stream.Send(point); err != nil {
    log.Fatalf("%v.Send(%v) = %v", stream, point, err)
  }
}
reply, err := stream.CloseAndRecv()
if err != nil {
  log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil)
}
log.Printf("Route summary: %v", reply)

RouteGuide_RecordRouteClient имеет метод Send(), который мы можем использовать для отправки запросов на сервер. После того, как мы закончили писать запросы нашего клиента в поток с помощью Send(), нам нужно вызвать CloseAndRecv() в потоке, чтобы сообщить gRPC, что мы закончили запись и ожидаем ответа. Мы получаем наш статус RPC из ошибки, возвращенной CloseAndRecv(). Если статус равен nil, то первым возвращаемым значением от CloseAndRecv() будет действительный ответ сервера.

Двунаправленный потоковый RPC

Наконец, давайте посмотрим на наш двунаправленный потоковый RPC RouteChat(). Как и в случае с RecordRoute, мы передаем методу только объект контекста и получаем обратно поток, который мы можем использовать как для записи, так и для чтения сообщений. Однако на этот раз мы возвращаем значения через поток нашего метода, в то время как сервер все еще записывает сообщения в свой поток сообщений.

stream, err := client.RouteChat(context.Background())
waitc := make(chan struct{})
go func() {
  for {
    in, err := stream.Recv()
    if err == io.EOF {
      // read done.
      close(waitc)
      return
    }
    if err != nil {
      log.Fatalf("Failed to receive a note : %v", err)
    }
    log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude)
  }
}()
for _, note := range notes {
  if err := stream.Send(note); err != nil {
    log.Fatalf("Failed to send a note: %v", err)
  }
}
stream.CloseSend()
<-waitc

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

Запустите пример

Выполните следующие команды из каталога examples/route_guide:

Запускаем сервер:

$ go run server/server.go

С другого терминала запустите клиент:

$ go run client/client.go

Вы увидите такой результат:

Getting feature for point (409146138, -746188906)
name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<latitude:409146138 longitude:-746188906 >
Getting feature for point (0, 0)
location:<>
Looking for features within lo:<latitude:400000000 longitude:-750000000 > hi:<latitude:420000000 longitude:-730000000 >
name:"Patriots Path, Mendham, NJ 07945, USA" location:<latitude:407838351 longitude:-746143763 >
...
name:"3 Hasta Way, Newton, NJ 07860, USA" location:<latitude:410248224 longitude:-747127767 >
Traversing 56 points.
Route summary: point_count:56 distance:497013163
Got message First message at point(0, 1)
Got message Second message at point(0, 2)
Got message Third message at point(0, 3)
Got message First message at point(0, 1)
Got message Fourth message at point(0, 1)
Got message Second message at point(0, 2)
Got message Fifth message at point(0, 2)
Got message Third message at point(0, 3)
Got message Sixth message at point(0, 3)


Читайте также:


воскресенье, 28 марта 2021 г.

Быстрый старт с gRPC в Golang

Это руководство поможет вам начать работу с gRPC в Go на простом рабочем примере.

Предпосылки

  • Go, любой из трех последних основных выпусков Go.
  • Protocol buffer компилятор, protoc, версия 3.
  • Плагины Go для компилятора protoc:

    Установите плагины компилятора protoc для Go, используя следующие команды:

    $ export GO111MODULE=on  # Включить режим модуля
    $ go get google.golang.org/protobuf/cmd/protoc-gen-go \
             google.golang.org/grpc/cmd/protoc-gen-go-grpc
    

    Обновите свой PATH, чтобы компилятор protoc мог найти плагины:

    $ export PATH="$PATH:$(go env GOPATH)/bin"
    

Получите пример кода

Код примера является частью репозитория grpc-go.

Загрузите репо в виде zip-файла и разархивируйте его или клонируйте репозиторий:

$ git clone -b v1.35.0 https://github.com/grpc/grpc-go

Перейдите в каталог примеров:

$ cd grpc-go/examples/helloworld

Запустите пример

Из каталога examples/helloworld:

  • Скомпилируйте и выполните код сервера:

    $ go run greeter_server/main.go
    

  • С другого терминала скомпилируйте и выполните клиентский код, чтобы увидеть выходные данные клиента:

    $ go run greeter_client/main.go
    Greeting: Hello world
    

Вы только что запустили клиент-серверное приложение с gRPC.

Обновите сервис gRPC

В этом разделе вы обновите приложение дополнительным серверным методом. Служба gRPC определяется с использованием protocol buffers. На данный момент все, что вам нужно знать, это то, что и сервер, и клиентская заглушка имеют RPC-метод SayHello(), который принимает параметр HelloRequest от клиента и возвращает HelloReply с сервера, и что этот метод определен следующим образом:

// Определение службы приветствия.
service Greeter {
  // Отправляет приветствие
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// Сообщение-запрос, содержащее имя пользователя.
message HelloRequest {
  string name = 1;
}

// Ответное сообщение с приветствием
message HelloReply {
  string message = 1;
}

Откройте helloworld/helloworld.proto и добавьте новый метод SayHelloAgain() с теми же типами запросов и ответов:

// Определение службы приветствия.
service Greeter {
  // Отправляет приветствие
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  // Отправляет другое приветствие
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

// Сообщение-запрос, содержащее имя пользователя.
message HelloRequest {
  string name = 1;
}

// Ответное сообщение с приветствием
message HelloReply {
  string message = 1;
}

Не забудьте сохранить файл!

Перегенерировать код gRPC

Прежде чем вы сможете использовать новый метод сервиса, вам необходимо перекомпилировать обновленный файл .proto.

Находясь в каталоге examples/helloworld, выполните следующую команду:

$ protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    helloworld/helloworld.proto

Это перегенерирует файлы helloworld/helloworld.pb.go и helloworld/helloworld_grpc.pb.go, которые содержат:

  • Код для заполнения, сериализации и получения типов сообщений HelloRequest и HelloReply.
  • Сгенерированный клиентский и серверный код.

Обновите и запустите приложение

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

Обновите сервер

Откройте greeter_server/main.go и добавьте к нему следующую функцию:

func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
        return &pb.HelloReply{Message: "Hello again " + in.GetName()}, nil
}

Обновите клиент

Откройте greeter_client/main.go, чтобы добавить следующий код в конец тела функции main():

r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name})
if err != nil {
        log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())

Не забудьте сохранить изменения.

Запустите

Запустите клиент и сервер, как и раньше. Выполните следующие команды из каталога examples/helloworld:

Запускаем сервер:

$ go run greeter_server/main.go

С другого терминала запустите клиент. На этот раз добавьте имя в качестве аргумента командной строки:

$ go run greeter_client/main.go Alice

Вы увидите следующий результат:

Greeting: Hello Alice
Greeting: Hello again Alice


Читайте также:


пятница, 26 марта 2021 г.

Новые изменения модулей в Go 1.16

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

Модули включены по умолчанию

Команда go теперь по умолчанию создает пакеты в режиме с поддержкой модулей, даже если go.mod отсутствует. Это большой шаг к использованию модулей во всех проектах.

По-прежнему можно создавать пакеты в режиме GOPATH, отключив переменную среды GO111MODULE. Вы также можете установить GO111MODULE на auto, чтобы включить режим с поддержкой модулей, только если файл go.mod присутствует в текущем или любом родительском каталоге. Раньше это было по умолчанию. Обратите внимание, что вы можете установить GO111MODULE и другие переменные навсегда с помощью go env -w:

go env -w GO111MODULE=auto

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

Никаких автоматических изменений go.mod и go.sum

Раньше, когда команда go обнаруживала проблему с go.mod или go.sum, например, отсутствующую директиву require или отсутствующую сумму, она пыталась устранить проблему автоматически. Было получено много отзывов о том, что такое поведение было неожиданным, особенно для таких команд, как go list, которые обычно не имеют побочных эффектов. Автоматические исправления не всегда были желательными: если импортированный пакет не был предоставлен каким-либо обязательным модулем, команда go добавляла новую зависимость, возможно, вызывая обновления общих зависимостей. Даже неверный путь импорта приведет к (неудачному) поиску в сети.

В Go 1.16 команды с поддержкой модулей сообщают об ошибке после обнаружения проблемы в go.mod или go.sum, вместо того, чтобы пытаться исправить проблему автоматически. В большинстве случаев сообщение об ошибке рекомендует команду для устранения проблемы.

$ go build
example.go:3:8: no required module provides package golang.org/x/net/html; to add it:
    go get golang.org/x/net/html
$ go get golang.org/x/net/html
$ go build

Как и раньше, команда go может использовать каталог vendor, если он присутствует. Такие команды, как go get and go mod tidy, по-прежнему изменяют go.mod и go.sum, поскольку их основная цель - управлять зависимостями.

Установка исполняемого файла определенной версии

Команда go install теперь может установить исполняемый файл определенной версии, указав суффикс @version.

go install golang.org/x/tools/gopls@v0.6.5

При использовании этого синтаксиса go install устанавливает команду из этой точной версии модуля, игнорируя любые файлы go.mod в текущем каталоге и родительских каталогах. (Без суффикса @version go install продолжает работать как всегда, собирая программу с использованием требований к версии и замен, перечисленных в go.mod текущего модуля.)

Раньше рекомендовалось go get -u program для установки исполняемого файла, но это использование вызвало слишком сильную путаницу со значением go get для добавления или изменения требований к версии модуля в go.mod. И чтобы избежать случайного изменения go.mod, люди начали предлагать более сложные команды, такие как:

cd $HOME; GO111MODULE=on go get program@latest

Теперь мы все можем использовать go install program@latest.

Чтобы устранить двусмысленность в отношении используемых версий, существует несколько ограничений на то, какие директивы могут присутствовать в файле go.mod программы при использовании этого синтаксиса установки. В частности, директивы replace и exclude запрещены, по крайней мере, на данный момент. В долгосрочной перспективе, когда новая программа go install program@version будет работать хорошо для достаточного количества вариантов использования, планируется сделать так, чтобы go get прекратил установку двоичных файлов команд.

Отзыв модуля

Вы когда-нибудь случайно публиковали версию модуля до того, как она была готова? Или вы обнаружили проблему сразу после публикации версии, которую нужно было быстро исправить? Ошибки в опубликованных версиях трудно исправить. Чтобы сборки модулей оставались детерминированными, версия не может быть изменена после публикации. Даже если вы удалите или измените тег версии, proxy.golang.org и другие прокси-серверы, вероятно, уже имеют исходный кешированный файл.

Авторы модулей теперь могут отзывать версии модулей с помощью директивы retract в go.mod. Отозванная версия все еще существует и может быть загружена (поэтому сборки, которые от нее зависят, не сломаются), но команда go не выберет ее автоматически при разрешении версий, таких как @latest. go get and go list -m -u выведет предупреждения о существующем использовании.

Например, предположим, что автор популярной библиотеки example.com/lib выпускает v1.0.5, а затем обнаруживает новую проблему безопасности. Он может добавить в свой файл go.mod директиву, подобную приведенной ниже:

// Remote-triggered crash in package foo. See CVE-2021-01234.
retract v1.0.5

Затем автор может пометить и опубликовать версию v1.0.6, новую высшую версию. После этого пользователи, которые уже зависят от версии 1.0.5, будут уведомлены об отзыве при проверке наличия обновлений или при обновлении зависимого пакета. Сообщение с уведомлением может включать текст из комментария над директивой retract.

$ go list -m -u all
example.com/lib v1.0.5 (retracted)
$ go get .
go: warning: example.com/lib@v1.0.5: retracted by module author:
    Remote-triggered crash in package foo. See CVE-2021-01234.
go: to switch to the latest unretracted version, run:
    go get example.com/lib@latest

Управление инструментами контроля версий с помощью GOVCS

Команда go может загружать исходный код модуля с зеркала, такого как proxy.golang.org, или напрямую из репозитория системы управления версиями, используя git, hg, svn, bzr или fossil. Прямой доступ к управлению версиями важен, особенно для частных модулей, которые недоступны на прокси-серверах, но это также потенциально проблема безопасности: ошибка в инструменте управления версиями может быть использована вредоносным сервером для запуска непредусмотренного кода.

Go 1.16 представляет новую конфигурационную переменную GOVCS, которая позволяет пользователю указывать, каким модулям разрешено использовать определенные инструменты контроля версий. GOVCS принимает разделенный запятыми список правил pattern:vcslist. pattern - это path.Match шаблон соответствующий одному или нескольким ведущим элементам пути к модулю. Специальные шаблоны public и private соответствуют общедоступным и частным модулям (private определяется как модули, соответствующие шаблонам в GOPRIVATE; public - это все остальное). Vcslist - это список разрешенных команд управления версиями, разделенных вертикальной чертой, или ключевое слово all или off.

Например:

GOVCS=github.com:git,evil.com:off,*:git|hg

С этой настройкой модули с путями на github.com можно загружать с помощью git; пути на evil.com нельзя загрузить с помощью какой-либо команды управления версиями, а все другие пути (* соответствует всему) можно загрузить с помощью git или hg.

Если GOVCS не установлен или модуль не соответствует какому-либо шаблону, команда go использует значение по умолчанию: git и hg разрешены для общедоступных модулей, а все инструменты разрешены для частных модулей. Причина, по которой разрешено использование только Git и Mercurial, заключается в том, что эти две системы уделяли наибольшее внимание вопросам работы в качестве клиентов ненадежных серверов. Напротив, Bazaar, Fossil и Subversion в основном использовались в надежных, аутентифицированных средах и не так тщательно изучаются, как поверхности для атак. То есть настройка по умолчанию:

GOVCS=public:git|hg,private:all


Читайте также:


четверг, 25 марта 2021 г.

Контексты и структуры в Golang

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

В документации для контекста указано:

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

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

Предпочитать контексты, передаваемые в качестве аргументов

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

type Worker struct { /* … */ }

type Work struct { /* … */ }

func New() *Worker {
  return &Worker{}
}

func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
  _ = ctx 
  // ctx для каждого вызова используется для отмены, 
  // крайних сроков и метаданных.
}

func (w *Worker) Process(ctx context.Context, work *Work) error {
  _ = ctx
  // ctx для каждого вызова используется для отмены, 
  // крайних сроков и метаданных.
}

Здесь методы (*Worker).Fetch и (*Worker).Process принимают контекст напрямую. Благодаря этой конструкции передачи в качестве аргумента пользователи могут устанавливать для каждого вызова крайние сроки, отмену и метаданные. И ясно, как будет использоваться context.Context, переданный каждому методу: нет никаких ожиданий, что context.Context, переданный одному методу, будет использоваться любым другим методом. Это связано с тем, что контекст ограничен настолько малой операцией, насколько это необходимо, что значительно увеличивает полезность и ясность контекста в этом пакете.

Хранение контекста в структурах приводит к путанице

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

type Worker struct {
  ctx context.Context
}

func New(ctx context.Context) *Worker {
  return &Worker{ctx: ctx}
}

func (w *Worker) Fetch() (*Work, error) {
  _ = w.ctx
  // Общий w.ctx используется для отмены, 
  // крайних сроков и метаданных.
}

func (w *Worker) Process(work *Work) error {
  _ = w.ctx 
  // Общий w.ctx используется для отмены, 
  // крайних сроков и метаданных.
}

Оба метода (*Worker).Fetch и (*Worker).Process используют контекст, хранящийся в Worker. Это не позволяет вызывающим объектам Fetch и Process (которые сами могут иметь разные контексты) указывать крайний срок, запрашивать отмену и прикреплять метаданные для каждого вызова. Например: пользователь не может указать крайний срок только для (*Worker).Fetch или отменить только вызов (*Worker).Process. Время жизни вызывающей стороны смешано с общим контекстом, а контекст ограничен временем жизни, в котором создается Worker.

API также намного больше сбивает пользователей с толку по сравнению с подходом с передачей в качестве аргумента. Пользователи могут спросить себя:

  • Поскольку New принимает context.Context, выполняет ли конструктор работу, требующую отмены или крайних сроков?
  • Применяется ли context.Context, переданный в New, для работы в (*Worker).Fetch и (*Worker).Process? Ни один? Один, а не другой?

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

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

Исключение из правила: сохранение обратной совместимости

Когда был выпущен Go 1.7, который представил context.Context, большому количеству API пришлось добавить поддержку контекста обратно совместимыми способами. Например, методы Client net/http, такие как Get и Do, были отличными кандидатами в контекст. Каждый внешний запрос, отправленный с помощью этих методов, выиграет от наличия крайнего срока, отмены и поддержки метаданных, которые поставляются с context.Context.

Существует два подхода к добавлению поддержки context.Context обратно совместимыми способами: включение контекста в структуру, как мы вскоре увидим, и дублирование функций с дублированием, принимающим context.Context и имеющим Context в качестве суффикса имени функции. Подход с дублированием должен быть предпочтительнее контекста-в-структуре и более подробно обсуждается в посте Обеспечение совместимости ваших модулей. Однако в некоторых случаях это непрактично: например, если ваш API предоставляет большое количество функций, то дублирование их всех может оказаться невозможным.

Пакет net/http выбрал подход контекст-в-структуре, который представляет собой полезный пример. Давайте посмотрим на Do. До введения context.Context Do определялся следующим образом:

func (c *Client) Do(req *Request) (*Response, error)

После Go 1.7 Do мог бы выглядеть следующим образом, если бы не тот факт, что это нарушило бы обратную совместимость:

func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)

Но сохранение обратной совместимости и соблюдение обещания совместимости Go 1 имеет решающее значение для стандартной библиотеки. Поэтому вместо этого разработчики решили добавить context.Context в структуру http.Request, чтобы разрешить поддержку context.Context без нарушения обратной совместимости:

type Request struct {
  ctx context.Context

  // ...
}

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
  // Упрощено для краткости.
  return &Request{
    ctx: ctx,
    // ...
  }
}

func (c *Client) Do(req *Request) (*Response, error)

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

func (c *Client) Call() error {
  return c.CallContext(context.Background())
}

func (c *Client) CallContext(ctx context.Context) error {
  // ...
}

Вывод

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

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

При разработке API с контекстом помните совет: передайте context.Context в качестве аргумента; не храните его в структурах.


Читайте также:


среда, 24 марта 2021 г.

Управление зависимостями в Golang

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

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

Рабочий процесс для использования и управления зависимостями

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

Ниже перечислены наиболее распространенные шаги управления зависимостями.

  • Найдите полезные пакеты на pkg.go.dev.
  • Импортируйте нужные вам пакеты в свой код.
  • Добавьте свой код в модуль для отслеживания зависимостей (если его еще нет в модуле).
  • Добавьте внешние пакеты в качестве зависимостей, чтобы вы могли ими управлять.
  • Обновляйте или понижайте версии зависимостей по мере необходимости.

Управление зависимостями как модулями

В Go вы управляете зависимостями как модулями, которые содержат импортируемые вами пакеты. Этот процесс поддерживается:

  • Децентрализованной системой для публикации модулей и получения их кода. Разработчики делают свои модули доступными для использования другими разработчиками из собственного репозитория и публикуют с номером версии.
  • Системой поиска пакетов и браузером документации (pkg.go.dev), в котором вы можете найти модули.
  • Соглашением о нумерации версий модуля, которое поможет вам понять гарантии стабильности и обратной совместимости модуля.
  • Инструментами Go, которые упрощают управление зависимостями, включая получение исходного кода модуля, обновление и т. д.

Поиск и импорт полезных пакетов

Вы можете выполнить поиск по pkg.go.dev, чтобы найти пакеты с функциями, которые могут оказаться полезными.

Когда вы нашли пакет, который хотите использовать в своем коде, найдите путь к пакету вверху страницы и нажмите кнопку Copy path (Копировать путь), чтобы скопировать путь в буфер обмена. В своем собственном коде вставьте путь в утверждение import, как в следующем примере:

import "rsc.io/quote"

После того, как ваш код импортирует пакет, включите отслеживание зависимостей и получите код пакета для компиляции.

Включение отслеживания зависимостей в вашем коде

Чтобы отслеживать и управлять добавляемыми зависимостями, вы начинаете с помещения кода в отдельный модуль. Это создает файл go.mod в корне дерева вашего исходного кода. Добавленные вами зависимости будут перечислены в этом файле.

Чтобы добавить свой код в собственный модуль, используйте команду go mod init. Например, из командной строки перейдите в корневой каталог кода, а затем выполните команду, как показано в следующем примере:

$ go mod init example.com/mymodule

Аргументом команды go mod init является путь к модулю вашего модуля. Если возможно, путь к модулю должен быть местоположением репозитория вашего исходного кода. Если сначала вы не знаете возможное местоположение репозитория модуля, рассмотрите возможность временного использования безопасной замены, такой как имя вашего домена или example.com, а также путь, следующий из имени модуля или исходного каталога.

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

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

Включите файлы go.mod и go.sum в свой репозиторий вместе с кодом.

Добавление зависимости

После импорта пакетов из опубликованного модуля вы можете добавить этот модуль для управления в качестве зависимости с помощью команды go get.

Команда делает следующее:

  • При необходимости она добавляет в файл go.mod директивы require для модулей, необходимых для сборки пакетов, названных в командной строке. Директива require отслеживает минимальную версию модуля, от которой зависит ваш модуль.
  • При необходимости она загружает исходный код модуля, чтобы вы могли компилировать пакеты, зависящие от них. Она может загружать модули с прокси-сервера модуля, такого как proxy.golang.org, или непосредственно из репозиториев системы контроля версий. Источник кешируется локально.
    Вы можете указать место, откуда инструменты Go загружают модули.

Ниже приводится несколько примеров.

  • Чтобы добавить все зависимости для пакета в свой модуль, выполните команду, подобную приведенной ниже ("." относится к пакету в текущем каталоге):

    $ go get .
    

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

    $ go get example.com/theirmodule
    

Команда также аутентифицирует каждый загружаемый модуль. Это гарантирует, что он не изменится с момента публикации модуля. Если модуль был изменен с момента публикации - например, разработчик изменил содержимое коммита - инструменты Go выдадут ошибку безопасности. Эта проверка аутентификации защищает вас от модулей, которые могли быть взломаны.

Получение конкретной версии зависимости

Вы можете получить конкретную версию модуля зависимостей, указав ее версию в команде go get. Команда обновляет директиву require в вашем файле go.mod (хотя вы также можете обновить ее вручную).

Вы можете сделать это, если:

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

Вот примеры использования команды go get:

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

    $ go get example.com/theirmodule@v1.3.4
    

  • Чтобы получить последнюю версию, добавьте путь к модулю с помощью @latest:

    $ go get example.com/theirmodule@latest
    

В следующем примере директивы require для файла go.mod показано, как требовать конкретный номер версии:

require example.com/theirmodule v1.3.4

Обнаружение доступных обновлений

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

Вот пара примеров.

  • Перечислить все модули, которые являются зависимостями вашего текущего модуля, вместе с последней доступной версией для каждого:

    $ go list -m -u all
    

  • Показать последнюю версию, доступную для конкретного модуля:

    $ go list -m -u example.com/theirmodule
    

Обновление или понижение уровня зависимости

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

  1. Для обнаружения новых версий используйте команду go list.
  2. Чтобы добавить определенную версию в качестве зависимости, используйте команду go get.

Синхронизация зависимостей вашего кода

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

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

Чтобы ваш управляемый набор зависимостей оставался аккуратным, используйте команду go mod tidy. Используя набор пакетов, импортированных в ваш код, эта команда редактирует ваш файл go.mod, добавляя необходимые, но отсутствующие модули. Она также удаляет неиспользуемые модули, которые не предоставляют никаких соответствующих пакетов.

У команды нет аргументов, кроме одного флага -v, который выводит информацию об удаленных модулях.

$ go mod tidy

Разработка и тестирование неопубликованного кода модуля

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

Возможно, вы захотите сделать это, когда:

  • Вы хотите внести свои собственные изменения в код внешнего модуля, например, после его разветвления и/или клонирования. Например, вы можете подготовить исправление для модуля, а затем отправить его в виде pull запроса разработчику модуля.
  • Вы создаете новый модуль и еще не опубликовали его, поэтому он недоступен в репозитории, где его может получить команда go get.

Требование кода модуля в локальном каталоге

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

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

Чтобы указать командам Go использовать локальную копию кода модуля, используйте директиву replace в файле go.mod, чтобы заменить путь к модулю, указанный в директиве require.

В следующем примере файла go.mod текущему модулю требуется внешний модуль example.com/theirmodule с несуществующим номером версии (v0.0.0-unpublished), который используется для обеспечения правильной работы замены. Затем директива replace заменяет исходный путь модуля на ../theirmodule, каталог, который находится на том же уровне, что и каталог текущего модуля.

module example.com/mymodule

go 1.16

require example.com/theirmodule v0.0.0-unpublished

replace example.com/theirmodule v0.0.0-unpublished => ../theirmodule

При настройке пары require/replace используйте команды go mod edit и go get, чтобы требования, описанные в файле, оставались согласованными:

$ go mod edit -replace=example.com/theirmodule@v0.0.0-unpublished=../theirmodule
$ go get -d example.com/theirmodule@v0.0.0-unpublished

Примечание. Когда вы используете директиву replace, инструменты Go не аутентифицируют внешние модули.

Требование кода внешнего модуля из вашего форка репозитория

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

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

В следующем примере файла go.mod текущему модулю требуется внешний модуль example.com/theirmodule. Затем директива replace заменяет исходный путь модуля на example.com/myfork/theirmodule, форк собственного репозитория модуля.

module example.com/mymodule

go 1.16

require example.com/theirmodule v1.2.3

replace example.com/theirmodule v1.2.3 => example.com/myfork/theirmodule v1.2.3-fixed

При настройке пары require/replace используйте команды инструмента Go, чтобы гарантировать, что требования, описанные в файле, остаются согласованными. Используйте команду go list, чтобы получить версию, используемую текущим модулем. Затем используйте команду go mod edit, чтобы заменить требуемый модуль форком:

$ go list -m example.com/theirmodule
example.com/theirmodule v1.2.3
$ go mod edit -replace=example.com/theirmodule@v1.2.3=example.com/myfork/theirmodule v1.2.3-fixed

Примечание. Когда вы используете директиву replace, инструменты Go не аутентифицируют внешние модули.

Получение конкретного коммита с использованием идентификатора репозитория

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

Для этого вы используете команду go get, указав нужный код знаком @. Когда вы используете go get, команда добавит в ваш файл go.mod директиву require, для которой требуется внешний модуль, используя номер псевдоверсии, основанный на деталях коммита.

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

  • Чтобы получить модуль в конкретном коммите, добавьте форму @commithash:

    $ go get example.com/theirmodule@4cf76c2
    

  • Чтобы получить модуль в определенной ветке, добавьте форму @branchname:

    $ go get example.com/theirmodule@bugfixes
    

Удаление зависимости

Когда ваш код больше не использует какие-либо пакеты в модуле, вы можете перестать отслеживать модуль как зависимость.

Чтобы остановить отслеживание всех неиспользуемых модулей, запустите команду go mod tidy. Эта команда также может добавлять недостающие зависимости, необходимые для сборки пакетов в вашем модуле.

$ go mod tidy

Чтобы удалить конкретную зависимость, используйте команду go get, указав путь к модулю модуля и добавив @none, как в следующем примере:

$ go get example.com/theirmodule@none

Команда go get также понижает или удаляет другие зависимости, которые зависят от удаленного модуля.

Указание прокси-сервера модуля

Когда вы используете инструменты Go для работы с модулями, они по умолчанию загружают модули с proxy.golang.org (общедоступное зеркало модуля, запущенное Google) или непосредственно из репозитория модуля. Вы можете указать, что инструменты Go должны вместо этого использовать другой прокси-сервер для загрузки и аутентификации модулей.

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

Чтобы указать другой прокси-сервер модуля для использования инструментами Go, установите в переменной среды GOPROXY URL-адрес одного или нескольких серверов. Инструменты Go будут пробовать каждый URL в указанном вами порядке. По умолчанию GOPROXY сначала указывает общедоступный прокси-сервер модуля, запущенный Google, а затем прямую загрузку из репозитория модуля (как указано в его пути к модулю):

GOPROXY="https://proxy.golang.org,direct"

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

1. Когда вы используете запятую, инструменты Go будут пытаться использовать следующий URL-адрес в списке, только если текущий URL-адрес возвращает HTTP 404 или 410.

GOPROXY="https://proxy.example.com,https://proxy2.example.com"

2. Когда вы используете канал (|), инструменты Go будут пытаться использовать следующий URL-адрес в списке независимо от кода ошибки HTTP.

GOPROXY="https://proxy.example.com|https://proxy2.example.com"


Читайте также:


вторник, 23 марта 2021 г.

Быстрое начало с Go

В этом руководстве вы получите краткое введение в программирование на Go. По пути вы:

  • Установите Go (если вы еще этого не сделали).
  • Напишите простой "Hello, world" код.
  • Используете команду go для запуска вашего кода.
  • Используете инструмент обнаружения пакетов Go, чтобы найти пакеты, которые вы можете использовать в своем собственном коде.
  • Вызовете функцию внешнего модуля.

Предпосылки

  • Некоторый опыт программирования. Код здесь довольно простой, но он помогает кое-что узнать о функциях.
  • Инструмент для редактирования вашего кода. Любой текстовый редактор, который у вас есть, будет работать нормально. Большинство текстовых редакторов хорошо поддерживают Go. Наиболее популярными являются VSCode (бесплатно), GoLand (платно) и Vim (бесплатно).
  • Командный терминал. Go хорошо работает с любым терминалом в Linux и Mac, а также с PowerShell или cmd в Windows.

Установите Go

Просто используйте шаги загрузки и установки.

Напишите код

Начните с Hello, World.

1. Откройте командную строку и перейдите в домашний каталог.

В Linux или Mac:

cd

В Windows:

cd %HOMEPATH%

2. Создайте каталог hello для вашего исходного кода Go.

Например, используйте следующие команды:

mkdir hello
cd hello

3. Включите отслеживание зависимостей для вашего кода.

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

Чтобы включить отслеживание зависимостей для вашего кода путем создания файла go.mod, запустите команду go mod init, указав ей имя модуля, в котором будет находиться ваш код. Имя - это путь модуля к модулю. В большинстве случаев это будет место репозитория, в котором будет храниться ваш исходный код, например github.com/mymodule. Если вы планируете опубликовать свой модуль для использования другими, путь модуля должен быть местом, из которого инструменты Go могут загрузить ваш модуль.

Для целей этого руководства просто используйте example.com/hello.

$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello

4. В текстовом редакторе создайте файл hello.go, в котором вы будете писать свой код.

5. Вставьте следующий код в файл hello.go и сохраните файл.

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Это ваш код Go. В этом коде вы:

  • Объявляете основной (main) пакет (пакет - это способ группировки функций, состоящий из всех файлов в одном каталоге).
  • Импортируете популярный пакет fmt, который содержит функции для форматирования текста, включая печать на консоль. Этот пакет является одним из пакетов стандартной библиотеки, которые вы получили при установке Go.
  • Реализуете основную (main) функцию для вывода сообщения на консоль. Основная (main) функция выполняется по умолчанию при запуске основного (main) пакета.

6. Запустите свой код, чтобы увидеть приветствие.

$ go run .
Hello, World!

Команда go run - одна из многих команд go, которые вы будете использовать для работы с Go. Используйте следующую команду, чтобы получить список остальных:

$ go help

Вызов кода во внешнем пакете

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

1. Сделайте ваше сообщение немного более интересным с помощью функции из внешнего модуля.

  • Посетите pkg.go.dev и найдите пакет "quote".
  • Найдите и щелкните на пакет rsc.io/quote в результатах поиска (если вы видите rsc.io/quote/v3, пока не обращайте на него внимания).
  • В разделе Documentation в разделе Index обратите внимание на список функций, которые вы можете вызывать из своего кода. Вы воспользуетесь функцией Go.
  • Обратите внимание, что в верхней части этой страницы пакет quote включен в модуль rsc.io/quote.

Вы можете использовать сайт pkg.go.dev, чтобы найти опубликованные модули, в пакетах которых есть функции, которые вы можете использовать в своем собственном коде. Пакеты публикуются в модулях, таких как rsc.io/quote, где их могут использовать другие. Со временем модули улучшаются новыми версиями, и вы можете обновить свой код, чтобы использовать улучшенные версии.

2. В коде Go импортируйте пакет rsc.io/quote и добавьте вызов его функции Go.

После добавления выделенных строк ваш код должен включать следующее:

package main

import "fmt"

import "rsc.io/quote"

func main() {
    fmt.Println(quote.Go())
}

3. Добавьте новые требования к модулю и суммы.

Go добавит модуль quote в качестве требования, а также файл go.sum для использования при аутентификации модуля.

$ go mod tidy
go: finding module for package rsc.io/quote
go: found rsc.io/quote in rsc.io/quote v1.5.2

4. Запустите свой код, чтобы увидеть сообщение, созданное вызываемой функцией.

$ go run .
Don't communicate by sharing memory, share memory by communicating.

Обратите внимание, что ваш код вызывает функцию Go, выводя сообщение о коммуникации.

Когда вы запустили go mod tidy, он обнаружил и загрузил модуль rsc.io/quote, содержащий импортированный вами пакет. По умолчанию загружена последняя версия - v1.5.2.


Читайте также:


понедельник, 22 марта 2021 г.

Работа с Redis, пакет github.com/gomodule/redigo/redis

Пакет redis (github.com/gomodule/redigo/redis) - это клиент для базы данных Redis.

Подключения

Интерфейс Conn - это основной интерфейс для работы с Redis. Приложения создают соединения, вызывая функции Dial, DialWithTimeout или NewConn. В будущем будут добавлены функции для создания сегментированных и других типов подключений.

Приложение должно вызвать метод Close соединения, когда приложение завершит соединение.

Выполнение команд

Интерфейс Conn имеет общий метод для выполнения команд Redis:

Do(commandName string, args ...interface{}) (reply interface{}, err error)

В справочнике по командам Redis (http://redis.io/commands) перечислены доступные команды. Пример использования команды Redis APPEND:

n, err := conn.Do("APPEND", "key", "value")

Метод Do преобразует аргументы команды в bulk строки для передачи на сервер следующим образом:

Go тип                Преобразование
[]byte                Отправляется как есть
string                Отправляется как есть
int, int64            strconv.FormatInt(v)
float64               strconv.FormatFloat(v, 'g', -1, 64)
bool                  true -> "1", false -> "0"
nil                   ""
все остальные типы    fmt.Fprint(w, v)

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

Redis тип               Go тип
error                   redis.Error
integer                 int64
simple string           string
bulk string             []byte или nil если значение отсутствует.
array                   []interface{} или nil если значение отсутствует.

Используйте утверждения типа или вспомогательные функции ответа для преобразования из interface{} в конкретный тип Go для результата команды.

Конвейерная обработка

Соединения поддерживают конвейерную обработку с использованием методов Send, Flush и Receive.

Send(commandName string, args ...interface{}) error
Flush() error
Receive() (reply interface{}, err error)

Send записывает команду в выходной буфер соединения. Flush сбрасывает выходной буфер соединения на сервер. Receive читает один ответ от сервера. В следующем примере показан простой конвейер.

c.Send("SET", "foo", "bar")
c.Send("GET", "foo")
c.Flush()
c.Receive() // ответ от SET
v, err = c.Receive() // ответ от GET

Метод Do сочетает в себе функциональность методов Send, Flush и Receive. Метод Do начинается с записи команды и очистки выходного буфера. Затем метод Do получает все ожидающие ответы, включая ответ на команду, только что отправленную Do. Если какой-либо из полученных ответов является ошибкой, то Do возвращает ошибку. Если ошибок нет, Do возвращает последний ответ. Если аргумент команды для метода Do - "", то метод Do очистит выходной буфер и получит ожидающие ответы без отправки команды.

Используйте методы Send и Do для реализации конвейерных транзакций.

c.Send("MULTI")
c.Send("INCR", "foo")
c.Send("INCR", "bar")
r, err := c.Do("EXEC")
fmt.Println(r) // печатает [1, 1]

Конкурентность

Соединения поддерживают одного конкурентного вызывающего объекта метода Receive и одного конкурентного вызывающего объекта методов Send и Flush. Никакой другой конкурентности не поддерживается, включая конкурентные вызовы методов Do и Close.

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

Опубликовать и подписаться

Используйте методы Send, Flush и Receive для реализации подписчиков Pub/Sub.

c.Send("SUBSCRIBE", "example")
c.Flush()
for {
    reply, err := c.Receive()
    if err != nil {
        return err
    }
    // обрабатываем отправленное сообщение
}

Тип PubSubConn оборачивает Conn удобными методами для реализации подписчиков. Методы Subscribe, PSubscribe, Unsubscribe и PUnsubscribe отправляют и сбрасывают команду управления подпиской. Метод получения преобразует отправленное сообщение в удобные типы для использования в переключателе типа.

psc := redis.PubSubConn{Conn: c}
psc.Subscribe("example")
for {
    switch v := psc.Receive().(type) {
    case redis.Message:
        fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
    case redis.Subscription:
        fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
    case error:
        return v
    }
}

Помощники ответа

Функции Bool, Int, Bytes, String, Strings и Values ​​преобразуют ответ в значение определенного типа. Чтобы обеспечить удобную упаковку вызовов методов соединения Do и Receive, функции принимают второй аргумент типа error. Если ошибка не равна nil, то вспомогательная функция возвращает ошибку. Если ошибка равна nil, функция преобразует ответ в указанный тип:

exists, err := redis.Bool(c.Do("EXISTS", "foo"))
if err != nil {
    // обрабатываем возврат ошибки из c.Do 
    // или ошибку преобразования типа.
}

Функция Scan преобразует элементы ответа массива в типы Go:

var value1 int
var value2 string
reply, err := redis.Values(c.Do("MGET", "key1", "key2"))
if err != nil {
    // обрабатываем ошибки
}
 if _, err := redis.Scan(reply, &value1, &value2); err != nil {
    // обрабатываем ошибки
}

Ошибки

Методы подключения возвращают с сервера ответы об ошибках в виде типа redis.Error.

Вызовите метод соединения Err(), чтобы определить, возникла ли в соединении неисправимая ошибка, например сетевая ошибка или ошибка анализа протокола. Если Err() возвращает значение, отличное от nil, соединение непригодно для использования и должно быть закрыто.

Пример (Zpop)

В этом примере реализуется ZPOP, как описано на http://redis.io/topics/transactions, с использованием WATCH/MULTI/EXEC и сценариев.

package main

import (
	"fmt"

	"github.com/gomodule/redigo/redis"
)

// zpop извлекает значение из ключа ZSET 
// с помощью команд WATCH/MULTI/EXEC.
func zpop(c redis.Conn, key string) (result string, err error) {

	defer func() {
		// Вернуть соединение в нормальное состояние 
		// при ошибке.
		if err != nil {
			c.Do("DISCARD") // nolint: errcheck
		}
	}()

	// Цикл, пока транзакция не будет успешной.
	for {
		if _, err := c.Do("WATCH", key); err != nil {
			return "", err
		}

		members, err := redis.Strings(c.Do("ZRANGE", key, 0, 0))
		if err != nil {
			return "", err
		}
		if len(members) != 1 {
			return "", redis.ErrNil
		}

		if err = c.Send("MULTI"); err != nil {
			return "", err
		}
		if err = c.Send("ZREM", key, members[0]); err != nil {
			return "", err
		}
		queued, err := c.Do("EXEC")
		if err != nil {
			return "", err
		}

		if queued != nil {
			result = members[0]
			break
		}
	}

	return result, nil
}

// zpopScript извлекает значение из ZSET.
var zpopScript = redis.NewScript(1, `
    local r = redis.call('ZRANGE', KEYS[1], 0, 0)
    if r ~= nil then
        r = r[1]
        redis.call('ZREM', KEYS[1], r)
    end
    return r
`)

// В этом примере реализуется ZPOP, как описано в
// http://redis.io/topics/transactions 
// с использованием WATCH/MULTI/EXEC и сценариев.
func main() {
	c, err := dial()
	if err != nil {
		fmt.Println(err)
		return
	}
	defer c.Close()

	// Добавляем тестовые данные с помощью конвейера.

	for i, member := range []string{"red", "blue", "green"} {
		if err = c.Send("ZADD", "zset", i, member); err != nil {
			fmt.Println(err)
			return
		}
	}

	if _, err := c.Do(""); err != nil {
		fmt.Println(err)
		return
	}

	// Извлечь с помощью WATCH/MULTI/EXEC

	v, err := zpop(c, "zset")
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(v)

	// Извлечь с помощью скрипта.

	v, err = redis.String(zpopScript.Do(c, "zset"))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(v)

}

Вывод:

red
blue


Читайте также:


воскресенье, 21 марта 2021 г.

Как писать Go код (с использованием модулей)

Этот пост демонстрирует разработку простого пакета Go внутри модуля и знакомит с инструментом go, стандартным способом получения, сборки и установки модулей, пакетов и команд Go.

Примечание. В этом посте предполагается, что вы используете Go 1.13 или новее, а переменная среды GO111MODULE не установлена. Если вы ищете старую версию этого поста, предшествующую модулям, она находится здесь.

Организация кода

Программы Go организованы в пакеты. Пакет - это набор исходных файлов в одном каталоге, которые скомпилированы вместе. Функции, типы, переменные и константы, определенные в одном исходном файле, видны всем другим исходным файлам в том же пакете.

Репозиторий содержит один или несколько модулей. Модуль - это набор связанных пакетов Go, которые выпускаются вместе. Репозиторий Go обычно содержит только один модуль, расположенный в корне репозитория. Файл с именем go.mod объявляет путь к модулю: префикс пути импорта для всех пакетов в модуле. Модуль содержит пакеты в каталоге, содержащем его файл go.mod, а также подкаталоги этого каталога до следующего подкаталога, содержащего другой файл go.mod (если есть).

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

Путь к каждому модулю не только служит префиксом пути импорта для его пакетов, но также указывает, где команда go должна искать его для его загрузки. Например, чтобы загрузить модуль golang.org/x/tools, команда go будет обращаться к репозиторию, указанному https://golang.org/x/tools.

Путь импорта - это строка, используемая для импорта пакета. Путь импорта пакета - это путь к модулю, соединенный с его подкаталогом внутри модуля. Например, модуль github.com/google/go-cmp содержит пакет в каталоге cmp/. Путь импорта этого пакета - github.com/google/go-cmp/cmp. Пакеты в стандартной библиотеке не имеют префикса пути к модулю.

Ваша первая программа

Чтобы скомпилировать и запустить простую программу, сначала выберите путь к модулю (мы будем использовать example.com/user/hello) и создайте файл go.mod, который его объявляет:

$ mkdir hello # Или клонируйте его, если он уже существует в системе контроля версий.
$ cd hello
$ go mod init example.com/user/hello
go: creating new go.mod: module example.com/user/hello
$ cat go.mod
module example.com/user/hello

go 1.16
$

Первым оператором в исходном файле Go должно быть имя пакета. Исполняемые команды всегда должны использовать пакет main.

Затем создайте внутри этого каталога файл с именем hello.go, содержащий следующий код Go:

package main

import "fmt"

func main() {
	fmt.Println("Hello, world.")
}

Теперь вы можете собрать и установить эту программу с помощью инструмента go:

$ go install example.com/user/hello
$

Эта команда создает команду hello, создавая исполняемый двоичный файл. Затем он устанавливает этот двоичный файл как $HOME/go/bin/hello (или, в Windows, %USERPROFILE%\go\bin\hello.exe).

Каталог установки управляется переменными среды GOPATH и GOBIN. Если установлен GOBIN, в этот каталог устанавливаются двоичные файлы. Если установлен GOPATH, двоичные файлы устанавливаются в подкаталог bin первого каталога в списке GOPATH. В противном случае двоичные файлы устанавливаются в подкаталог bin GOPATH по умолчанию ($HOME/go или %USERPROFILE%\go).

Вы можете использовать команду go env для переносимой установки значения по умолчанию для переменной среды для будущих команд go:

$ go env -w GOBIN=/somewhere/else/bin
$

Чтобы отключить переменную, ранее установленную с помощью go env -w, используйте go env -u:

$ go env -u GOBIN
$

Такие команды, как go install, применяются в контексте модуля, содержащего текущий рабочий каталог. Если рабочий каталог не находится в модуле example.com/user/hello, go install может завершиться ошибкой.

Для удобства команды go принимают пути относительно рабочего каталога и по умолчанию используют пакет в текущем рабочем каталоге, если не указан другой путь. Итак, в нашем рабочем каталоге все следующие команды эквивалентны:

$ go install example.com/user/hello

$ go install .

$ go install

Затем запустим программу, чтобы убедиться, что она работает. Для дополнительного удобства мы добавим каталог установки в наш PATH, чтобы упростить запуск двоичных файлов:

$ export PATH=$PATH:$(dirname $(go list -f '{{.Target}}' .))
$ hello
Hello, world.
$

Пользователи Windows могут проконсультироваться в этом посте по установке %PATH%.

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

$ git init
Initialized empty Git repository in /home/user/hello/.git/
$ git add go.mod hello.go
$ git commit -m "initial commit"
[master (root-commit) 0b4507d] initial commit
 1 file changed, 7 insertion(+)
 create mode 100644 go.mod hello.go
$

Команда go находит репозиторий, содержащий заданный путь к модулю, запрашивая соответствующий HTTPS URL-адрес и считывая метаданные, встроенные в HTML ответ. Многие службы хостинга уже предоставляют эти метаданные для репозиториев, содержащих код Go, поэтому самый простой способ сделать ваш модуль доступным для использования другими - это, как правило, сделать так, чтобы его путь модуля соответствовал URL-адресу репозитория.

Импорт пакетов из вашего модуля

Напишем пакет morestrings и воспользуемся им из программы hello. Сначала создайте каталог для пакета с именем $HOME/hello/morestrings, а затем файл с именем reverse.go в этом каталоге со следующим содержимым:

// Пакет morestrings реализует дополнительные функции  
// для управления UTF-8 закодированными строками сверх того, 
// что предусмотрено в стандартном пакете "strings".
package morestrings

// ReverseRunes возвращает строку аргументов, 
// перевернутую по рунам слева направо.
func ReverseRunes(s string) string {
	r := []rune(s)
	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r)
}

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

Давайте проверим, что пакет компилируется с помощью go build:

$ cd $HOME/hello/morestrings
$ go build
$

Это не приведет к созданию выходного файла. Вместо этого он сохраняет скомпилированный пакет в локальном кеше сборки.

Убедившись, что пакет morestrings собирается, давайте воспользуемся им из программы hello. Для этого измените исходный $HOME/hello/hello.go, чтобы использовать пакет morestrings:

package main

import (
	"fmt"

	"example.com/user/hello/morestrings"
)

func main() {
	fmt.Println(morestrings.ReverseRunes("!oG ,olleH"))
}

Установите программу hello:

$ go install example.com/user/hello

Запустив новую версию программы, вы должны увидеть новое перевернутое сообщение:

$ hello
Hello, Go!

Импорт пакетов из удаленных модулей

Путь импорта может описывать, как получить исходный код пакета с помощью системы контроля версий, такой как Git или Mercurial. Инструмент go использует это свойство для автоматической загрузки пакетов из удаленных репозиториев. Например, чтобы использовать github.com/google/go-cmp/cmp в своей программе:

package main

import (
	"fmt"

	"example.com/user/hello/morestrings"
	"github.com/google/go-cmp/cmp"
)

func main() {
	fmt.Println(morestrings.ReverseRunes("!oG ,olleH"))
	fmt.Println(cmp.Diff("Hello World", "Hello Go"))
}

Теперь, когда у вас есть зависимость от внешнего модуля, вам нужно загрузить этот модуль и записать его версию в файл go.mod.
Команда go mod tidy добавляет требования к отсутствующим модулям для импортированных пакетов и удаляет требования к модулям, которые больше не используются.

$ go mod tidy
go: finding module for package github.com/google/go-cmp/cmp
go: found github.com/google/go-cmp/cmp in github.com/google/go-cmp v0.5.4
$ go install example.com/user/hello
$ hello
Hello, Go!
  string(
- 	"Hello World",
+ 	"Hello Go",
  )
$ cat go.mod
module example.com/user/hello

go 1.16

require github.com/google/go-cmp v0.5.4
$

Зависимости модулей автоматически загружаются в подкаталог pkg/mod каталога, указанного переменной среды GOPATH. Загруженное содержимое для данной версии модуля совместно используется всеми другими модулями, которым требуется эта версия, поэтому команда go помечает эти файлы и каталоги как доступные только для чтения. Чтобы удалить все загруженные модули, вы можете передать флаг -modcache для очистки:

$ go clean -modcache
$

Тестирование

Go имеет легкий фреймворк тестирования, состоящую из команды go test и пакета testing.

Вы пишете тест, создавая файл с именем, заканчивающимся на _test.go, который содержит функции с именем TestXXX с сигнатурой func (t *testing.T). Фреймворк тестирования запускает каждую такую ​​функцию; если функция вызывает функцию сбоя, такую ​​как t.Error или t.Fail, тест считается неудачным.

Добавьте тест в пакет morestrings, создав файл $HOME/hello/morestrings/reverse_test.go, содержащий следующий код Go.

package morestrings

import "testing"

func TestReverseRunes(t *testing.T) {
	cases := []struct {
		in, want string
	}{
		{"Hello, world", "dlrow ,olleH"},
		{"Hello, 世界", "界世 ,olleH"},
		{"", ""},
	}
	for _, c := range cases {
		got := ReverseRunes(c.in)
		if got != c.want {
			t.Errorf("ReverseRunes(%q) == %q, want %q", c.in, got, c.want)
		}
	}
}

Затем запустите тест с помощью go test:

$ go test
PASS
ok  	example.com/user/morestrings 0.165s
$

Что дальше

Чтобы ознакомиться с оcновами Go воспользуйтесь серией постов Основы Go.

Подборка Эффективный Go содержит советы по написанию понятного идиоматического Go кода.


Читайте также: