Если вы видите что-то необычное, просто сообщите мне.

Go lang

Доступ к K8S CRD из go-клиента

Kubernetes API сервер легко расширяется с помощью Custom Resource Defenition. Однако, доступ к этом ресурсу из популярных библиотек go-клиентов сложна и плохо задокументированна. Эта статья содержит маленькую инструкцию как получить доступ к такому ресурсу из вашего кода Go.

Цель

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

Определим и создадим CRD

Для этой статьи будем работать над простым примером: CRD может быть леко создан используя kubectl для этого примера, мы начнем с одиночного простого определения ресурса:

apiVersion: "apiextensions.k8s.io/v1beta1"
kind: "CustomResourceDefinition"
metadata:
  name: "projects.example.martin-helmich.de"
spec:
  group: "example.martin-helmich.de"
  version: "v1alpha1"
  scope: "Namespaced"
  names:
    plural: "projects"
    singular: "project"
    kind: "Project"
  validation:
    openAPIV3Schema:
      required: ["spec"]
      properties:
        spec:
          required: ["replicas"]
          properties:
            replicas:
              type: "integer"
              minimum: 1

Для определения CRD, нам понадобиться озаботиться об API Group Name( в этом случае, example.martin-helmich.de). По соглашению, это обычно доменное, которым вы владеете(например домер организации), чтобы предотвратить конфликты наименования. CRD именя обычно выглядят так: <plural-resource-name>.<api-group-name>, в нашем примере: projects.example.martin-helmich.de.

Так же, будьте внимательны когда выбираете версию CRD(spec.version в примере выше). Пока еще рабоатает над CRD, то будет хорошей идей поместить CRD в группу alpha версии API. Для пользователей вашего самодельного ресурса, это будет значить, что что-то может измениться.

Часто, нужно проверить что данные которые хранит пользователень в вашем CRD содержит опредленные схемы. За это отвечает spec.validation.openAPIV3Schema. Она содержит JSON схему которая описывает формат который должны иметь CRD.

После сохранения CRD в файл, применим его в кластере:

> kubectl apply -f projects-crd.yaml
customresourcedefinition "projects.example.martin-helmich.de" created

После создания CRD вы можете создать объект этого типа. Работает это так же как с обычными Kubernetes Объектами(pods, deploymens и так далее). Отличается только kind и apiVersion:

apiVersion: "example.martin-helmich.de/v1alpha1"
kind: "Project"
metadata:
  name: "example-project"
  namespace: "default"
spec:
  replicas: 1

Можно создать CRD как любой другой объект через kubectl

> kubectl apply -f project.yaml
project "example-project" created

Можно даже использовать kubectl чтобы получить самодельный ресурс обратно из K8S.

> kubectl get projects
NAME               AGE
example-project    2m

Cоздание Golang клиента

Теперь, будем использовать пакет go-клиента, для доступа к этим CRD. Для примера, я буду считать, что мы работаем над Go проектом, с названием github.com/martin-helmich/kubernetes-crd-example(репозиторий существует) и у него есть go-клиент и apimachinery установленну библиотеку в качестве модуля Go.

go mod init github.com/martin-helmich/kubernetes-crd-example
go get k8s.io/client-go@v0.17.0
go get k8s.io/apimachinery@v0.17.0
Множество документаций работая с CRD предполагают, что вы работаете с некоторым типом генерации кода, чтобы собрать клиентскую библиотеку автоматически. Однако, этот процесс задокументирован редко, и после прочтения нескольких ненавистных дискуссий на github, создалось впечатление, что всё ещё в "прогрессе". В общем, я встял в самостоятельную реализацую клиентат.

Шаг 1: Определение типов

Начав с определения типов для самодельного ресурса. Я нашел, что это хорошая практика, организовывать эти типа группируя по версии API. Для примера, можно создать файл api/types/v1alpha1/project.go содержащий следующее:

package v1alpha1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

type ProjectSpec struct {
    Replicas int `json:"replicas"`
}

type Project struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec ProjectSpec `json:"spec"`
}

type ProjectList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`

	Items []Project `json:"items"`
}

Тип metav1.ObjectMeta содержит типичные свойства метадаты, которые вы можете найти в любом K8s ресурсе.

Шаг 2: Определим метод DeepCopy

Каждый тип, который будет обслуживаться K8S API(в нашем случае, Project и ProjectList) требует реализацию k8s.io/apimachinery/pkg/runtime.Object интерфейса. Этот интерфейс определяет 2 метода GetObjectKind() и DeepCopyObject(). Первый метод уже предоставлен встронной структурой metav1.TypeMeta, второй нужно реализовать самостоятельно.

Метод DeepCopyObject предназначен для создания полной копии объекта. Так как это требует шаблонного кода, этот метод часто генерируется автоматически. Для этой статьи мы сделаем это ручками. Продолжим с добавления второго файла deepcopy.go в тот же покет:

package v1alpha1

import "k8s.io/apimachinery/pkg/runtime"

// DeepCopyInto copies all properties of this object into another object of the
// same type that is provided as a pointer.
func (in *Project) DeepCopyInto(out *Project) {
    out.TypeMeta = in.TypeMeta
    out.ObjectMeta = in.ObjectMeta
    out.Spec = ProjectSpec{
        Replicas: in.Spec.Replicas,
    }
}

// DeepCopyObject returns a generically typed copy of an object
func (in *Project) DeepCopyObject() runtime.Object {
    out := Project{}
    in.DeepCopyInto(&out)

    return &out
}

// DeepCopyObject returns a generically typed copy of an object
func (in *ProjectList) DeepCopyObject() runtime.Object {
    out := ProjectList{}
    out.TypeMeta = in.TypeMeta
    out.ListMeta = in.ListMeta

    if in.Items != nil {
        out.Items = make([]Project, len(in.Items))
        for i := range in.Items {
            in.Items[i].DeepCopyInto(&out.Items[i])
        }
    }

    return &out
}

Интерлюдия: Автоматическое создание DeepCopy метода

Так, мы могли заметить, что определение всех этих различных DeepCopy методов вовсе не веселое занятие. Есть множество различных инструментов и фреймворков около автогенерации этих методов(все зависит от уровня документации и в целом зрелости). То что нашел я, работает отлично в инструменте controller-gen, что является частью файмеворка Kuberbuilder:

$ go get -u github.com/kubernetes-sigs/controller-tools/cmd/controller-gen

Чтобы использовать controller-gen, опишите ваш CRD тип через +k8s:deepcopy-gen annotation

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Project struct {
    // ...
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type ProjectList struct {
    // ...
}

Затем, выполните команду, для автоматического создания метода deepcopy

controller-gen object paths=./api/types/v1alpha1/project.go

Можно еще проще, вы можете добавить go:generate выражение в целый файл:

package v1alpha1

import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

//go:generate controller-gen object paths=$GOFILE

// ...

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

go generate ./... 

Шаг 3: Зарегистрируем типы на схеме компоновщика

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

Для этого, добавим новый файл register.go в наш пакет:

package v1alpha1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"
)

const GroupName = "example.martin-helmich.de"
const GroupVersion = "v1alpha1"

var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: GroupVersion}

var (
    SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
    AddToScheme   = SchemeBuilder.AddToScheme
)

func addKnownTypes(scheme *runtime.Scheme) error {
    scheme.AddKnownTypes(SchemeGroupVersion,
        &Project{},
        &ProjectList{},
    )

    metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
    return nil
}

Как можно заметить, этот код не делает что-то реальное, пока еще(за исключением создания нового runtime.SchemeBuilder). Важная часть в том. что AddToScheme функция(строка 16)б которая экспортирует членов структуры созданых с типом runtime.SchemeBuilder в строке 15. Вы можете вызвать эту функцию позже, из любой части вашего клиентского кода как только клиент K8S будет готов к регистрации вашего определенного типа.

Шаг 4: создание HTTP клиента

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

Для этого, добавим следующий код в ваш main.go вашего пакета

package main

import (
    "flag"
    "log"
    "time"

    "k8s.io/apimachinery/pkg/runtime/schema"
    "k8s.io/apimachinery/pkg/runtime/serializer"

    "github.com/martin-helmich/kubernetes-crd-example/api/types/v1alpha1"
    "k8s.io/client-go/kubernetes/scheme"
    "k8s.io/client-go/rest"
    "k8s.io/client-go/tools/clientcmd"
)

var kubeconfig string

func init() {
    flag.StringVar(&kubeconfig, "kubeconfig", "", "path to Kubernetes config file")
    flag.Parse()
}

func main() {
    var config *rest.Config
    var err error

    if kubeconfig == "" {
        log.Printf("using in-cluster configuration")
        config, err = rest.InClusterConfig()
    } else {
        log.Printf("using configuration from '%s'", kubeconfig)
        config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
    }

    if err != nil {
        panic(err)
    }

    v1alpha1.AddToScheme(scheme.Scheme)

    crdConfig := *config
    crdConfig.ContentConfig.GroupVersion = &schema.GroupVersion{Group: v1alpha1.GroupName, Version: v1alpha1.GroupVersion}
    crdConfig.APIPath = "/apis"
    crdConfig.NegotiatedSerializer = serializer.NewCodecFactory(scheme.Scheme)
    crdConfig.UserAgent = rest.DefaultKubernetesUserAgent()

    exampleRestClient, err := rest.UnversionedRESTClientFor(&crdConfig)
    if err != nil {
        panic(err)
    }
}

Теперь можно использовать exampleRestClient созданный в строке 48, для запроса всех самостоятельных ресурсов внутри example.martin-helmich.de/v1alpha1 API группы. Пример может выглядить сдедующим образом:

result := v1alpha1.ProjectList{}
err := exampleRestClient.
    Get().
    Resource("projects").
    Do().
    Into(&result)

Чтобы использовать API типобезопасным способом, обычно хорошая идея обернуть эти операции внутри своего клиентского набора. Для этого, создаём новый подпакет clientset/v1alpha1. Для начала, реализуем интерфейс который определяет типы для вашей группы API. И Переносим настройки конфигурации из вашего главного метода в эту функцию коснтуктора клиенсткого набора(NewForConfig для примера ниже):

package v1alpha1

import (
    "github.com/martin-helmich/kubernetes-crd-example/api/types/v1alpha1"
    "k8s.io/apimachinery/pkg/runtime/schema"
    "k8s.io/apimachinery/pkg/runtime/serializer"
    "k8s.io/client-go/kubernetes/scheme"
    "k8s.io/client-go/rest"
)

type ExampleV1Alpha1Interface interface {
    Projects(namespace string) ProjectInterface 
}

type ExampleV1Alpha1Client struct {
    restClient rest.Interface
}

func NewForConfig(c *rest.Config) (*ExampleV1Alpha1Client, error) {
    config := *c
    config.ContentConfig.GroupVersion = &schema.GroupVersion{Group: v1alpha1.GroupName, Version: v1alpha1.GroupVersion}
    config.APIPath = "/apis"
    config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
    config.UserAgent = rest.DefaultKubernetesUserAgent()

    client, err := rest.RESTClientFor(&config)
    if err != nil {
        return nil, err
    }

    return &ExampleV1Alpha1Client{restClient: client}, nil
}

func (c *ExampleV1Alpha1Client) Projects(namespace string) ProjectInterface {
    return &projectClient{
        restClient: c.restClient,
        ns: namespace,
    }
}

Код ниже, всё еще, не будет компилироваться, так как в нем всё еще отсутствуют ProjectInterface и projectClient типы. Мы сейчас до них доберемся.

ExampleV1Alpha1Interface и его реализация, ExampleV1Alpha1Client структура это главная точка входа для доступа к самодельным ресурсам. Вы можете легко создать новый клиентский набор в вашем main.go, просто вызывая clientset, err := v1alpha1.NewForConfig(config).

Дальше, вам нужно реализовать определенный клиентский набор для доступа к самодельному ресурсу Project(пример выше уже использует ProjectInterface и projectClient типы которые всё еще нужно поддерживать). Создадим втрой файл в том же пакете prjects.go:

package v1alpha1

import (
    "github.com/martin-helmich/kubernetes-crd-example/api/types/v1alpha1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/watch"
    "k8s.io/client-go/kubernetes/scheme"
    "k8s.io/client-go/rest"
)

type ProjectInterface interface {
    List(opts metav1.ListOptions) (*v1alpha1.ProjectList, error)
    Get(name string, options metav1.GetOptions) (*v1alpha1.Project, error)
    Create(*v1alpha1.Project) (*v1alpha1.Project, error)
    Watch(opts metav1.ListOptions) (watch.Interface, error)
    // ...
}

type projectClient struct {
    restClient rest.Interface
    ns         string
}

func (c *projectClient) List(opts metav1.ListOptions) (*v1alpha1.ProjectList, error) {
    result := v1alpha1.ProjectList{}
    err := c.restClient.
        Get().
        Namespace(c.ns).
        Resource("projects").
        VersionedParams(&opts, scheme.ParameterCodec).
        Do().
        Into(&result)

    return &result, err
}

func (c *projectClient) Get(name string, opts metav1.GetOptions) (*v1alpha1.Project, error) {
    result := v1alpha1.Project{}
    err := c.restClient.
        Get().
        Namespace(c.ns).
        Resource("projects").
        Name(name).
        VersionedParams(&opts, scheme.ParameterCodec).
        Do().
        Into(&result)

    return &result, err
}

func (c *projectClient) Create(project *v1alpha1.Project) (*v1alpha1.Project, error) {
    result := v1alpha1.Project{}
    err := c.restClient.
        Post().
        Namespace(c.ns).
        Resource("projects").
        Body(project).
        Do().
        Into(&result)

    return &result, err
}

func (c *projectClient) Watch(opts metav1.ListOptions) (watch.Interface, error) {
    opts.Watch = true
    return c.restClient.
        Get().
        Namespace(c.ns).
        Resource("projects").
        VersionedParams(&opts, scheme.ParameterCodec).
        Watch()
}

Этот клиент очевидно еще не закончен и не имеет методы типа Delete, Update и другие. Однако, это можно реализовать похожим на существующий метод образом. Посмотрите на существующий клиенсткий набор(для примера Pod client set) для вдохновения.

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

import clientV1alpha1 "github.com/martin-helmich/kubernetes-crd-example/clientset/v1alpha1"
// ...

func main() {
    // ...

    clientSet, err := clientV1alpha1.NewForConfig(config)
    if err != nil {
        panic(err)
    }

    projects, err := clientSet.Projects("default").List(metav1.ListOptions{})
    if err != nil {
        panic(err)
    }

    fmt.Printf("projects found: %+v\n", projects)
}

Шаг 5: Созадем оповещятель

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

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

Этот шаблон широко распространнен, что библиотеки go-клиентов предлагают готовое решение: пакет k8s.io/client-go/tools/cache. Вы можете создать новый оповещатель для ваших ресурсов:

package main

import (
    "time"

    "github.com/martin-helmich/kubernetes-crd-example/api/types/v1alpha1"
    client_v1alpha1 "github.com/martin-helmich/kubernetes-crd-example/clientset/v1alpha1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/util/wait"
    "k8s.io/apimachinery/pkg/watch"
    "k8s.io/client-go/tools/cache"
)

func WatchResources(clientSet client_v1alpha1.ExampleV1Alpha1Interface) cache.Store {
    projectStore, projectController := cache.NewInformer(
        &cache.ListWatch{
            ListFunc: func(lo metav1.ListOptions) (result runtime.Object, err error) {
                return clientSet.Projects("some-namespace").List(lo)
            },
            WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) {
                return clientSet.Projects("some-namespace").Watch(lo)
            },
        },
        &v1alpha1.Project{},
        1*time.Minute,
        cache.ResourceEventHandlerFuncs{},
    )

    go projectController.Run(wait.NeverStop)
    return projectStore
}

Метод NewInformer возвращает два объекта: второй - значение, controller - управляет List() и Watch() вызывает и наполняет первое значение, храня некоторое количество кэшированных ресурсов с API сервера(в нашем случае CRD).

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

store := WatchResource(clientSet)

project := store.GetByKey("some-namespace/some-project").(*v1alpha1.Project)

Вывод

Создание клиентов для CRD - это что-то что мало задокументированно(на данный момент) и подчас может быть довольно сложным.

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

Как мокать? Go способ.

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

Система.

Наша система имеет 2 компонента:

Код сторонней библиотеки

package userdb

// db act as a dummy package level database.
var db map[string]bool

// init initialize a dummy db with some data
func init() {
	db = make(map[string]bool)
	db["ankuranand@dummy.org"] = true
	db["anand@example.com"] = true
}

// UserExists check if the User is registered with the provided email.
func UserExists(email string) bool {
	if _, ok := db[email]; !ok {
		return false
	}
	return true
}

Сервис

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

package simpleservice

import (
	"fmt"
	"log"

	"github.com/ankur-anand/mocking-demo/userdb"
)

// User encapsulate a user in the system.
type User struct {
	Name     string `json:"name"`
	Email    string `json:"email"`
	UserName string `json:"user_name"`
}

// RegisterUser will register a User if only User has not been previously
// registered.
func RegisterUser(user User) error {
	// check if user is already registered
	found := userdb.UserExists(user.Email)
	if found {
		return fmt.Errorf("email '%s' already registered", user.Email)
	}
	// carry business logic and Register the user in the system
	log.Println(user)
	return nil
}
package simpleservice

import "testing"

func TestCheckUserExist(t *testing.T) {
	user := User{
		Name:     "Ankur Anand",
		Email:    "anand@example.com",
		UserName: "anand",
	}

	err := RegisterUser(user)
	if err == nil {
		t.Error("Expected Register User to throw and error got nil")
	}
}

Если мы посмотрим на функцию theRegisterUser. Она вызывает Функцию userdb.UserExistкоторая является для нас стороннеё библиотекой и мы не можем протестировать нашу RegisterUser функцию без её вызова.

Моки

Попробуем это исправить моками.

Мок-объекты соответствуют требованиям к интерфейсу и заменяют более сложные настоящие объекты.

Мок-объекты соответствуют требованиям к интерфейсу.

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

// registrationPreChecker validates if user is allowed to register.
type registrationPreChecker interface {
    userExists(string) bool
}

Реализация интерфейса.

userExists функция нашего нового интерфейса просто оборачивает вызова настоящего вызова к стороннему сервису.

type regPreCheck struct {}
func (r regPreCheck) userExists(email string) bool {
    return userdb.UserExist(email)
}

Теперь создадим, переменную на уровне пакета типа registrationPreChecker и присвоим экземпляр regPreCheck внутри init функции.

var regPreCond registrationPreChecker

func init() {
   regPreCond = regPreCheck{}
}

Так как regPreCond является типом registrationPreChecker который проверяет существования пользователя, мы можем использовать RegisterUser фнукцию. Поэтому вместо прямого вызова функции userdb.UserExist внутри функции RegisterUser мы вызовем через реализацию интерфейса.

// check if user is already registered
found := regPreCond.userExist(user.Email)

Изменный код:

package unitservice

import (
	"fmt"
	"log"

	"github.com/ankur-anand/mocking-demo/userdb"
)

// User encapsulate a user in the system.
type User struct {
	Name     string `json:"name"`
	Email    string `json:"email"`
	UserName string `json:"user_name"`
}

// Mock objects meet the interface requirements of,
// and stand in for, more complex real ones
type registrationPreChecker interface {
	userExists(string) bool
}

type regPreCheck struct{}

func (r regPreCheck) userExists(email string) bool {
	return userdb.UserExist(email)
}

var regPreCond registrationPreChecker

func init() {
	regPreCond = regPreCheck{}
}

// RegisterUser will register a User if only User has not been previously
// registered.
func RegisterUser(user User) error {
	// check if user is already registered
	found := regPreCond.userExist(user.Email)
	if found {
		return fmt.Errorf("email '%s' already registered", user.Email)
	}
	// carry business logic and Register the user in the system
	log.Println(user)
	return nil
}

Если мы запустим test опять он пройет так как мы не трогали поведение нашей функции. Теперь давайте посмотрим как заставить наше тестирование проходить с помощью моков.

Написание моков

Для начала посмотрим на полный код:

package unitservice

import "testing"

// This helps in assigning mock at the runtime instead of compile time
var userExistsMock func(email string) bool

type preCheckMock struct{}

func (u preCheckMock) userExists(email string) bool {
	return userExistsMock(email)
}

func TestRegisterUser(t *testing.T) {
	user := User{
		Name:     "Ankur Anand",
		Email:    "anand@example.com",
		UserName: "anand",
	}

	regPreCond = preCheckMock{}
	userExistsMock = func(email string) bool {
		return false
	}

	err := RegisterUser(user)
	if err != nil {
		t.Fatal(err)
	}

	userExistsMock = func(email string) bool {
		return true
	}
	err = RegisterUser(user)
	if err == nil {
		t.Error("Expected Register User to throw and error got nil")
	}
}

Мок объекты отвечают требованиям интерфейса. Тут наш мок объект реализует registrationPreChecker интерфейс.

type preCheckMock struct{}

func (u preCheckMock) userExists(email string) bool {
   return userExistsMock(email)
}

Реализация мока возвращает userExistMock тип функции вместо прямого возвращения true или false. Это помогает в назначении мока во время работы вместо во время компиляции. Вы можете увидеть это в TestRegisterUser фукнции.

Их заменяют более сложные настоящие объекты

regPreCond = preCheckMock{}

Мы просто назначили нашему regPreCond тип registrationPreChecker, который проверяет есть ли пользователь или нет в нашей реализации мока во времы выполнения теста. Это можно увидеть в TestRegisterUser функции.

func TestRegisterUser(t *testing.T) {
   user := User{
      Name:     "Ankur Anand",
      Email:    "anand@example.com",
      UserName: "anand",
   }

   regPreCond = preCheckMock{}
   userExistsMock = func(email string) bool {
      return false
   }

   err := RegisterUser(user)
   if err != nil {
      t.Fatal(err)
   }

   userExistsMock = func(email string) bool {
      return true
   }
   err = RegisterUser(user)
   if err == nil {
      t.Error("Expected Register User to throw and error got nil")
   }
}

Но мы еще не закончиил!

Мы сказали что будет делать через "Способ GO", да, но пока есть проблема, для этого нужно провести рефакторинг.

Мы использовали глобальную переменную и события, которые мы не обновляем переменную во время реального выполнения, это должно сломать паралельный тест. Есть несколько способов, чтобы это исправить. В нашем случае, мы собираемся передать registrationPreCheker как зависимость нашей функции и представим новую функцию конструктора которая будет создавать по умолчанию regitrationPreCheck тип, который может быть использован во время настоящей работы, и так как мы передаем его как зависимость, мы можем передать нашу реализацию мок как параметр, в этом случае.

func NewRegistrationPreChecker() RegistrationPreChecker {
   return regPreCheck{}
}

// RegisterUser will register a User if only User has not been previously
// registered.
func RegisterUser(user User, regPreCond RegistrationPreChecker) error {
   // check if user is already registered
   found := regPreCond.userExists(user.Email)
   if found {
      return fmt.Errorf("email '%s' already registered", user.Email)
   }
   // carry business logic and Register the user in the system
   log.Println(user)
   return nil
}

Let’s modify our test code too. So instead of modifying the package level variable, we are now explicitly passing it as a dependency inside our RegisterUser function.

func TestRegisterUser(t *testing.T) {
   user := User{
      Name:     "Ankur Anand",
      Email:    "anand@example.com",
      UserName: "anand",
   }

   regPreCond := preCheckMock{}
   userExistsMock = func(email string) bool {
      return false
   }

   err := RegisterUser(user, regPreCond)
   if err != nil {
      t.Fatal(err)
   }

   userExistsMock = func(email string) bool {
      return true
   }
   err = RegisterUser(user, regPreCond)
   if err == nil {
      t.Error("Expected Register User to throw and error got nil")
   }
}

Выводы

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

Злоупотребление синтаксисом Go для создания DSL(предметно-ориентированного языка)

Go частый выбор для создания внутреностей высокопроизводительных систем, но так он так же разработан с некоторым количеством функций которые хороши для создания высокоуровневых абстракций. Вам не нужно переключаться на динамические языки такие как Ruby или Python, чтобы получить удовольствие от API или объяснительный синтакс.

Довольно часто для выражения API используется DSL(Предметно ориентированный язык). DSL это язык внутри языка который компилируется или интерпритируется внутри языка, в нашем случае Go. Если API хорошо спроектирован, DSL выглядит как его собственная спецификация специально созданная для конкретной задачи. DSL такие как CSS, SQL созданы как отдельный язык с их собственными анализаторами, но на данный момент мы сосредоточимся на одном, который построим с помощью компилятора Go, и используем внутри Go кода.

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

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

Пример использования.

Мы собираемся построить просто DSL для создания HTTP посредника. Эта область отличный кандидат для DSL потому что оно наполнено общими, хорошо понимаемыми и часто переиспользуемыми шаблонами как ограничени доступа, частого ограничения, обработки сессии и так далее. Лучше всего для читалей и писателей кода который реализует эти шаблоны, если код будет читаться как декларативный файл настроек, чем как императивное изобретение колеса.

Личные типы.

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

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

type Host string

Мы также можем создать тип для набора:

type HostList []Host
type HostSet map[Host]interface{}

Теперь где нибудь в нашем коде, переменная типа HostList будет []Host, или на самом деле []string под капотом, но с более понятным имененм.

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

func (s HostSet) Add(n Host) {
	s[n] = struct{}{}
}

func (s HostSet) Remove(n Host) {
	delete(s, n)
}

func (s HostSet) Contains(n Host) bool {
	_, found := s[n]
	return found
}

Как мы можем использовать HostSet как если бы это был более сложная структура контейнера доступная через методы:

func main() {
	s := make(HostSet)
	s.Add("golang.org")
	s.Add("google.com")
	s.Add("gopheracademy.org")
	s.Remove("google.com")

	hostnames := HostList{
		"golang.org",
		"google.com",
		"gopheracademy.org",
	}
	for _, n := range hostnames {
		fmt.Printf("%s? %v\n", n, s.Contains(n))
	}
}

Вот что мы получим на выходе:

golang.org? true
google.com? false
gopheracademy.org? true

Что мы тут имеем? Мы созадли абстракцию над простой мапой, мы можем использовать её как набор - у нее есть Add и Remove операции, и у нее есть проверка Contains - созданные нами обороты речи, которые могут быть использованны нами в коде. Он лучше изолирован, чем простая передача map[string]interface{} и надеясь что значение "набор имен хостов" соблюдается при доступе к map. Это решение более гибкое и явное, чем скажем:

func SetContains(s map[string]interface{}, hostname string) bool {
	_, found := s[hostname]
	return found
}

func main() {
	s := make(map[string]interface{})
	if SetContains(s, hostname) {
        // do stuff
	}
}

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

Высокоуровневые функции.

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

Давайте построим наш пример выше по-новому. Добавим метод в HostList который принимает функцию как входной параметр и возвращает новый HostList:

func (l HostList) Select(f func(Host) bool) HostList {
	result := make(HostList, 0, len(l))
	for _, h := range l {
		if f(h) {
			result = append(result, h)
		}
	}
	return result
}

Этот метод HostList имеет эффективность создания нового HostList для которого предоставленное условие (func f) верно. Создадим простое выражение функции для вставки в f:

// import “strings” 
func IsDotOrg(h Host) bool {
	return strings.HasSuffix(string(h), ".org")
}

И используем его в наше новом методе HostList

myHosts := HostList{"golang.org", "google.com", "gopheracademy.org"}
fmt.Printf("%v\n", myHosts.Select(IsDotOrg))

Вывод будет таким:

[golang.org gopheracademy.org]

Select возвращает только те элементы myHosts для которых переданная функция IsDotOrg будет возвращать true, то есть для тех имен у которых есть ".org".

func(Host) bool немного кривовата как параметричечкий тип и создает подпись метода Select сложно для чтения, поэтому давайте используем наш трюк для типов, чтобы сделать его аккуратным.

type HostFilter func(Host) bool

Он делает Select более читаемым:

func (l HostList) Select(f HostFilter) HostList {
        //...
}

и добавляет выгоду с которой мы можем объявить некоторые методы HostFilter:

func (f HostFilter) Or(g HostFilter) HostFilter {
    return func(h Host) bool {
        return f(h) || g(h)
    }
}

func (f HostFilter) And(g HostFilter) HostFilter {
    return func(h Host) bool {
        return f(h) && g(h)
    }
}

Если мы хотим объявить функцию которая может использовать HostFilter метода, к сожалению нам нжуно пойти другим путём, чтобы это сделать. Чтобы функция имела верный приемни HostFilter метода, недостаточно совпадать описанию HostFilter, нам нужно объявить функцию как HostFilter явно.

var IsDotOrg HostFilter = func(h Host) bool {
	return strings.HasSuffix(string(h), ".org")
}

Но теперь стало ясно что мы начали делать хорошо для нашей угрозы злоупотребления синтаксисом Go. Объявление функции с помощью передачи анонимной функции в переменную дает неясное ощущение. Отметим что это не требование использовать высокоуровневую функцию или преимущество типа для подписи функции - любоая func(Host) bool может быть назначения переменной HostFilter или параметру. Это безрассудное объявление нужно только чтобы можно было использовать функции такие как IsDotOrg как приемник HostFilter метода

Однако выгода, заключается в том, что использование методов в HostFilter функции позволяет получить нам интересный синтаксис:

var HasGo HostFilter = func (h Host) bool {
    return strings.Contains(string(h), "go")
}

var IsAcademic HostFilter = func(h Host) bool {
    return strings.Contains(string(h), "academy")
}

func main() {
    myHosts := HostList{"golang.org", "google.com", "gopheracademy.org"}
    goHosts := myHosts.Select(IsDotOrg.Or(HasGo))
    academies := myHosts.Select(IsDotOrg.And(IsAcademic))

    fmt.Printf("Go sites: %v\n", goHosts)
    fmt.Printf("Academies: %v\n", academies)
}

Запустим:

Go sites: [golang.org google.com gopheracademy.org]
Academies: [gopheracademy.org]

Мы можем увидеть язык приобретает форму выражений типа myHosts.Select(IsDotOrg.Or(HasGo)). Он читается как английский, что-то подобное вы можете услышать на болотах Дагобы. Декларативный синтаксис, начинает появляться - выражения говорять больше о желаемом результате(“select the elements of myHosts that are .orgs or contain ‘Go’) нежели шаги которые требуется чтобы до этого добраться. Мы использовали высокоуровневые функции, Select, And и Or, для построения поведения от этих трех различных кусоков кода в полностью динамичном виде.

Это очень могущественный вид выражения поведения, но все эти методы очереди могут стать запутанными.

// etc.
myHosts.Select(IsDotOrg.Or(HasGo).Or(IsAcademic).Or(WelcomesGophers).And(UsesSSL)

Поэтому возмож мы должны очитсть вещи используя вариативные метод:

var HostFilter Or = func (clauses ...HostFilter) HostFilter {
    var f HostFilter = nil
    for _, c := range clauses {
        f = f.Or(c)
    }
    return f
}

И затем переписать очередь вызовов выше таким образом:

myHosts.Select(Or(IsDotOrg, HasGo, IsAcademic, WelcomesGophers).And(UsesSSL))
Предупредение: эти функциональные сущности самые опасные свойства Go - любой Go код который я читал или писал должен злоупотреблять этими возможностями, создание анонимных функци и передача их через слой за слой с косвенным обращением.

Одноко, динамика функционального стиля программирования бесценна, когда строишь свой язык внутри Go. Высокоуровневые функции, функции которые управляют другими функциями и возвращают целую новую фунцкию, дают возможно сочинять или параметризовать поведение. Цель создания DSL - упростить решение в классе проблем выставив пользоватлю DSL несколько ползеных идей для решения этих проблем, расширив их и объединив эти идеи в какой-то смысл. Созданное динамическое поведение созднное с помощью высокоуровневых функций это одна из возможностей доставить эту функциональность.

Пример по серьёзнее

Мы построим нашу работу над именами хостов так, чтобы сделать что-то ближе к тому, что мы возможно используем в реальном приложении. Ипмортируем пакет net/http, и давайте создадим другой тип:

type RequestFilter func(*http.Request) bool

Мы можем использовать RequestFilter в простом HTTP сервере для вычисления удовлетворяет http.Request услови, как мы это делали с HostFilter выше. Мы можем использовать эти условия, чтобы определить обработать или отбросить запрос.

Мы перейдем от имен хосто к работе с набором ip адресов. Будем использовать CIDR блоки типа "192.168.0.0/16", который определяет набор ip адресов, в данном случае, от 192.168.0.0 до 192.168.255.255. Создадим RequestFilter который фильтрует запросы основанные на ip.

Из net пакета, будем использовать ParseCIDR функцию, и PaeseIP чтобы анализировать входящие запросы. Значения которые возвращает ParseCIDR - IPNet который имеет удобный метод Contains, он будет нам говорить входит ли ip в наш список CIDR блоков.

Давайте импортируем net пакет и напишем RequestFilter который принимает вариативный набор CIDR блоков в формате string:

func CIDR(cidrs ...string) RequestFilter {
	nets := make([]*net.IPNet, len(cidrs))
	for i, cidr := range cidrs {
        // TODO: handle err
		_, nets[i], _ = net.ParseCIDR(cidr)
	}
	return func(r *http.Request) bool {
        // TODO: handle err
		host, _, _ := net.SplitHostPort(r.RemoteAddr)
		ip := net.ParseIP(host)
		for _, net := range nets {
			if net.Contains(ip) {
				return true
			}
		}
		return false
	}
}

Заметим, что net/http пакет уже содержит тип HTTP обработчика, HandlerFunc:

type HandlerFunc func(ResponseWriter, *Request)

А мы будем использовать высокоуровневую функцию и наш RequestFilter для изменения http.HandlerFuncs, объявим тип для функции которая обрабатывает http.HandlerFuncs:

type Middleware func(http.HandlerFunc) http.HandlerFunc

И давайте сделаем несколько функций чтобы построить промежуточный слой используя RequestFilter:

func Allow(f RequestFilter) Middleware {
   return func(h http.HandlerFunc) http.HandlerFunc {
   	return func(w http.ResponseWriter, r *http.Request) {
   		if f(r) {
   			h(w, r)
   		} else {
   			// TODO
   			w.WriteHeader(http.StatusForbidden)
   		}
   	}
   }
}

Теперь для примера вы можете изменить http обработчик MyHandler так, чтобы он прининмал только запросы от 127.0.0.1, следующим образом:

filteredHandler := Allow(CIDR("127.0.0.1/32"))(MyHandler)

Давайте попробуем запустить простой сервер:

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello\n")
}

func main() {
    http.HandleFunc("/hello", Allow(CIDR("127.0.0.1/32")(hello))
	log.Fatal(http.ListenAndServe(":1217", nil))
}

Если мы перейдем по ссылке с локальной машины: http://0.0.0.0:1217/hello, то вы должны увидеть "Hello" в ответ, если вы зайдете на эту ссылку с другого ip, то увидите ошибку 403 Forbidden error.

Для прикола, давайте добавим другой вид RequestFilter который реализует реально просто механизм аутентификации

func PasswordHeader(password string) RequestFilter {
	return func(r *http.Request) bool {
		return r.Header.Get("X-Password") == password
	}
}

И один на основе HTTP метода:

func Method(methods ...string) RequestFilter {
	return func(r *http.Request) bool {
		for _, m := range methods {
			if r.Method == m {
				return true
			}
		}
		return false
	}
}

Ну и промежуточный слой который что-то просто логирует

func Logging(f http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		fmt.Printf("[%v] - %s %s\n", time.Now(), r.Method, r.RequestURI)
		f(w, r)
	}
}

Который мы можем попробовать обновив наш сервер:

func main() {
    http.HandleFunc("/hello", Logging(Allow(CIDR("127.0.0.1/32")(hello)))
	log.Fatal(http.ListenAndServe(":1217", nil))
}

Запустите сервер и посетите страницу http://localhost:1217/hello несколько раз в браузере и в консоли сервера вы увидите:

[2016-12-14 07:42:12.022266374 -0500 EST] - GET /hello
[2016-12-14 07:42:14.537985456 -0500 EST] - GET /hello
[2016-12-14 07:42:24.220089221 -0500 EST] - GET /hello

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

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

type Filters []RequestFilters
type Stack []Middleware
type Endpoint struct {
	Handler    http.HandlerFunc
	Allow      Filters
	Middleware Stack
}

Затем, мы можем выразить эндпоинт выше тем же ограничением как:

var MyEndpoint = Endpoint{
	Handler: hello,
	Allow: Filters{
		CIDR("127.0.0.1/32"),
	},
	Middleware: Stack{
		Logging,
	},
}

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

// Combine creates a RequestFilter that is the conjunction
// of all the RequestFilters in f.
func (f Filters) Combine() RequestFilter {
	return func(r *http.Request) bool {
		for _, filter := range f {
			if !filter(r) {
				return false
			}
		}
		return true
	}
}

// Apply returns an http.Handlerfunc that has had all of the
// Middleware functions in s, if any, to f.
func (s Stack) Apply(f http.HandlerFunc) http.HandlerFunc {
	g := f
	for _, middleware := range s {
		g = middleware(g)
	}
	return g
}

// Builds the endpoint described by e, by applying
// access restrictions and other middleware.
func (e Endpoint) Build() http.HandlerFunc {
	allowFilter := e.Allow.Combine()
	restricted := Allow(allowFilter)(e.Handler)

	return e.Middleware.Apply(restricted)
}

И наконец, изменим сервер чтобы использовать новый сервер:

func main() {
	http.HandleFunc("/hello", mw.MyEndpoint.Build())
	log.Fatal(http.ListenAndServe(":1217", nil))
}

Чтобы увидеть выходу этого миниDSL что мы создали, добавим еще один промежуточный слой:

func SetHeader(key, value string) Middleware {
    return func(f http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *RequestFilter) {
            w.Header().Set(key, value)
            f(w, r)
        }
    }
}

И затем добавим его, вместе с другим RequestFilter в наш эндпоинт:

var MyEndpoint = Endpoint{
	Handler: hello,
	Allow: Filters{
		CIDR("127.0.0.1/32"),
		PasswordHeader("opensesame"), // added
		Method("GET"), // added
	},
	Middleware: Stack{
		Logging,
		SetHeader("X-Foo", "Bar"), // added
	},
}

МЫ добавили существенности в сложность MyEndpoint без добавления множества сложности в его объявление.

Этот полезный DLS удобен для построения одного HTTP эндпоинт, но часто мы хотим больше чем просто один сервис. Мы добавим еще один элемент в наше DSL, способ создать несколько маршрутов и их ендпоинты за раз:

type Routes map[string]Endpoint

func (r Routes) Serve(addr string) error {
	mux := http.NewServeMux()
	for pattern, endpoint := range r {
		mux.Handle(pattern, endpoint.Build())
	}

	return http.ListenAndServe(addr, mux)
}

И наш сервис превращается в:

func main() {
	routes := Routes{
		"/hello": {
			Handler: hello,
			Middleware: Stack{
				Logging,
			},
		},
		"/private": {
			Handler: hello,
			Allow: Filters{
				CIDR("127.0.0.1/32"),
				PasswordHeader("opensesame"),
			},
			Middleware: Stack{
				Logging,
			},
		},
		"/test": {
			Handler: hello,
			Middleware: Stack{
				Logging,
				SetHeader("X-Foo", "Bar"),
			},
		},
	}
	log.Fatal(routes.Serve(":1217"))
}

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

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

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

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

Первые шаги на Go — Построение простого веб приложения с Neo4j

Цель:

Цель этого поста создать простое веб приложение которое снимает структуру вашего сайта получая все ссылки которые содержит и сохраняет их в neo4j базу данных. Идея проста - и в ней следующие шаги:

Требования:

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

Создаем ползунок:

Теперь, когда у нас все есть. Начнем.

Получение одной страницы из интеренета:

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

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

type responseWriter struct{}

func main() {

	resp, err := http.Get("http://www.sfeir.com")

	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}

	rw := responseWriter{}
	io.Copy(rw, resp.Body)
}

func (responseWriter) Write(bs []byte) (int, error) {
	fmt.Printf(string(bs))

	return len(bs), nil
}

Мы начали с объявления главного пакета и импорта требуемых пакетов. Дальше, мы объявили структуру которая будет реализовывать Writer интерфейс. В main функции, мы собираемся присвоить множествую переменных значения. В основном. http.Get будет возвращать значения с ответом и некоторой ошибкой, если что-то пойдет не так. Это общий способ обработки ошибок в Go программах.

Если вы посмотрите на документацию, вы найдете Writer интерфейс с одной функцией. Для того, чтобы реализовать этот интерфейс, нам нужно добавить получателя функции к нашей responseWriter структуре которая совпадает с функцией Writer. Если вы пришли из Java вы должны ожидать синтаксис типа реалзиация Writer. Ну чтож, это не тот случай для Go, так как тут реализация происходит неявно.

Наконец, мы используем io.Copy для записи тела ответа в нашу переменную ответа. Следующий шаг это модификация нашего кода для извлечения ссылок из данного адреса страницы. После некоторого рефакторинга, у нас будет два фала. Это main.go:

package main

import (
	"fmt"
	"os"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	url := os.Args[1]

	retreive(url)
}

И этот retreiver.go:

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

type responseWriter struct{}

func (responseWriter) Write(bs []byte) (int, error) {
	fmt.Printf(string(bs))

	return len(bs), nil
}

func retreive(uri string) {
	resp, err := http.Get(uri)

	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}

	rw := responseWriter{}
	io.Copy(rw, resp.Body)
}

Мы можем запустить это для небольшого вебсайта:

go run main.go retreiver.go http://www.sfeir.com

Теперь мы сделали наш первый шаг для создание ползунка. Есть возможность загрузить и спарсить данный адрес, открыть подключение прям к удлённому хосту, и получить html содержание.

Создадим все гиперссыылки для одной страницы

Теперь начинается часть где нам нужно извлечь все ссылки из html документа. К сожалению, нет доступного для этого помошника для обработки HTML в Go API. Поэтому мы должны посмотреть на стороннюю API. Давайте рассмотрим goquery. Как вы можете догадаться, она похоже на jquery только для Go.

Пакет goquery легко получается командой:

go get github.com/PuerkitoBio/goquery
package main

import (
	"fmt"
	"os"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	url := os.Args[1]

	links, err := retrieve(url)

	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}

	for _, link := range links {
		fmt.Println(link)
	}
}

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

package main

import (
	"fmt"
	"net/http"
	"net/url"
	"strings"

	"github.com/PuerkitoBio/goquery"
)

func retrieve(uri string) ([]string, error) {
	resp, err := http.Get(uri)
	if err != nil {
		fmt.Println("Error:", err)
		return nil, err
	}

	doc, readerErr := goquery.NewDocumentFromReader(resp.Body)
	if readerErr != nil {
		fmt.Println("Error:", readerErr)
		return nil, readerErr
	}
	u, parseErr := url.Parse(uri)
	if parseErr != nil {
		fmt.Println("Error:", parseErr)
		return nil, parseErr
	}
	host := u.Host

	links := []string{}
	doc.Find("a[href]").Each(func(index int, item *goquery.Selection) {
		href, _ := item.Attr("href")
		lu, err := url.Parse(href)
		if err != nil {
			fmt.Println("Error:", err)
			return
		}
		if isInternalURL(host, lu) {
			links = append(links, u.ResolveReference(lu).String())
		}

	})

	return unique(links), nil
}

// insures that the link is internal
func isInternalURL(host string, lu *url.URL) bool {

	if lu.IsAbs() {
		return strings.EqualFold(host, lu.Host)
	}
	return len(lu.Host) == 0
}

// insures that there is no repetition
func unique(s []string) []string {
	keys := make(map[string]bool)
	list := []string{}
	for _, entry := range s {
		if _, value := keys[entry]; !value {
			keys[entry] = true
			list = append(list, entry)
		}
	}
	return list
}

Как можно увидеть, наша retrieve функция стала существенно улучшена. Я убрал responseWriter стуктуру так как она больше не нужна из-за goqeury имеет свою реализацию интерфейса Writer. Я так же добавил две функции помошников. Первая - определяет URL указывающий на внутреннюю страницу. Вторая - проверяет, что список не содержит дубликатов ссылок.

Вновь запустим программу для простого сайта:

go run main.go retreiver.go http://www.sfeir.com

Получаем все гиперссылки для всего сайта.

Ура! Мы сделали большую работу. Следующее, мы собираемся посмотре, как улучшить retrieve функцию для того, чтобы получить ссылки с других страниц, в том числе. Я предлагю рассмотреть использование рекурсии. Мы создадим другую функцию под названием crawl и эта функция будет вызывать себя рекурсивно, с каждой следующей полученой ссылкой. Так же, нам нужно отслеживать посещенные страницы, чтоби избежать повторных переходов по ссылкам. Прроверим:

// part of retreiver.go
var visited = make(map[string]bool)

func crawl(uri string) {

	links, _ := retrieve(uri)

	for _, l := range links {
		if !visited[l] {
			fmt.Println("Fetching", l)
			visited[uri] = true
			crawl(l)
		}
	}
}

Теперь можно вызывать crawl вместо retrieve функции в main.go. Код будет следующим.

package main

import (
	"fmt"
	"os"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	url := os.Args[1]

	crawl(url)

}

Запустим нашу программу:

go run main.go retreiver.go http://www.sfeir.com

Реализуем слушателя событий через каналы.

В прошлой части, мы увидели как полученные URL отображаются внутри crawl функции. Это не лучшее решение особенно когда вам нужно делать больеш чем просто вывод на экране. Чтобы это исправить, в основном, нам нужно реализовать слушателя событий для получения URL через каналы. Давайте посмотрим на это:

// same imports
type link struct {
	source string
	target string
}

type retriever struct {
	events  map[string][]chan link
	visited map[string]bool
}

func (b *retriever) addEvent(e string, ch chan link) {
	if b.events == nil {
		b.events = make(map[string][]chan link)
	}
	if _, ok := b.events[e]; ok {
		b.events[e] = append(b.events[e], ch)
	} else {
		b.events[e] = []chan link{ch}
	}
}

func (b *retriever) removeEvent(e string, ch chan link) {
	if _, ok := b.events[e]; ok {
		for i := range b.events[e] {
			if b.events[e][i] == ch {
				b.events[e] = append(b.events[e][:i], b.events[e][i+1:]...)
				break
			}
		}
	}
}

func (b *retriever) emit(e string, response link) {
	if _, ok := b.events[e]; ok {
		for _, handler := range b.events[e] {
			go func(handler chan link) {
				handler <- response
			}(handler)
		}
	}
}

func (b *retriever) crawl(uri string) {

	links, _ := b.retrieve(uri)

	for _, l := range links {
		if !b.visited[l] {
			b.emit("newLink", link{
				source: uri,
				target: l,
			})
			b.visited[uri] = true
			b.crawl(l)
		}
	}
}

func (b *retriever) retrieve(uri string) ([]string, error) {
	resp, err := http.Get(uri)
	if err != nil {
		fmt.Println("Error:", err)
		return nil, err
	}

	doc, readerErr := goquery.NewDocumentFromReader(resp.Body)
	if readerErr != nil {
		fmt.Println("Error:", readerErr)
		return nil, readerErr
	}
	u, parseErr := url.Parse(uri)
	if parseErr != nil {
		fmt.Println("Error:", parseErr)
		return nil, parseErr
	}
	host := u.Host

	links := []string{}
	doc.Find("a[href]").Each(func(index int, item *goquery.Selection) {
		href, _ := item.Attr("href")
		lu, err := url.Parse(href)
		if err != nil {
			fmt.Println("Error:", err)
			return
		}
		if isInternalURL(host, lu) {
			links = append(links, u.ResolveReference(lu).String())
		}

	})

	return unique(links), nil
}

// same helper functions

Как можно увидеть, у нас есть дополнительные функции, для управлеия событиями для данного retriever. Для этого кода я использовал go ключевое слово. В основном, написание go foo() запустит функцию foo запуститься асинхронно. В нашем случае мы использовали go которая является анонимной функцией, чтобы послать парамметр события(ссылку) всем слушателям через канал. Я указал тип канала данных как link, который содержит источник и целевую страницу.

Теперь давайте взглянем на main функцию:

package main

import (
	"fmt"
	"os"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	url := os.Args[1]
	ev := make(chan link)
	r := retriever{visited: make(map[string]bool)}
	r.addEvent("newLink", ev)

	go func() {
		for {
			l := <-ev
			fmt.Println(l.source + " -> " + l.target)
		}
	}()

	r.crawl(url)

}

Вновь используя go ключевое слово, в этот раз чтобы получать параметры события отправленные crawl функцией. Если мы запустим нашу программу, теперь мы должны увидеть все внутренние ссылки для данного сайта.

Этого достаточно для ползунка.

Интеграция с Neo4j

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

go get github.com/neo4j/neo4j-go-driver/neo4j

После установки драйвера, нам нужно создать некую базовую функцию которая позволит нам работать с Neo4j. Создадим файл под названием neo4j.go:

package main

import (
	"github.com/neo4j/neo4j-go-driver/neo4j"
)

func connectToNeo4j() (neo4j.Driver, neo4j.Session, error) {

	configForNeo4j40 := func(conf *neo4j.Config) { conf.Encrypted = false }
	
	driver, err := neo4j.NewDriver("bolt://localhost:7687", neo4j.BasicAuth(
		"neo4j", "alice!in!wonderland", ""), configForNeo4j40)

	if err != nil {
		return nil, nil, err
	}

	sessionConfig := neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}
	session, err := driver.NewSession(sessionConfig)
	if err != nil {
		return nil, nil, err
	}

	return driver, session, nil
}

func createNode(session *neo4j.Session, l *link) (neo4j.Result, error) {
	r, err := (*session).Run("CREATE (:WebLink{source: $source, target: $target}) ", map[string]interface{}{
		"source": l.source,
		"target": l.target,
	})

	if err != nil {
		return nil, err
	}

	return r, err
}

func createNodesRelationship(session *neo4j.Session) (neo4j.Result, error) {
	r, err := (*session).Run("MATCH (a:WebLink),(b:WebLink) WHERE a.target = b.source CREATE (a)-[r:point_to]->(b)", map[string]interface{}{})

	if err != nil {
		return nil, err
	}

	return r, err
}

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

Чтобы создать WebLink ноду нам просто нужно запустить следующий запрос:

CREATE (:WebLink{source: "http://www.sfeir.com/", target: "http://www.sfeir.com/en/services"})

Как только нода будет создана, нам нужно создать отношение между ними запустив следующий запрос:

MATCH (a:WebLink),(b:WebLink) 
WHERE a.target = b.source 
CREATE (a)-[r:point_to]->(b)

Давайте обновим нашу main функцию.

package main

import (
	"fmt"
	"os"

	"github.com/neo4j/neo4j-go-driver/neo4j"
)

func main() {

	if len(os.Args) < 2 {
		fmt.Println("Web site url is missing")
		os.Exit(1)
	}

	driver, session, connErr := connectToNeo4j()

	if connErr != nil {
		fmt.Println("Error connecting to Database:", connErr)
		os.Exit(1)
	}

	defer driver.Close()

	defer session.Close()

	url := os.Args[1]
	ev := make(chan link)
	r := retriever{visited: make(map[string]bool)}
	r.addEvent("newLink", ev)

	go func(session *neo4j.Session) {
		for {
			l := <-ev
			fmt.Println(l.source + " -> " + l.target)
			_, err := createNode(session, &l)

			if err != nil {
				fmt.Println("Failed to create node:", err)
			}

		}
	}(&session)

	r.crawl(url)

	fmt.Println("Creation of relationship between nodes.. ")
	_, qErr := createNodesRelationship(&session)

	if qErr == nil {
		fmt.Println("Nodes updated")
	} else {
		fmt.Println("Error while updating nodes:", qErr)
	}

}

Использую три функциюю объявленные в neo4j.go наша программа создаст подключение к neo4j, подпишется на newLink событие для вставки нод и наконец обновит связи нод. Я использовал defer ключево слово, чтобы сослаться на исполнение функции до тех пор пока не завершиштся main функция. Давайте запустим в последний раз:

go run main.go retreiver.go neo4j.go http://www.sfeir.com

Чтобы проверить результат в Neo4j вы можете запустить следующий запрос в вашем Neo4j браузере.

MATCH (n:WebLink) RETURN count(n) AS count

Или этот запрос отобразит все ноды:

MATCH (n:WebLink) RETURN n

Выводы

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

Пишем REST API клиента на Go

Api клиенты, очень полезны когда вы выставляете ваш REST API на публику. И go делает это проще для вас, как для разработчика, так же как и для ваших пользователей, спасибо его структуре и типу систем. Но как определить хороший Api клиент?

В этой инструкции, мы собираемся обсудить несколько хороших практик написания SDK на Go.

Мы будем исполльзовть Facest.io Api как пример.

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

Ответы на эти вопросы помогут вам создать правильную структуру.

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

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

Создаем поля BaseURL и HTTPClient экспортируемыми, чтобы пользователь мог использовать их в своём HTTP клиенте, если это нужно.

package facest

import (
    "net/http"
    "time"
)

const (
    BaseURLV1 = "https://api.facest.io/v1"
)

type Client struct {
    BaseURL    string
    apiKey     string
    HTTPClient *http.Client
}

func NewClient(apiKey string) *Client {
    return &Client{
        BaseURL: BaseURLV1,
        apiKey:  apiKey,
        HTTPClient: &http.Client{
            Timeout: time.Minute,
        },
    }
}

Продолжаем: реализуем "Get Faces" точку доступа, которая возвращает список результатов и поддерживает постраничный вывод, что говорит о том, что постраничный вывод - это опция ввода.

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

type errorResponse struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

type successResponse struct {
    Code int         `json:"code"`
    Data interface{} `json:"data"`
}

Убедитесь, что вы не пишете все точки доступа в одном .go файле, но сгрупируйте их используя отдельные файлы, для примера вы можете сгрупировать их по типу, всё что начинается с /v1/faces идет в fasec.go файл.

Я обычно начинаю с определения типаов, вы можете это сделать вручную сконвертировав JSON в JSON-to-Go инструменте.

package facest

import "time"

type FacesList struct {
    Count      int    `json:"count"`
    PagesCount int    `json:"pages_count"`
    Faces      []Face `json:"faces"`
}

type Face struct {
    FaceToken  string      `json:"face_token"`
    FaceID     string      `json:"face_id"`
    FaceImages []FaceImage `json:"face_images"`
    CreatedAt  time.Time   `json:"created_at"`
}

type FaceImage struct {
    ImageToken string    `json:"image_token"`
    ImageURL   string    `json:"image_url"`
    CreatedAt  time.Time `json:"created_at"`
}

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

type FacesListOptions struct {
    Limit int `json:"limit"`
    Page  int `json:"page"`
}

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

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

func (c *Client) GetFaces(ctx context.Context, options *FacesListOptions) (*FacesList, error) {
    return nil, nil
}

Время сделать сам API:

func (c *Client) GetFaces(ctx context.Context, options *FacesListOptions) (*FacesList, error) {
    limit := 100
    page := 1
    if options != nil {
        limit = options.Limit
        page = options.Page
    }

    req, err := http.NewRequest("GET", fmt.Sprintf("%s/faces?limit=%d&page=%d", c.BaseURL, limit, page), nil)
    if err != nil {
        return nil, err
    }

    req = req.WithContext(ctx)

    res := FacesList{}
    if err := c.sendRequest(req, &res); err != nil {
        return nil, err
    }

    return &res, nil
}
func (c *Client) sendRequest(req *http.Request, v interface{}) error {
    req.Header.Set("Content-Type", "application/json; charset=utf-8")
    req.Header.Set("Accept", "application/json; charset=utf-8")
    req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.apiKey))

    res, err := c.HTTPClient.Do(req)
    if err != nil {
        return err
    }

    defer res.Body.Close()

    if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
        var errRes errorResponse
        if err = json.NewDecoder(res.Body).Decode(&errRes); err == nil {
            return errors.New(errRes.Message)
        }

        return fmt.Errorf("unknown error, status code: %d", res.StatusCode)
    }

    fullResponse := successResponse{
        Data: v,
    }
    if err = json.NewDecoder(res.Body).Decode(&fullResponse); err != nil {
        return err
    }

    return nil
}

Так как все точки доступа API работают одинаково, функция помошника sendRequest создана, чтобы избежать повторения в коде. Он задает большиство заголовков(content type, auth header), создает запрос, проверяет на ошибки, парсит ответ.

Отменит, что мы предполагаем ответы < 200 и >= 400, как ошибки, в этом случае парсится ответ из errorResponse. Понятно, что это зависит от архитектуры API, ваша Api может по разному обрабатывать ошибки.

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

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

Тесты востребованны в данном месте, и есть возможность двух типов: unit тесты, и интеграционные тесты. Для второго будем вызвать настоящий API. Напишем простой тест:

// +build integration

package facest

import (
    "os"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestGetFaces(t *testing.T) {
    c := NewClient(os.Getenv("FACEST_INTEGRATION_API_KEY"))

    ctx := context.Background()
    res, err := c.GetFaces(nil)

    assert.Nil(t, err, "expecting nil error")
    assert.NotNil(t, res, "expecting non-nil result")

    assert.Equal(t, 1, res.Count, "expecting 1 face found")
    assert.Equal(t, 1, res.PagesCount, "expecting 1 PAGE found")

    assert.Equal(t, "integration_face_id", res.Faces[0].FaceID, "expecting correct face_id")
    assert.NotEmpty(t, res.Faces[0].FaceToken, "expecting non-empty face_token")
    assert.Greater(t, len(res.Faces[0].FaceImages), 0, "expecting non-empty face_images")
}

Этот тест использует env переменную где указывается API ключ. Поступая таким образом, мы убеждаемся, что они не публичны. Позже мы можем сконфигурировать эту переменную с помощью окружения используемого в CI\CD.

Эти тесты, отьделены от юнит тестов(так как они выполняются гораздо дольше): execute):

go test -v -tags=integration

Документация.

Делайте ваш SDK не требующий описания с понятными типами и абстракциями. не выставляйте много информации. Обычно достаточно предоставить godoc ссылку как главную документацию.

Свместимость и версионирование.

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

Пример использования Jenkins REST API на Golang

Я собираюсь описать как вызывать Jenkins REST API используя GO. Создадим три Go файла:

  1. packages/helpers/helpers.go
  2. packages/jenkins/jenkins.go
  3. main.go

Исходный код доступен по адресу: https://github.com/mohdnaim/jenkins_rest_api

helpers.go содержит функции помощники. В нем будут расположены функции для чистоты и организации кода.

jenkins.go содержит функции связанные c Jenkins. Это функции IsJobExist() для проверки существования конвеера задачи или билда, CopyJenkinsJob() для копирования Jenkins задания и DownloadConfigXML() для скачивания конфигурации задачи в формате XML.

main.go это главный файл, точка старта нашей программы.

//main.go
package main

import (
	"fmt"
	"log"
	"strings"
	helpers "./packages/helpers"
	jenkins "./packages/jenkins"
)

func main() {
	// Обязательные настройки
	jenkinsURL := "АДРЕС СЕРВЕРА"
	jenkinsUsername := "ИМЯ ПОЛЬЗОВАТЕЛЯ"
	jenkinsAPIToken := "API ТОКЕР"
	jenkins.JenkinsDetails = jenkins.Details{jenkinsURL, jenkinsUsername, jenkinsAPIToken}
	xmlFolder := "xml"
	// 1. Получаем все доступные провекты\задачи 
	allProjectNames := jenkins.GetAllProjectNames()
	// 2. Фильтруем наши проекты которые мы ищем 
	filteredProjectNames := make([]string, 0)
	for _, projectName := range allProjectNames {
		// Что-то делаем 
		// Добавляем в другой срез основываясь на условии
		if strings.HasPrefix(projectName, "prefix") {
			filteredProjectNames = append(filteredProjectNames, projectName)
		}
	}
	// 3. Для каждого проекта получаем его  config.xml
	for _, projectName := range filteredProjectNames {
		xmlPath := fmt.Sprintf("%s/%s.xml", xmlFolder, projectName)
		if err := jenkins.DownloadConfigXML(projectName, xmlPath); err != nil {
			log.Println("error download config.xml for project:", projectName)
			continue // пропускаем
		}
	}
	// 4. Изменяем config.xml
	files := helpers.GetFilenamesRecursively(xmlFolder)
	for _, xmlFile := range files {
		log.Println(xmlFile)
	}
	// 4b. Переписываем  config.xml
	// 5. http POST запрос обновления config.xml
	for _, xmlFile := range files {
		tmpSlice := strings.Split(xmlFile, "/")
		projectName := tmpSlice[len(tmpSlice)-1]
		log.Println(projectName)
		if err := jenkins.PostConfigXML(projectName, xmlFile); err != nil {
			log.Println("error postconfigxml:", projectName)
		}
	}
}

Нам нужно настроить детали Jenkins объекта. Укажем корректное значение для следующих переменных:

	jenkinsURL := "АДРЕС СЕРВЕРА"
	jenkinsUsername := "ИМЯ ПОЛЬЗОВАТЕЛЯ"
	jenkinsAPIToken := "API ТОКЕР"

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

Строка #19 Укажем папку в которой будем хранить XML файл, который является конфигурацией Jenkins задачи.

Строка #22 Получаем имя всех проектов существующих в Jenkins. Если мы посмотрим на реализацию функции GetAllProjectNames() в jenkins.go, функция вызывает DownloadFileToBytes(). Она делаеет HTTP запрос на /api/json используя имя пользователя и пароль в заголовке запроса. Точка доступа вернет имя проекта в JSON формате. Затем преобразуется JSON в result словарь. Любое имя в словаре добавляет имя в allProjectNames массив.

Строка #24 Отсортируем наши проекты, так как мы хотим. Пройдемся по allProjectNames массива и если проект встречает определенный критерий, мы добавляем имя проекта в filteredProjectNames массив который мы будем использовать вместо allProjectNames.

Строка #35 Для каждого имени проекта в filteredProjectNames мы получаем его конфигурационный файл config.xml.

Строка #45 Вызываем фукнкцию GetFileNamesRecursively() в helpers.go для чтения всех имен файлов в xmlFolder.

Строка #50 Мы делаем всё что хотим с config.xml файлом, например переименовываем имя проекта, добавляем параметры сборки, изменяем права и т.д.

Я написал скрипт на Python вместо Go для управления config.xml файлом, так как Pyehon имеет больше библиотек чем Go, которые могут помочь нам для управления строками и файлами.

Строка #52 Наконец, после того, как мы создали изменения в строке 50, мы отправляем изменения выполняя POST запрос в Jenkins.

//helpers.go
package helpers

import (
	"os"
	"path/filepath"
)
// StringInSlice ...
func StringInSlice(a string, list []string) bool {
	for _, b := range list {
		if b == a {
			return true
		}
	}
	return false
}
// GetFilenamesRecursively ...
func GetFilenamesRecursively(path string) []string {
	var files []string
	err := filepath.Walk("xml", func(path string, info os.FileInfo, err error) error {
		files = append(files, path)
		return nil
	})
	if err != nil {
		panic(err)
	}
	return files
}
//jenkins.go
package jenkins
import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"reflect"
)
// Details ...
type Details struct {
	URL      string
	Username string
	APIToken string
}
// JenkinsDetails ...
var JenkinsDetails = Details{}
// IsJobExist ...
func IsJobExist(projectName string) bool {
	fullURL := fmt.Sprintf("%sjob/%s", JenkinsDetails.URL, projectName)
	request, err := http.NewRequest("GET", fullURL, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		log.Println("project doesn't exist:", projectName)
		return false
	}
	if resp.StatusCode == 200 {
		// log.Println("project exists:", projectName)
		return true
	}
	log.Println("project doesn't exist:", projectName)
	return false
}
// CopyJenkinsJob ...
func CopyJenkinsJob(srcJob string, dstJob string) error {
	fullURL := fmt.Sprintf("%s%s?name=%s&mode=copy&from=%s", JenkinsDetails.URL, "createItem", dstJob, srcJob)
	request, err := http.NewRequest("POST", fullURL, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		return err
	}
	if resp.StatusCode == 200 {
		return nil
	}
	return fmt.Errorf("error copying job: %s", srcJob)
}
// DownloadConfigXML ...
func DownloadConfigXML(projectName string, dstFilename string) error {
	fullURL := fmt.Sprintf("%sjob/%s/config.xml", JenkinsDetails.URL, projectName)
	if DownloadFile(fullURL, dstFilename) == nil {
		return nil
	}
	return fmt.Errorf("error downloading file %s", fullURL)
}
// PostConfigXML ...
func PostConfigXML(projectName string, filename string) error {
	// read content of file
	data, err := os.Open(filename)
	if err != nil {
		log.Fatal(err)
		return err
	}
	fullURL := fmt.Sprintf("%sjob/%s/config.xml", JenkinsDetails.URL, projectName)
	request, err := http.NewRequest("POST", fullURL, data)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	// perform the request
	resp, err := client.Do(request)
	if err != nil {
		return err
	}
	if resp.StatusCode == 200 {
		// log.Printf("success postConfigXML: %s %s %s", projectName, filename, resp.Status)
		return nil
	}
	return fmt.Errorf("error postConfigXML: %s %s %s", projectName, filename, resp.Status)
}
// DownloadFile ...
func DownloadFile(url string, filepath string) error {
	// Create the file
	out, err := os.Create(filepath)
	if err != nil {
		return err
	}
	defer out.Close()
	// Get the data
	request, err := http.NewRequest("GET", url, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	// Write the body to file
	_, err = io.Copy(out, resp.Body)
	if err != nil {
		return err
	}
	return nil
}
// DownloadFileToBytes ...
func DownloadFileToBytes(url string) ([]byte, error) {
	request, err := http.NewRequest("GET", url, nil)
	request.SetBasicAuth(JenkinsDetails.Username, JenkinsDetails.APIToken)
	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err) // writes to standard error
	}
	return bodyBytes, nil
}
// GetAllProjectNames ...
func GetAllProjectNames() []string {
	allProjectNames := make([]string, 0)
	allProjectsURL := fmt.Sprintf("%sapi/json?pretty=true", JenkinsDetails.URL)
	if respBytes, err := DownloadFileToBytes(allProjectsURL); err == nil {
		// json --> map
		var result map[string]interface{}
		json.Unmarshal([]byte(respBytes), &result)
		// if map has slice 'jobs', iterate over it
		if jobs, keyIsPresent := result["jobs"]; keyIsPresent && reflect.TypeOf(jobs).Kind() == reflect.Slice {
			jobs2 := result["jobs"].([]interface{})
			for _, job := range jobs2 {
				// log.Println(job)
				_map, _ := job.(map[string]interface{}) // assert to map
				allProjectNames = append(allProjectNames, _map["name"].(string))
			}
		}
	}
	return allProjectNames
}

Простая инструкция изучения Цепочи обязаностей в Golang

Что такое цепочка обязаностей(Chain of Responsibility)?

Проблема

Представим, вы рабоаете над приложениме онлаин курсов. Вот список проверок перед тем как можно использовать курс.

Having a big headache

Solution

Diagram

Handler

Base handler

Main

Concrete Handlers

Pros and Cons Pros

Cons

How to code a simple Chain of Responsibility in Golang?

Section

package main

type section interface {
	execute(*task)
	setNext(section)
}

Material

package main

import (
	"fmt"
)

type material struct {
	next section
}

func (m *material) execute(t *task) {
	if t.materialCollected {
		fmt.Println("Material already collected")
		m.next.execute(t)
		return
	}
	fmt.Println("Material section gathering materials")
	t.materialCollected = true
	m.next.execute(t)
}

func (m *material) setNext(next section) {
	m.next = next
}

Assembly

package main

import (
	"fmt"
)

type assembly struct {
	next section
}

func (a *assembly) execute(t *task) {
	if t.assemblyExecuted {
		fmt.Println("Assembly already done")
		a.next.execute(t)
		return
	}
	fmt.Println("Assembly section assembling...")
	t.assemblyExecuted = true
	a.next.execute(t)
}

func (a *assembly) setNext(next section) {
	a.next = next
}
view raw

Packaging

package main

import (
	"fmt"
)

type packaging struct {
	next section
}

func (p *packaging) execute(t *task) {
	if t.packagingExecuted {
		fmt.Println("Packaging already done")
		p.next.execute(t)
		return
	}
	fmt.Println("Packaging section doing packaging")
}

func (p *packaging) setNext(next section) {
	p.next = next
}

Task

package main

type task struct {
	name				string
	materialCollected	bool
	assemblyExecuted	bool
	packagingExecuted	bool
}

Main

package main

func main() {
	packaging := &packaging{}

	// set next for assembly section
	assembly := &assembly{}
	assembly.setNext(packaging)

	material := &material{}
	material.setNext(assembly)

	task := &task{name: "truck_toy"}
	material.execute(task)
}

Explanation

section.go is a Handler interface. This interface handling requests and sets the next handler on the chain.

material.go is a Concrete handler. It decides if the request to collect material should be processed and can move up the chain.

assembly.go is a Concrete handler. It decides if the request to perform the assembly work should be processed and can move up the chain.

packaging.go is a Concrete handler. It decides if the request to perform the packaging work should be processed and can move up the chain.

main.go is a client. Initializes up the chain of handlers.

How to run

Command:

go run .

Material section gathering materials
Assembly section assembling…
Packaging section doing packaging

Takeaways

I hope you understand how to code a simple abstract factory in Golang and more importantly understand how a chain of responsibility design pattern can help you to design your code better and to ensure maintainability.

Реализация RSA шифрования и подписи на Golang

Эта статья описывает как работает RSA алгоритм, и как его можно реалоизоват на Go.

RSA (Rivest–Shamir–Adleman) шифрование наиболее широко распространнено для безопасного шифрования данных.

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

RSA Шифрование изнутри

RSA работает генерируя публичный и приватный ключ. public и private ключи создаются одновременно и формируют пару ключей.

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

encryption

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

decryption

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

key distribution

Генерация ключа

Первое, что мы хотим сделать, это создать пару ключей: public и private. Эти ключи случайным образом генерируются, и будут использоваться вдальнейшем.

Мы используем crypto/rsa стандартную библиотеку для генерации ключа и crypto/rand библиотеку для генерации случайных чисел.

// Метод GenerateKey принимает reader, который возвращает случайные биты, и количество бит. 
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
	panic(err)
}

//  Публичный ключ это часть *rsa.PrivateKey структуры
publicKey := privateKey.PublicKey

// Используем приватный и публичные ключи 
// ...

Переменные publicKey и privateKey будут использоваться шифрования и расшифровки соответственно.

Шифрование

Мы буддем использовать метод EncryptOAEP для шифрования случайного сообщения. Мы должны предоставить этому методу:

  1. Хэширующую функцию, выбранную таким образом, что даже если ввод изменится едва ли, вывод должен менятся совершенно. Алгоритм SHA256 подходит для этого.
  2. Случайный reader используемый для создания случаный битов, таким образом, чтобы каждый одинаковый ввод будет отдавать тот же вывод.
  3. Публичный ключ ранее сгенерированный.
  4. Сообщение которое мы шифруем.
  5. Дополнительные параметры, которые мы опустим в этот раз.
encryptedBytes, err := rsa.EncryptOAEP(
	sha256.New(),
	rand.Reader,
	&publicKey,
	[]byte("super secret message"),
	nil)
if err != nil {
	panic(err)
}

fmt.Println("encrypted bytes: ", encryptedBytes)

Функция выведет зашифрованные байты, которые выглядят как шум.

Расшифровка

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

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

Структура *rsa.PrivateKey имеет метод Decrypt который мы будем использовать для получения оригинальной информации обратно из зашифрованной.

Данные необходимые для расшифровки:

  1. Зашифрованные данные(называемые cipher text)
  2. Хэш который мы использовать для шафрования данных
// Первый аргумент это генератор случайных данных( rand.Reader который мы использовали ранее)
// Мы можем указать это значение как nil
// OAEPOptions в конце означают, что мы шифруем данные используя OAEP, и то мы используем SHA256 в качестве входного хэша.
decryptedBytes, err := privateKey.Decrypt(nil, encryptedBytes, &rsa.OAEPOptions{Hash: crypto.SHA256})
if err != nil {
	panic(err)
}

// Мы возвращаем информацию в оригинальной форме байт, которые мы приводим к строке и выводим. 
fmt.Println("decrypted message: ", string(decryptedBytes))

Подпись и проверка

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

Что это значит? Что вместо маскирования содержания оригинального сообщения(как это было сделано ранее), часть данных созданная из сообщения называется signature сигнатурой.

signing

У кого есть сигнатура, сообщение и публичный ключ, могут использвать RSA для проверки, того, что сообщение действительно пришло от того, чей публичный ключ используется. Если данные или подпись не совпадают, проверка завершается неудачей.

verification

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

msg := []byte("verifiable message")

// Перед попдписью, на мнужен хэш сообщения
// Хэш - то что мы подписали. 
msgHash := sha256.New()
_, err = msgHash.Write(msg)
if err != nil {
	panic(err)
}
msgHashSum := msgHash.Sum(nil)

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

signature, err := rsa.SignPSS(rand.Reader, privateKey, crypto.SHA256, msgHashSum, nil)
if err != nil {
	panic(err)
}

// Для проверки подписи, мы предоставляем публичный ключ, алгоритм хэширования,
// хэш сумму нашего сообщения и подпись которую мы ранее сгенеерировали. 
// А так же "options" параметры которые мы опустим
err = rsa.VerifyPSS(&publicKey, crypto.SHA256, msgHashSum, signature, nil)
if err != nil {
	fmt.Println("could not verify signature: ", err)
	return
}
// Если не получили ошибки от метода `VerifyPSS`, значит наша подпись верна.
fmt.Println("signature verified")

Выводы

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

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

  1. Данные, которые вы пытаетесь зашифровать должны быть гораздо короче чем сила вашего ключа. Для примера: EncryptOAEP документация говорит, что сообщение не должно быть больше, чем длинна публичного модуля минус двойная длинна хэша, и дальше минус 2.
  2. Используемый алгоритм хэширования должен быть подобран к случаю. SHA256(который мы используем) является достаточным для большинства случаем, но вы можете захотет использовать что-то по серьезнее типа SHA512 для критически важных данных приложения.

Строим свой первый Rest API с помощью GO

У нас есть три необходимые части для построения.

API

Если вы близки к компьютерам достаточно давно, вы возможно слышали об этих вещах. Что такое API?

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

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

func addNumber(a, b int) int {
    // DO AMAZING MATH HERE
    // and return the result
}

Даже если вы совсем новичок в go, вы можете сказать, что функция возвращает результат сложения двху чисел.

Как пользователь функции вы просто вызываете функцию и не беспокоитесь о том, функция выполняет свою работу(не доверяйтся всем функциям подряд)

Это всё что такое API. API может быть функция которую вы написали, или функция из библиотеки или метод фреймворка, или http точка доступа.

Rest API

Большинство API написаные в наши дни это web api. Только не цитируйте меня, так как я не делал никаких исследований чтобы получить настоящее число. Но учитывая количество web сервисов и прилжений я не думаю, что я далеко от правды.

What is REST

REST - это акроним для REpresentational State Transfer(передача репрезентативного состояния). Это архитекторный стиль распределнной гипермедия системы и была впервые представлена Роем Филдингом в 2000 году в его известной диссертации.

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

Ведущие принципы REST

  1. Клиент-сервер - Разделяя проблемы пользовательских интерфейсов от проблем хранения данных, вы улучшаем переносимость пользовательского интерфейса на многие платформы и улучшаем масштабируемость упрощением сервреных компонентов.
  2. Не хранит состояние - каждый запрос от клиента к серверу должен содержать всю необходимую информацию для понимания запросов, и не может воспользоваться любых сохраненным содеражением на сервере. Состояние сессии отсюда хранится полностью на клиенте.
  3. Кешируемость - Ограничения кэша требуют. чтобы данные в ответе на запрос были явно или не явно обозначены как кэшируемые или не кешуруемые. Если ответ кэшируемый, тогда право кэша клиента для переиспользования данные ответа в дальнейшем, похожем запросе.
  4. Универсальный интерфейс - Применя принцыпи програмной инженерии в целом к компонентам интерфейса, общая архитектура системы упрощена и видимость взаимодействий улучшена. Для того, чтобы получить универсальный интерфейс, множество универсальных ограничений нужны для указания поведения компонентов. REST определяется четырьмя ограничивающими интерфейсами:
    • Определение ресурса
    • Упралвение ресурсами через репрезентацию
    • Самоописываемое сообщение
    • Гипермедия - как движок состояния приложения.
  5. Слоёная система - Стиль слоёной системы позволяет архитектуре быть составленной из иерархических слоёв ограниченными поведением компонентов, так, что каждый компоненто не может "смотреть сковозь" слой с которым происходит взаимодействие.
  6. Код по запросу(по желанию) - REST позволяет расширить функциональность клиенту скачать и выполнив код в форме аплетов или скриптов. Это урощает клиентов с помощью сокращения количества особенностей требуемых для погдготовки реализации.

HTTP глаголы

Вот несколько условностей соблюдаемы HTTP API. Это не часть спецификации REST. Но нам нужно это понять, что бы использовать REST API по-понной.

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

GET - метод запрашивает представление указанного ресурка. Запросы используещие GET должны только получать данные.

HEAD - метод просит ответ идентичный GET запросу, но опускает body.

POST - методв используется для отправки сущности указанному ресурсу, часто является причиной изменения изменения состояния или имеет побочный эффект на сервер.

PUT - метод заменяет все текущее представление целевого ресурса загруженным в запросе.

DELETE - метод удаляет указанный ресурс.

CONNECT - метод устанавливает тоннель к серверу указанному как целевой.

OPTIONS - метод используется для описания возможностей подключения к удаленному ресурсу.

TRACE - метод производит сообщение петлевого контроля на пути к ресурсу цели.

PATCH - метод используется для применения частичных изменений ресурса.

ЭТО ВСЁ ВРАНЬЕ.

Статус коды

1xx Информация

100 Continue 101 Switching Protocols 102 Processing 2xx Success

200 OK 201 Created 202 Accepted 203 Non-authoritative Information 204 No Content 205 Reset Content 206 Partial Content 207 Multi-Status 208 Already Reported 226 IM Used 3xx Redirects

300 Multiple Choices 301 Moved Permanently 302 Found 303 See Other 304 Not Modified 305 Use Proxy 307 Temporary Redirect 308 Permanent Redirect 4xx Client Error

400 Bad Request 401 Unauthorized 402 Payment Required 403 Forbidden 404 Not Found 405 Method Not Allowed 406 Not Acceptable 407 Proxy Authentication Required 408 Request Timeout 409 Conflict 410 Gone 411 Length Required 412 Precondition Failed 413 Payload Too Large 414 Request-URI Too Long 415 Unsupported Media Type 416 Requested Range Not Satisfiable 417 Expectation Failed 418 I'm a teapot 421 Misdirected Request 422 Unprocessable Entity 423 Locked 424 Failed Dependency 426 Upgrade Required 428 Precondition Required 429 Too Many Requests 431 Request Header Fields Too Large 444 Connection Closed Without Response 451 Unavailable For Legal Reasons 499 Client Closed Request 5xx Server Error

500 Internal Server Error 501 Not Implemented 502 Bad Gateway 503 Service Unavailable 504 Gateway Timeout 505 HTTP Version Not Supported 506 Variant Also Negotiates 507 Insufficient Storage 508 Loop Detected 510 Not Extended 511 Network Authentication Required 599 Network Connect Timeout Error This also has no actual meaning.

Терминология

Ниже - самые важные понятия связанные с REST API.

API Endpoint

Вот как выглдяит пример такой точки:

https://www.github.com/golang/go/search?q=http&type=Commits

Разделим URL на части:

protocol	subdomain	domain	path	Port	query
http/https	subdomain	base-url	resource/some-other-resource	some-port	key value pair
https	www	github.com	golang/go/search	80	?q=http&type=Commits

Протоколы

Как браузер или клиент должен взаимодействовать с сервером.

Поддомен

Подраздел главного домена.

Порт

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

Петь

Пусть - параметры REST API отражающие ресурсы.

https://jsonplaceholder.typicode.com/posts/1/comments
posts/1/comments

Этот пусть отражает коментарии 1го поста ресурса.

Базовой структурой является

top-level-resource/<some-identifier>/secondary-resource/<some-identifier>/...

Запрос

Запросы - пары ключ-значение информации, используемый в основном для целей фильтрования.

https://jsonplaceholder.typicode.com/posts?userId=1

Часть после ? это параметры запроса. У нас есть только один запрос тут: userID=1.

Заголовки

Это не часть самого URL, но заголовки это часть сетевого компонента посылаемые клиентом или сервером. В зависимости от того, кто послал его. Есть 1 типа заголовков.

  1. Заголовок запроса (client -> server)
  2. Заголовок ответа (server -> client)

Тело

Вы можете добавить дополнительную инфомацию в обза запроса к серверу и к ответу от сервера.

Тип ответа

Обычно JSON или XML.

В наши дни это обычно JSON.

Rest API на GO

Это то почему вы тут. Ну или я, по крайней, мере я надеюсь на это.

Если вы пишите REST API, почему вы должны выбрать Go?

net/http

Стандартная библиотека в go идущая с net/http пакетом, что является отличной точкой отсчета для построения REST API. И большинство других библиотек добавлюят особенности тоже взаимодействуют с net/http пакетом, поэтому понимание пакется является критичным для использования golang в качестве REST API.

net/http

Нам, возможно, не нужно знать всё в пакете net/http. но есть несколько вещей, который мы должны знать для начала.

Интерфейс обработчика

нам нужно помнить интерфейс обработчика:

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

Ну вот и он.

У него есть тольк один метод и только.

Структура или объект будет обработан если он имеет один метод ServeHTTP который принимает ResponseWriter и указатель на Request.

Теперь, с этим знанием, мы готовы к некоторым увечьям.

Начнем

Я думаю мы готовы начать.

Мы узнали много теории. Я обещал что вы создадите свой первый RestAPI.

Простой Rest API

В папке где вы хотите писать ваш код выполните команду в терминале:

go mod init api-test

Созайте новый файл, можно назвать его как угодно.

Я назвал свой main.go

package main

import (
    "log"
    "net/http"
)

type server struct{}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"message": "hello world"}`))
}

func main() {
    s := &server{}
    http.Handle("/", s)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Пройдемся по коду.

В самом начала, у нас стоит строка package main, все go исполняемые файлы должны иметь main пакет. Дальше идут иморты:

Далее идет структура под названием server. Она не имеет полей. Добавим метод ServerHTTP этому server, чтобы удовлетворить обработчик интерфейс. Одна вещь, которую вы можете заметить в go, нам не нужно явно говорить какой из интерфейсов мы реализуем. Компилятор достаточно умен, чтобы выяснить это самостоятельно. В ServerHTTP методе мы настраиваем httpStatus 200, чтобы обозначить, что запрост прошел успешно. Мы видим тип содержания application/json так, что клиент понимает когда мы отправляет ему что-то полезное. В мы пишем {"message": "hello world"} в ответ.

Запустим наш сервер.

go run main.go

Для проверки выполняем команду из соседнего терминала:

curl localhost:8000

В ответ получае json описанный выше. Отличная работа!

Но подождите.

Давайте посмотрим какие другие HTTP глаголы обрабатывает наше приложение.

Теперь попробуем выполлнить POST запрос:

curl localhost:8000 -X POST

Когда мы выполняем запрос, то получаем тот же самый результат.

Это вовсе не баг. Но часто мы будем хотеть чтобы выполнялись разные задачи в зависимости от типа запроса.

Посмотрим, что мы тут можем сделать.

Изменим наш ServerHTTP метод следующим образом:

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    switch r.Method {
    case "GET":
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"message": "get called"}`))
    case "POST":
        w.WriteHeader(http.StatusCreated)
        w.Write([]byte(`{"message": "post called"}`))
    case "PUT":
        w.WriteHeader(http.StatusAccepted)
        w.Write([]byte(`{"message": "put called"}`))
    case "DELETE":
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"message": "delete called"}`))
    default:
        w.WriteHeader(http.StatusNotFound)
        w.Write([]byte(`{"message": "not found"}`))
    }
}

Если наш сервер уже запущен, останавливаем его нажимая CTRL-C.

И заново запускаем, чтобы изменения из файла применились.

go run main.go

Проверяем, как теперь себя ведет reas api, в зависимости от типа запроса.

$ curl localhost:8000
{"message": "get called"}
$ curl localhost:8000 -X POST
{"message": "post called"}
$ curl localhost:8000 -X PUT
{"message": "put called"}
$ curl localhost:8000 -X DELETE
{"message": "delete called"}
$ curl localhost:8000/test
{"message": "not found"}

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

Go команда знала что это будет не удобно, и дала нам HandleFunc метод в http пакете который позволяет нам передавать функцию которая имеет ту же подпись что и ServerHTTP и может обслуживать маршруты.

Немного упростим наш код.

package main

import (
    "log"
    "net/http"
)

func home(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    switch r.Method {
    case "GET":
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"message": "get called"}`))
    case "POST":
        w.WriteHeader(http.StatusCreated)
        w.Write([]byte(`{"message": "post called"}`))
    case "PUT":
        w.WriteHeader(http.StatusAccepted)
        w.Write([]byte(`{"message": "put called"}`))
    case "DELETE":
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"message": "delete called"}`))
    default:
        w.WriteHeader(http.StatusNotFound)
        w.Write([]byte(`{"message": "not found"}`))
    }
}

func main() {
    http.HandleFunc("/", home)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Функциональность не должна измениться.

Gorilla Mux

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

Gorilla Mux очень популярная библиотека которая работает отлично по сравнению с net/http пакетом и помогает нам с некоторыми вещями при создании api.

Использование Gorilla Mux

Чтобы установить этот пакет будем использовать get. Под капотом go get использует git. В папке где лежит go.mod и main.go, запустите:

go get github.com/gorilla/mux

Изменим наш код следующим образом.

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func home(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    switch r.Method {
    case "GET":
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"message": "get called"}`))
    case "POST":
        w.WriteHeader(http.StatusCreated)
        w.Write([]byte(`{"message": "post called"}`))
    case "PUT":
        w.WriteHeader(http.StatusAccepted)
        w.Write([]byte(`{"message": "put called"}`))
    case "DELETE":
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"message": "delete called"}`))
    default:
        w.WriteHeader(http.StatusNotFound)
        w.Write([]byte(`{"message": "not found"}`))
    }
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/", home)
    log.Fatal(http.ListenAndServe(":8080", r))
}

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

HandleFunc HTTP Методы

Но теперь мы можем делать немного больше с помощью HandleFunc, к примеру создание каждого обработчика функции для определенного метода HTTP. Выглядить это будет следующим образом.

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func get(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"message": "get called"}`))
}

func post(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    w.Write([]byte(`{"message": "post called"}`))
}

func put(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusAccepted)
    w.Write([]byte(`{"message": "put called"}`))
}

func delete(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"message": "delete called"}`))
}

func notFound(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusNotFound)
    w.Write([]byte(`{"message": "not found"}`))
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/", get).Methods(http.MethodGet)
    r.HandleFunc("/", post).Methods(http.MethodPost)
    r.HandleFunc("/", put).Methods(http.MethodPut)
    r.HandleFunc("/", delete).Methods(http.MethodDelete)
    r.HandleFunc("/", notFound)
    log.Fatal(http.ListenAndServe(":8080", r))
}

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

Лучше чище, чем умнее.

Подмаршруты

func main() {
    r := mux.NewRouter()
    api := r.PathPrefix("/api/v1").Subrouter()
    api.HandleFunc("", get).Methods(http.MethodGet)
    api.HandleFunc("", post).Methods(http.MethodPost)
    api.HandleFunc("", put).Methods(http.MethodPut)
    api.HandleFunc("", delete).Methods(http.MethodDelete)
    api.HandleFunc("", notFound)
    log.Fatal(http.ListenAndServe(":8080", r))
}

Всё остается тем же самым за исплючением создания sub-router(подмаршрут). Подмаршрут очень полезен, когда нам нужно поддерживать большое количество ресурсов. Он помогает нам группировать содержание, а так же защищает нас от перенабора одного и того же префикса пути.

Пенесем наше api в api/va. Таким образом мы можем создавать v2 и так далее.

Параметры пути и запроса

package main

import (
    "fmt"
    "log"
    "net/http"
    "strconv"

    "github.com/gorilla/mux"
)

func get(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"message": "get called"}`))
}

func post(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    w.Write([]byte(`{"message": "post called"}`))
}

func put(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusAccepted)
    w.Write([]byte(`{"message": "put called"}`))
}

func delete(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"message": "delete called"}`))
}

func params(w http.ResponseWriter, r *http.Request) {
    pathParams := mux.Vars(r)
    w.Header().Set("Content-Type", "application/json")

    userID := -1
    var err error
    if val, ok := pathParams["userID"]; ok {
        userID, err = strconv.Atoi(val)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]byte(`{"message": "need a number"}`))
            return
        }
    }

    commentID := -1
    if val, ok := pathParams["commentID"]; ok {
        commentID, err = strconv.Atoi(val)
        if err != nil {
            w.WriteHeader(http.StatusInternalServerError)
            w.Write([]byte(`{"message": "need a number"}`))
            return
        }
    }

    query := r.URL.Query()
    location := query.Get("location")

    w.Write([]byte(fmt.Sprintf(`{"userID": %d, "commentID": %d, "location": "%s" }`, userID, commentID, location)))
}

func main() {
    r := mux.NewRouter()

    api := r.PathPrefix("/api/v1").Subrouter()
    api.HandleFunc("", get).Methods(http.MethodGet)
    api.HandleFunc("", post).Methods(http.MethodPost)
    api.HandleFunc("", put).Methods(http.MethodPut)
    api.HandleFunc("", delete).Methods(http.MethodDelete)

    api.HandleFunc("/user/{userID}/comment/{commentID}", params).Methods(http.MethodGet)

    log.Fatal(http.ListenAndServe(":8080", r))
}

Давайте посмотрим на параметры функции в строке 36. Мы обрабатываем оба параметра: путь и запрос.

Теперь вы знаете достаточно, чтобы быть "опасным".

Книжное API

В Kaggle есть набор данных по книгам. Это csv файл с 13000 книгами. Мы будем использовать его для создания нешго api.

books.csv

Файл можно найти выше.

Склонируем репозиторий

В отдельной папке.

git clone https://github.com/moficodes/bookdata-api.git

Пройдемся по коду

Есть ва пакета внутри кода. Один называется datastore, другой - loader.

un app

Из корня репозитория запустите:

go run .

Точки доступа

У приложения есть несколько точек доступа. Все точки доступа api имеют префикс /api/v1. Чтобы достичь какую либо из них, нужно использовать базовый адрес: baseurl:8080/api/v1/{endpoint}

Получить книги для автора:

"/books/authors/{author}" 

Можно указать параметр запроса для пределов ratingAbove и ratingBelow


Получить книги по названию.

"/books/book-name/{bookName}"

Можно указать параметр запроса для пределов ratingAbove и ratingBelow


Получить книгу по ISBN

"/book/isbn/{isbn}"

Удалить книгу по ISBN

"/book/isbn/{isbn}"

Создать новую книгу

"/book"

Echo framework + GORM = Огненно быстрое Golang приложение на стороне сервера. Пример аутентификации.

В статье, я хочу показать пример реализации входа, выхода и регистрации используя Go фреймворк Echo и Gorm для postgreSQL. Для аутентификации пользователей будет использоваться JWT(Json web token).

Для начала создадим main.go:

package main

import (
	"app1/helper"
	"app1/models"
	"fmt"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres"
	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

func main() {
	configuration := helper.GetConfig()

	gormParameters := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable", configuration.DbHost, configuration.DbPort, configuration.DbName, configuration.DbUsername, configuration.DbPassword)
	gormDB, err := gorm.Open("postgres", gormParameters)
	if err != nil {
		panic("failed to connect database")
	}
	helper.GormDB = gormDB

	// Migrate the schema (tables): User, Authentication
	helper.GormDB.AutoMigrate(&helper.User{})
	helper.GormDB.AutoMigrate(&helper.Authentication{})
	helper.GormDB.Model(&helper.Authentication{}).AddForeignKey("user_id", "users(id)", "CASCADE", "CASCADE")

	echoFramework := echo.New()
	echoFramework.Use(middleware.Logger()) // log
	echoFramework.Use(middleware.CORS())   // CORS from Any Origin, Any Method

	echoGroupUseJWT := echoFramework.Group("/api/v1")
echoGroupUseJWT.Use(middleware.JWT([]byte(configuration.EncryptionKey)))
	echoGroupNoJWT := echoFramework.Group("/api/v1")

	// /api/v1/users : logged in users
	echoGroupUseJWT.POST("/users/logout", models.Logout)

	// /api/v1/users : public accessible
	echoGroupNoJWT.POST("/users", models.CreateUser)
	echoGroupNoJWT.POST("/users/login", models.Login)

	defer helper.GormDB.Close()
	echoFramework.Logger.Fatal(echoFramework.Start(":1323"))
}

Второе - создадим globals.go:

package helper

import (
	"regexp"
	"time"
	"github.com/jinzhu/gorm"

)

type Configuration struct {
	EncryptionKey string
	DbHost        string
	DbPort        string
	DbName        string
	DbUsername    string
	DbPassword    string
}

var configuration Configuration
type ModelBase struct {
	ID        int       `gorm:"primary_key"`
	CreatedAt time.Time `json:"-"`
	UpdatedAt time.Time `json:"-"`
}

type User struct {
	ModelBase        // replaces gorm.Model
	Email     string `gorm:"not null; unique"`
	Password  string `gorm:"not null" json:"-"`
	Name      string `gorm:"not null; type:varchar(100)"` // unique_index
}

type Authentication struct {
	ModelBase
	User   User `gorm:"foreignkey:UserID; not null"`
	UserID int
	Token  string `gorm:"type:varchar(200); not null"`
}

type CustomHTTPSuccess struct {
	Data string `json:"data"`
}

type ErrorType struct {
	Code    int    `json:"code"`
	Message string `json:"message"`
}

type CustomHTTPError struct {
	Error ErrorType `json:"error"`
}

var GormDB *gorm.DB
func init() {
	configuration = Configuration{
		EncryptionKey: "F61L8L7CUCGN0NK6336I8TFP9Y2ZOS43",
		DbHost:        "localhost",
		DbPort:        "5432",
		DbName:        "postgres",
		DbUsername:    "postgres",
		DbPassword:    "postgres",
	}
}

func GetConfig() Configuration {
	return configuration
}

func ValidateEmail(email string) (matchedString bool) {
	re := regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
	matchedString = re.MatchString(email)
	return
}

Теперь реализуем метод аутентификации в user.go:

package models

import (
	"app1/helper"
	"fmt"
	"net/http"
	"strconv"
	jwt "github.com/dgrijalva/jwt-go"
	"github.com/labstack/echo"
)

func CreateUser(c echo.Context) error {
	m := echo.Map{}
	if err := c.Bind(&m); err != nil {
		// return err
	}

	name := m["name"].(string)
	email := m["email"].(string)
	password := m["password"].(string)
	confirmPassword := m["confirm_password"].(string)
	if password == "" || confirmPassword == "" || name == "" || email == "" {
		return echo.NewHTTPError(http.StatusBadRequest, "Please enter name, email and password")
	}
	if password != confirmPassword {
		return echo.NewHTTPError(http.StatusBadRequest, "Confirm password is not same to password provided")
	}
	if helper.ValidateEmail(email) == false {
		return echo.NewHTTPError(http.StatusBadRequest, "Please enter valid email")
	}
	if bCheckUserExists(email) == true {
		return echo.NewHTTPError(http.StatusBadRequest, "Email provided already exists")
	}
	configuration := helper.GetConfig()
	enc, _ := helper.EncryptString(password, configuration.EncryptionKey)
	user1 := helper.User{Name: name, Email: email, Password: enc}
  
	// globals.GormDB.NewRecord(user) // => returns `true` as primary key is blank
	helper.GormDB.Create(&user1)
	token := jwt.New(jwt.SigningMethodHS256)
	claims := token.Claims.(jwt.MapClaims)
	claims["name"] = user1.Name
	claims["email"] = user1.Email
	t, err := token.SignedString([]byte(configuration.EncryptionKey)) // "secret" >> EncryptionKey
	if err != nil {
		return err
	}
	authentication := helper.Authentication{}
	if helper.GormDB.First(&authentication, "user_id =?", user1.ID).RecordNotFound() {
		// insert
		helper.GormDB.Create(&helper.Authentication{User: user1, Token: t})
	} else {
		authentication.User = user1
		authentication.Token = t
		helper.GormDB.Save(&authentication)
	}
	return c.JSON(http.StatusOK, map[string]string{
		"token": t,
	})
}
func bCheckUserExists(email string) bool {
	user1 := helper.User{}
	if helper.GormDB.Where(&helper.User{Email: email}).First(&user1).RecordNotFound() {
		return false
	}
	return true
}
func ValidateUser(email, password string, c echo.Context) (bool, error) {
	fmt.Println("validate")
	var user1 helper.User
	if helper.GormDB.First(&user1, "email =?", email).RecordNotFound() {
		return false, nil
	}
	configuration := helper.GetConfig()
	decrypted, _ := helper.DecryptString(user1.Password, configuration.EncryptionKey)
	if password == decrypted {
		return true, nil
	}
	return false, nil
}

func Login(c echo.Context) error {
	m := echo.Map{}
	if err := c.Bind(&m); err != nil {
		// return err
	}
	email := m["email"].(string)
	password := m["password"].(string)
	var user1 helper.User
	if helper.GormDB.First(&user1, "email =?", email).RecordNotFound() {
		_error := helper.CustomHTTPError{
			Error: helper.ErrorType{
				Code:    http.StatusBadRequest,
				Message: "Invalid email & password",
			},
		}
		return c.JSONPretty(http.StatusBadGateway, _error, "  ")
	}
	configuration := helper.GetConfig()
	decrypted, _ := helper.DecryptString(user1.Password, configuration.EncryptionKey)
	if password == decrypted {
		token := jwt.New(jwt.SigningMethodHS256)
		claims := token.Claims.(jwt.MapClaims)
		claims["name"] = user1.Name
		claims["email"] = user1.Email
		claims["id"] = user1.ModelBase.ID
		t, err := token.SignedString([]byte(configuration.EncryptionKey)) // "secret" >> EncryptionKey
		if err != nil {
			return err
		}
		authentication := helper.Authentication{}
		if helper.GormDB.First(&authentication, "user_id =?", user1.ID).RecordNotFound() {
			// insert
			helper.GormDB.Create(&helper.Authentication{User: user1, Token: t})
		} else {
			// update
			authentication.User = user1
			authentication.Token = t
			helper.GormDB.Save(&authentication)
		}
		return c.JSON(http.StatusOK, map[string]string{
			"token": t,
		})
	} else {
		_error := helper.CustomHTTPError{
			Error: helper.ErrorType{
				Code:    http.StatusBadRequest,
				Message: "Invalid email & password",
			},
		}
		return c.JSONPretty(http.StatusBadGateway, _error, "  ")
	}
}
func Logout(c echo.Context) error {
	tokenRequester := c.Get("user").(*jwt.Token)
	claims := tokenRequester.Claims.(jwt.MapClaims)
	fRequesterID := claims["id"].(float64)
	iRequesterID := int(fRequesterID)
	sRequesterID := strconv.Itoa(iRequesterID)
	requester := helper.User{}
	if helper.GormDB.First(&requester, "id =?", sRequesterID).RecordNotFound() {
		return echo.ErrUnauthorized
	}
	authentication := helper.Authentication{}
	if helper.GormDB.First(&authentication, "user_id =?", requester.ModelBase.ID).RecordNotFound() {
		return echo.ErrUnauthorized
	}
	helper.GormDB.Delete(&authentication)
	return c.String(http.StatusAccepted, "")
}

Пропущенный функции шифроки\расшифровки. Напишите мне чтобы их увидеть.

Go — Raspberry Pi GPIO "Привет мир" руководство

Для относительно нового языка, к которым относится Go. Я использовать его с тех годов, по нескольким причинам, так как он полон сюрпризов.

Поэтому недавно, я купил новую pi3 и в основном использовал C для прошлой pi2. Однако, отметим, множество руководств использует питон, а я не большой фанат этого языка. Поэтому я случайным образом поискал библиотеке Go и различные руководства для Pi GPIO, и поигрался чуток с проводульками у меня дома.

В этой статье мы собираемся построить очень простую LED переключалку используя Raspberry Pi и язык Golang.

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

1. Используем GPIO

Мы собираемся создать очень простую мерцающую программу, которую усстановим и запустим на Raspberry PI. Поэтому. перед этим, нужно понять базу, как работать с GPIO.

Самый простой способ считать GPIO интерфейсом, который лежит между системой Pi и внешней системой. IO модуль будет использовать GPIO пины, таким образом, чтобы raspberry могла читать ввод или показывать вывод от девайса или сенсора.

FIg 2. Raspberry Pi 2 GPIO Map

Как показано на рисунке, устройство имеет несколько типов пинов, такие как GPIO, земля, 3.3 вольтовые и 5 вольтовые пины. Для нашей инструкции мы собираемся играться с GPIO 18 пином и землей.

Мы так же можем получить доступ к GPIO информации через термина исопльзуются команду pinout.

2. Собираем LED устройство

Для этой инструкции, мы собираемся подготовить несколько вещей:

  1. USB кабель и зарядник для питания Raspberry PI,
  2. 2 коротких переключателя проводов,
  3. Макет,
  4. LED, and
  5. 330Ω резистор

Я не буду рассказывать глубого процесс установки, так как это легко можно найти в интернете.

Figure 3. Raspberry Pi 3 with LED

3. Разработка Go кода

Наконец, мы собираемся разработать код используя Go. Перво-наперво, мы будем использовать доступный такой пакет в интернете, как:

https://github.com/stianeikeland/go-rpio, and http://gobot.io/documentation/platforms/raspi/

Go-rpio поддерживает операции для GPIO используя Golang, поэтому я верю, что он достаточно подходит для наших целей. В то время как Gobot предоставялет целую SDK библиотеку для доступа к GPIO пинам и специфические драйвера как: LED, мотор, реле, сервопривод и т.д.

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

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

go get github.com/stianeikeland/go-rpio

Это пример кода для создания мерцающего приложения используя Go на raspberry pi:

package main

import (
	"fmt"
	"time"

	"github.com/stianeikeland/go-rpio"
)

func main() {
	fmt.Println("opening gpio")
	err := rpio.Open()
	if err != nil {
		panic(fmt.Sprint("unable to open gpio", err.Error()))
	}

	defer rpio.Close()

	pin := rpio.Pin(18)
	pin.Output()

	for x := 0; x < 20; x++ {
		pin.Toggle()
		time.Sleep(time.Second / 5)
	}
}

Объяснение: Этот код открывает GPIO и устанавливает режим приема на пине 18. Пин будет настроен для вывода(Ввода для чтения, типа сенсора и т.д.). 20 переключит режим пина(Low > High > Low) и установит задержку 200ms. Как только операция выполнится, программа будет завершена.

4. Сборка и загрузка программы.

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

env GOOS=linux GOARCH=arm GOARM=5 go build

Команда выше говорит компилятору. что мы собираем программу для Linux и ARM архитектуру. На данный момент, есть поддержка нескольких версий ARM компилятором Go, но мы ищем версию 5. Затем мы собираемся загрузитьь \ скопировать файл на подключенную raspberry pi. Для этой цели, мы собираемся использовать scp команду.

scp [go binary] pi@192.168.43.208:[remote dir]

5. Запуск программы

Как только бинарник был загружен(для примера, мы загрузили его по пути /home/pi/go/gopio), то чтобы его запустить нужно выполнить команду:

./gopio

Если программа верно собрана, и нет ошибок в коде, LED будет мерцать и остановится после 20 переключений.

Figure 4. Raspberry Pi LED Blink

6. Выводы

Можно использовать Raspberry-Pi для разработки программ внутри Raspberry-Pi. В этой статье, я показал вам простой пример, как создать мерцающий LED используя Raspberry Pi, на языке Golang.

7. Image Link and Resources

Go: Ввдение в сокеты межпроцессного взаимодействия(unixsocket)

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

Всё что вам нужно это только пакет net для запуска подключения:

К сожалению эти фукнции и интерфейсы редко документированны(в частности сокеты межпроцессного взаимодействия), так же нет официального Go блога о том, как с чего начать при работе с сокетами. Похоже что недостаток хорошего ввдения на StackOverflow и Go блогах. Большинство статей о сокетах показывают C реализацию; где я сосредоточусь на том как начать Работать с Go.

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

Вы возможно думаете, что вам нужно создать этот специальный файл используя пакет os. В нем вы можете найти постоянную FileMode, ModeSocket, которая может взывать к вам. Но это не поможет: Незадокументированный функцонионал функции Listen заключается в том, что (это должно) создать файл для вас, и он будет существовать с ошибкой вводящей в заблуждение: “bind: address already in use”, если файл уже существует. Отсюда дла начала, нелогичный шаг в создании сервера это удаление файла который вы собираетесь слушать, и только помто его можно слушать:

    os.Remove(cfg.SocketFile)
    listener, err := net.Listen("unix", cfg.SocketFile)
    if err != nil {
        log.Fatalf("Unable to listen on socket file %s: %s", cfg.SocketFile, err)
    }
    defer listener.Close()

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

    for {
        conn, err := listener.Accept()
        if err != nil {
            log.Fatalf("Error on accept: %s", err)
        }
        go message.HandleConn(conn)
    }

Ваш обработчик должен создать буффер любого желаемого размера, и прочитать в него бесконечный цыкл который остановится когда Read выдаст ошибку. Read едва документирован в net пакете. Странным образом, вы должны знать что дальше нужно смотреть в документацию io пакета для Reader интерфейса:

    type Reader interface {
            Read(p []byte) (n int, err error)
    }

... чтобы узнать важную информацию про поведение Read в других пакетах стандартной библиотеки:

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

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

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

func HandleConn(c net.Conn) {
    received := make([]byte, 0)
    for {
        buf := make([]byte, 512)
        count, err := c.Read(buf)
        received = append(received, buf[:count]...)
        if err != nil {
            ProcessMessage(received)
            if err != io.EOF {
                log.Errorf("Error on read: %s", err)
            }
            break
        }        
    }
}

Этим методом, все данные отправленные подключением воспринимаются сервером как одно сообщение. Клиент должен закрыть подключение сигналом о конце сообщения. Чтение закрытого подключения вренуть ошибку io.EOF, которая не должна быть обработа как обыная ошибка. Это просто сигнал о том, что сообщение закончилось, часто подсказка к началу обработки сообщения так как оно закончено.

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

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

type Sender struct {
    Logger     *log.Logger
    SocketFile string
}

func (s *Sender) SendMessage(message []byte) {
    c, err := net.Dial("unix", s.SocketFile)
    if err != nil {
        s.Logger.Errorf("Failed to dial: %s", err)
    }
    defer c.Close()
    count, err := c.Write(message)
    if err != nil {
        s.Logger.Errorf("Write error: %s", err)
    }
    s.Logger.Infof("Wrote %d bytes", count)
}

Следующим шагом может быть добавление ответа с подтверждением полученного. Для множества приложений, выше приведенная инструкция это всё что нужно для начала связи между GO процессами использующими Unix сокеты.

Go: горутины , потоки ОС и управление ЦПУ

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

M, P, G оркестрация

Чтобы решить эту проблему, Go имеет своё расписание для распределения горутин через различные потоки. Это расписание определяет три главных идеи, как сказано в самом коде:

Главные идеи:

Вот диаграмма этих трех моделей.

Каждая горутина (G) запускается в потоке ОС (M) которая назначена логическому ЦПУ (P). Давайте возьмем простой пример, чтобы посмотреть как Go управляет ими:

func main() {
   var wg sync.WaitGroup
   wg.Add(2)

   go func() {
      println(`hello`)
      wg.Done()
   }()

   go func() {
      println(`world`)
      wg.Done()
   }()

   wg.Wait()
}

Сначала Go создаст различные P с номерами логических ЦПУ присутствующих в машине и сохранит их в качестве списка "холостых" P: P initialization

Затем, новая горутина или горутины готовые к запуску будут будить P для назначения на них работ. В этом случае P создаст M и свяжет их с ОС потоками:

OS thread creation

Однако, как и P, M без работы - то есть не имеющая работающей горутины ожидающей запуска, возвращается из syscall или принудительно завершенная сборщиком мусора, попадет в "холостой" список:

M and P idle list

Во время загрузки программы, Go уже создает потоки ОС и связывает их с M. Для нашего примера, первая горутина которая выводит "привет" будет использовать главную горутину, в то время как вторая получит M и P из "холостого" списка:

M and P pulled from the idle list

Теперь у нас есть общая картина упрвления горутинами и потоками, давайте посмотрим в какомслучае Go станет использовать M чаще чем P и как горутины управляются в случае системых вызовов.

Системные вызовы

Go оптимизирует системные вызовы, вне зависимости от блокировки, с помощью оборачивания их во время исполнения. Эта обертка будет автоматически отделять P от треда M и позволять другим тредам запускать его. Давайте посмотрим на пример чтения файла:

func main() {
   buf := make([]byte, 0, 2)

   fd, _ := os.Open("number.txt")
   fd.Read(buf)
   fd.Close()

   println(string(buf)) // 42
}

Вот рабочий процесс открытия файла:

Syscall handoffs P

P0 в данный момент простаивает, и потенциально доступен. Затем, как только систенмый вызов завершен, Go применяет следующий набор правил пока одно из правил не будет удовлетворено:

Однакой, Go, так же обрабатывает ситуации когда ресурсы еще не готовы, на случай не блокируемых I/O, например http вызовы. Тогда, первый системый вызов, который следуюет представленному выше рабочему процессу, упадет, так как нет готовых ресурсов, заставляет Go использовать сетевой опросник и остановливает горутину. Вот пример:

func main() {
   http.Get(`https://httpstat.us/200`)
}

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

Network poller waiting for the resource

Горутина заново запустится когда Go планировщик начнет искать работу. Планировщик затем будет передавать сетевому опроснику, что горутина ожидает запуска в случае успешного получения информации которая ожидается:

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

Ограничения в рамках потоков ОС

Когда системные вызовы использутся, Go не ограничивает количество потоков ОС, которые могут быть заблокированны, как указано в коде:

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

Вот пример ситуации:

func main() {
   var wg sync.WaitGroup

   for i := 0;i < 100 ;i++  {
      wg.Add(1)

      go func() {
         http.Get(`https://httpstat.us/200?sleep=10000`)

         wg.Done()
      }()
   }

   wg.Wait()
}

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

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

Raspberry Pi и программирование на Go

Go или GoLang - компилируемый язык программирования разработанный Google.

Go широко используется для web разработки и на сегодня это самый быстро растущий язык. В отличии от Java который запускается в JVM, Go собирается прямо в Windows, OS X or Linux исполняемые файлы.

В этой статье мы взглянем на создание двух програм, которые разговаривают с Raspberry Pi GPIO. Первая будет просто клавиатурный ввод программы, а вторая будет отдельным web приложением управляюищим пинами GPIO.

Raspberry Pi GPIO

Есть можество различных способов производить подключение к GPIO. Для нашего примера мы собираемся посмотреть на вывод gpio терминальной утилиты, так же будем использовать go-rpi библиотеку.

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

Аппаратное обеспечение Raspberry PI настраивается используя резистор на физическом пине 7(BCM пин 4)

Led_setu

Наша первая программа(keyin.go) будет читать ввод клавиатуры и затем направлять в gpio дважды, первый раз, чтобы записать значение, второй раз чтобы прочитать значение обратно.

package main

import (
    "bufio"
    "fmt"
    "os/exec"
    "os"
)

func main() {
    // Get keyboard input
    reader := bufio.NewReader(os.Stdin)
    fmt.Print("Enter value for GPIO pin 7 : ")
    pinval, _ := reader.ReadString('\n')

    // Write to GPIO pin 7 using keyboard input 
    testCmd := exec.Command("gpio","write", "7", pinval)
    testOut, err := testCmd.Output()
    if err != nil {
        println(err)
    }
    // Read back the GPIO pin 7 status
    testCmd = exec.Command("gpio","read", "7")
    testOut, err = testCmd.Output()
    if err != nil {
        println(err)
    } else {
      fmt.Print("GPIO Pin 4 value : ")
      fmt.Println(string(testOut))
    }
}

Скомпилируем и запустим keyin.go программу:

 $ go build keyin.go
 $ ./keyin
Enter value for GPIO pin 7 : 1
GPIO Pin 4 value : 1

Простое Go веб-приложение

Для начального примера мы сделаем веб-приложение(web_static.go) которое показывает страничку web_static.html.

web_static.html выглядит следующим образом:

<html>
  <head>
    <title>GO PI Static Page</title>
  </head>
  <body>
    <h1>GO PI Static Page</h1>
    <hr>
    This is a static test page
  </body>
</html>

web_static.go программе необходимо импортировать net/http библиотеку. http.HandleFunc вызов используется для стандартного адреса “/” и раздачи с помощью сервера нашего web_static.html файла. http.ListenAndServe фукнция слушает web запросы на порту 8081.

package main
 
import (
    "log"
    "net/http"
)
 
func main() {
    // Create default web handler, and call a starting web page
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "web_static.html")
        println("Default Web Page")
    })
    // start a listening on port 8081
    log.Fatal(http.ListenAndServe("8081", nil))
 
}

Go код может быть скомпилирован и запущен.

$ go build web_static.go
$ ./web_static
Default Web Page

Из web бразуера указывающего на Raspberry Pi и порт 8081,наше приложение будет выглядит так:

web_static

Go Web приложение с Pi GPIO

Следующий шаг - создание web страницы которая может передавать параметры. Для этогоп риложения мы переключим GPIO вывод с on на off.

Новая web страница(go_buttons.html) создается с двумя кнопками. Html якорь используется для передачи /on и /off параметров в наше web приложение.

CACHE-CONTROL тег устанавливается в NO-CACHE для того, чтобы мы точно знали, что страница перезагружается. Я так же включил EXPIRES тег равным нулю, чтобы браузер всегда видел страничку "испорченной". Если вы не включаете их то страница может быт загружена только один раз.

<html>
  <head>
    <title>GO PI GPIO</title>
    <META HTTP-EQUIV="CACHE-CONTROL" CONTENT="NO-CACHE">
    <META HTTP-EQUIV="EXPIRES" CONTENT="0">
  </head>
  <body>
    <h1>Go Raspberry Pi GPIO Test</h1>
    <a href="/on"><button>Set LED ON</button></a><br>
    <a href="/off"><button>Set LED OFF</button></a>
  </body>
</html>

Наше новое web приложение(go_buttons.go) теперь включает еще две http.HandleFunc функции обработчика, одну для /on и одну для /off. Эти обработчики вызывают функцию которая вызывает gpio, что используется для написания вывода и чтения обратно статуса ввода.

Наша новоиспеченная gpio функция выполняет ходит 2 раза к gpio командной утилите, первый раз записать значение, второй - прочитать значение обратно.

package main

import (
	"log"
	"net/http"
	"os/exec"
)
func gpio( pinval string) {
    testCmd := exec.Command("gpio","write", "7", pinval)  
    testOut, err := testCmd.Output()      
    if err != nil {
        println(err)
    }
    testCmd = exec.Command("gpio","read", "7") 
    testOut, err = testCmd.Output()
    if err != nil {
        println(err)
    } else { 
      print("GPIO Pin 4 value : ")  
      println(string(testOut))
    }
}

func main() {

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFile(w, r, "go_buttons.html")
		println("Default Web Page")
	})

	http.HandleFunc("/on", func(w http.ResponseWriter, r *http.Request) {
                 http.ServeFile(w, r, "go_buttons.html")
		 println("Web Page sent : ON")
		 gpio("1")

	})

	http.HandleFunc("/off", func(w http.ResponseWriter, r *http.Request) {
                 http.ServeFile(w, r, "go_buttons.html")
		 println("Web Page sent : OFF")
		 gpio("0")
	})

	log.Fatal(http.ListenAndServe(":8081", nil))

}

Скомпилируем и запустим go_buttons:

$ go build go_buttons.go
$ ./go_buttons


Default Web Page
Web Page sent : ON
GPIO Pin 4 value : 1

Default Web Page
Web Page sent : OFF
GPIO Pin 4 value : 0

The web page should look something like:

Выводы

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