Go lang
- Доступ к K8S CRD из go-клиента
- Как мокать? Go способ.
- Злоупотребление синтаксисом Go для создания DSL(предметно-ориентированного языка)
- Первые шаги на Go — Построение простого веб приложения с Neo4j
- Пишем REST API клиента на Go
- Пример использования Jenkins REST API на Golang
- Простая инструкция изучения Цепочи обязаностей в Golang
- Реализация RSA шифрования и подписи на Golang
- Строим свой первый Rest API с помощью GO
- Echo framework + GORM = Огненно быстрое Golang приложение на стороне сервера. Пример аутентификации.
- Go — Raspberry Pi GPIO "Привет мир" руководство
- Go: Ввдение в сокеты межпроцессного взаимодействия(unixsocket)
- Go: горутины , потоки ОС и управление ЦПУ
- Raspberry Pi и программирование на Go
Доступ к 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 компонента:
- "Наш сервис" который у нас есть и мы создаем
- "Сторонние сервисы и библиотеки" которые взаимодействуют с базой данных и мы используем для работоспособности нашего сервиса. Теперь так как мы строим "Наш сервис", мы хотим написать независимую единицу для "Нашего сервиса" но так как мы используем функционал стороннего сервиса или библиотеки в нашем сервисе, если мы тестируем без моков, мы будет производить интеграционное тестирование, что иногда сложно и более временно затратно. Для демонстрации, мы напишем простую библиотеку которая проверяет существует ли пользователь внутри map. Наш сервис будет нужен для хранения бизнес логики, и это станет сторонней библиотекой внутри нашей стистемы.
Код сторонней библиотеки
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 и для того чтобы сделать его более мощным.
- Реализовать дополнительный
ReqeustFilters
как ограничитель частоты, возможно использовать golang.org/x/time/rate или juju/ratelimit, или более сложный механизм аутентификации - Реазиловать другой промежуточный слой
- Изменить структуру эндпоинта чтобы включить полу
Deny
для типаFilters
, это будет отвергать запрос если одно из полейRequestFiltesr
-true
- Каждый эндпоинт в конечном примере включает логирование в промежуточном слое, добавьте в DSL средство для применения набора общих ограничений или промежуточный слой для всех энпоинтов.
- Создать способ для стека промежуточного слоя, чтобы создать
context.Context
и работать с обработчиками которые их принимают.
Резюмируем, мы использовать типы для создания абстрактнций поверх наборов простых типов и функций отдельных подписей, и мы берем преимущество Go свойств синтаксиса такие как вариативные функции и приведение типов для написания спокойного и ненагроможденного синтаксиса. Тяжелый подъем в создании DSL был произведен с помощью функций высшего порядка который позволили параметризовать поведения объединеные и настроены во время работы. Мы использовали несколько опасных практик написания кода, но чем больше мы применяем их только когда сокращаем сложность для конечного пользователя, тем крепче мы можем спать ночью.
Go, который вы получаете из коробки ориентирован на детали, минималистичный, и может быть довольно подробным. Go дает инструменты, однако, чтобы построить вашу собственную абстракцию, ваш собственный высокоуровневый язык для написания кода который содержательный, элегантный, и выразительный как и любой другой который вы находите в динамичном или чистом функциональном языке, но это дает нам доступ ко всем свойствам который мы любим в Go.
Первые шаги на Go — Построение простого веб приложения с Neo4j
Цель:
Цель этого поста создать простое веб приложение которое снимает структуру вашего сайта получая все ссылки которые содержит и сохраняет их в neo4j базу данных. Идея проста - и в ней следующие шаги:
- Делаем запрос на URL
- парсим ответ
- Извлекаем ссылки из ответа
- Сохраняем извлеченные ссылки в neo4j
- Повторяем 1 шаг с полученными ссылками пока не исследуем весь сайт
- Наконец используем 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 чтобы понять главные его вещи, такие как:
- Что такое "Base url", и можно ли его поменять?
- Поддерживает ли он версионирование?
- Какие ошибки можно встретить?
- Каким образом аутентифицируются клиенты?
Ответы на эти вопросы помогут вам создать правильную структуру.
Начнем с основ. Создадим репозиторй, выберем название, в идеаеле одно должно совпадать с именем 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 файла:
- packages/helpers/helpers.go
- packages/jenkins/jenkins.go
- 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)?
- Поведенческий шаблон проектирования который позволяет вам передавать запрос через цепочку обработчиков.
- При получении запроса, каждый обработчик решает сам обрабатывать запрос или передать его дальше по цепочке.
Проблема
Представим, вы рабоаете над приложениме онлаин курсов. Вот список проверок перед тем как можно использовать курс.
- Один из ваших коллег, Джимм, предположил, что это не безопасно передавать чистые данные в приложении. Поэтому вы добавли дополнительную проверку шага для обработки данных в запросе.
- One of your colleagues, Tommy, noticed that the system is vulnerable to brute force password cracking. To negate this, you promptly added a check that filters repeated failed requests coming from the same IP address.
- One of your colleagues, Daniel, suggested that you could speed up the system by returning cached results on repeated requests containing the same data. Hence, you added another check which lets the request pass through to the system only if there’s no suitable cached response.
Having a big headache
- The code of the checks, which had already looked like a mess, became more and more bloated as you added each new feature
- Changing one check sometimes affected the others
- Worst of all, when you tried to reuse the checks to protect other components of the system, you had to duplicate some of the code since those components required some of the checks, but not all of them.
Solution
- CoR relies on transforming particular behaviors into stand-alone objects called handlers
- The pattern suggests that you link these handlers into a chain
- Each linked handler has a field for storing a reference to the next handler in the chain. In addition to processing a request, handlers pass the request further along the chain
- The request travels along the chain until all handlers have had a chance to process it.
Diagram
Handler
- declares the interface common to all concrete handlers.
- usually contains just 1 method for handling requests and another method to set the next handler in the chain
Base handler
- optional class to put the boilerplate code that’s common to all handler class
- this class defines a field for storing a reference to the next handler
- the main class build a chain by passing a handler to the constructor or setter of the previous handler
Main
- compose chains just once or compose them dynamically depending on the app logic
Concrete Handlers
- contain actual logic for processing requests
- upon receiving a request, each handler decide whether to process it and whether to pass it along the chain
- usually self contained and immutable, accepting all necessary data just one via the constructor
Pros and Cons Pros
- control the order of request handling
- Fulfills Single Responsibility Princple
- Fulfills Open/Closed Principle
Cons
- some request may end up unhandled
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.
Here the link to my github page for all source code on chain of responsibility design pattern in Golang: https://github.com/leonardyeoxl/go-patterns/tree/master/behavioral/chain_of_responsibility
Реализация RSA шифрования и подписи на Golang
Эта статья описывает как работает RSA алгоритм, и как его можно реалоизоват на Go.
RSA (Rivest–Shamir–Adleman) шифрование наиболее широко распространнено для безопасного шифрования данных.
Это асиметрический алгорит шифрования, который по простому можно назвать "в одну сторону". В данном случае, легко для кого угодно зашифровать кусочек данных, но расшифровать его может только тот у кого есть настоящий ключ.
RSA Шифрование изнутри
RSA работает генерируя публичный и приватный ключ. public
и private
ключи создаются одновременно и формируют пару ключей.
Публичный ключ может быть использовать для шифрования произвольной части кода, но не может его расшифровать.
Приватный ключ может быть использовать для расшифровки любой части данных которая была зашифрованна соотвесттвующим ключем.
Это значит, что мы даем наш публичный ключ, кому угодно. Они могут затем зашифровать любую информацию, которую они хотят нам послать, и единственный способ получить к ней доступ можно только с помощью приватного ключа.
Генерация ключа
Первое, что мы хотим сделать, это создать пару ключей: 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 для шифрования случайного сообщения. Мы должны предоставить этому методу:
- Хэширующую функцию, выбранную таким образом, что даже если ввод изменится едва ли, вывод должен менятся совершенно. Алгоритм SHA256 подходит для этого.
- Случайный reader используемый для создания случаный битов, таким образом, чтобы каждый одинаковый ввод будет отдавать тот же вывод.
- Публичный ключ ранее сгенерированный.
- Сообщение которое мы шифруем.
- Дополнительные параметры, которые мы опустим в этот раз.
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
который мы будем использовать для получения оригинальной информации обратно из зашифрованной.
Данные необходимые для расшифровки:
- Зашифрованные данные(называемые cipher text)
- Хэш который мы использовать для шафрования данных
// Первый аргумент это генератор случайных данных( 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
сигнатурой.
У кого есть сигнатура, сообщение и публичный ключ, могут использвать RSA для проверки, того, что сообщение действительно пришло от того, чей публичный ключ используется. Если данные или подпись не совпадают, проверка завершается неудачей.
Только часть с приватным ключем может подписать сообщение, но кто угодно с публичным ключем могут проверить его.
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 публичный и приватные ключи и как использовать их для шифрования, подписи и проверки данных.
Есть ограничение, о которых стоит знать прежде чем использовать алгоритм.
- Данные, которые вы пытаетесь зашифровать должны быть гораздо короче чем сила вашего ключа. Для примера: EncryptOAEP документация говорит, что сообщение не должно быть больше, чем длинна публичного модуля минус двойная длинна хэша, и дальше минус 2.
- Используемый алгоритм хэширования должен быть подобран к случаю. SHA256(который мы используем) является достаточным для большинства случаем, но вы можете захотет использовать что-то по серьезнее типа SHA512 для критически важных данных приложения.
Строим свой первый Rest API с помощью GO
У нас есть три необходимые части для построения.
- API
- Rest API
- 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
- Клиент-сервер - Разделяя проблемы пользовательских интерфейсов от проблем хранения данных, вы улучшаем переносимость пользовательского интерфейса на многие платформы и улучшаем масштабируемость упрощением сервреных компонентов.
- Не хранит состояние - каждый запрос от клиента к серверу должен содержать всю необходимую информацию для понимания запросов, и не может воспользоваться любых сохраненным содеражением на сервере. Состояние сессии отсюда хранится полностью на клиенте.
- Кешируемость - Ограничения кэша требуют. чтобы данные в ответе на запрос были явно или не явно обозначены как кэшируемые или не кешуруемые. Если ответ кэшируемый, тогда право кэша клиента для переиспользования данные ответа в дальнейшем, похожем запросе.
- Универсальный интерфейс - Применя принцыпи програмной инженерии в целом к компонентам интерфейса, общая архитектура системы упрощена и видимость взаимодействий улучшена. Для того, чтобы получить универсальный интерфейс, множество универсальных ограничений нужны для указания поведения компонентов. REST определяется четырьмя ограничивающими интерфейсами:
- Определение ресурса
- Упралвение ресурсами через репрезентацию
- Самоописываемое сообщение
- Гипермедия - как движок состояния приложения.
- Слоёная система - Стиль слоёной системы позволяет архитектуре быть составленной из иерархических слоёв ограниченными поведением компонентов, так, что каждый компоненто не может "смотреть сковозь" слой с которым происходит взаимодействие.
- Код по запросу(по желанию) - 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.
- Ресурс - объект или представление чего-то, что имеет некоторую ассоциацию данных с ним и может иметь набор методов для обработки оных. К примеру: Живтоные, школы или работники ресурсы, а
delete
,add
,update
- операции для обработки этих данных. - Коллекции - наборы ресурсов: Компании это наборы ресурсов компаний.
- URL - это путь по которому ресурс может быть определен и действия которые необходимо с ним произвести.
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 типа заголовков.
- Заголовок запроса (client -> server)
- Заголовок ответа (server -> client)
Тело
Вы можете добавить дополнительную инфомацию в обза запроса к серверу и к ответу от сервера.
Тип ответа
Обычно JSON или XML.
В наши дни это обычно JSON.
Rest API на GO
Это то почему вы тут. Ну или я, по крайней, мере я надеюсь на это.
Если вы пишите REST API, почему вы должны выбрать Go?
- Он компилируем. Вы получаете маленькие бинарники.
- Он быстр. Медленнее чем c/c++ или rust, но быстрее чем большинство других языков web программирования.
- Он легок в изучении.
- Он работает отлично в мире микросервисов - это причини номер 1.
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
пакет.
Дальше идут иморты:
-
log
для логирования ошибок, если случаются. -
net/http
потому что мы пишем rest api.
Далее идет структура под названием 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.
Файл можно найти выше.
Склонируем репозиторий
В отдельной папке.
git clone https://github.com/moficodes/bookdata-api.git
Пройдемся по коду
Есть ва пакета внутри кода. Один называется datastore
, другой - loader
.
-
lader
- конвертирует csv данные в массив объектов с данными о книге. -
datastore
работает с доступом к масиву. Обычно это интерфейс который имеет метод.
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 могла читать ввод или показывать вывод от девайса или сенсора.
Как показано на рисунке, устройство имеет несколько типов пинов, такие как GPIO, земля, 3.3 вольтовые и 5 вольтовые пины. Для нашей инструкции мы собираемся играться с GPIO 18 пином и землей.
Мы так же можем получить доступ к GPIO информации через термина исопльзуются команду pinout
.
2. Собираем LED устройство
Для этой инструкции, мы собираемся подготовить несколько вещей:
- USB кабель и зарядник для питания Raspberry PI,
- 2 коротких переключателя проводов,
- Макет,
- LED, and
- 330Ω резистор
Я не буду рассказывать глубого процесс установки, так как это легко можно найти в интернете.
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 переключений.
6. Выводы
Можно использовать Raspberry-Pi для разработки программ внутри Raspberry-Pi. В этой статье, я показал вам простой пример, как создать мерцающий LED используя Raspberry Pi, на языке Golang.
7. Image Link and Resources
- turning-on-an-led-with-your-raspberry
- cross-compiling-golang-applications-raspberry-pi
- go-rpio
- gobot
- Raspberry Pi icon in Color Style
Go: Ввдение в сокеты межпроцессного взаимодействия(unixsocket)
Сокеты межпроцессного взаимодействия предлагают эффективное,безопасное двухсторонее подключение между процессами на Unix/Linux машинах. В то время как каналы отлично используются для подключения между горутинами приложения, и HTTP вездесущь, при подключении между Go приложениями(межпроцессороное взаимодействие) запущенные на той же машине каналы не помогают, а подключление к сокету межпроцессного взаимодействия гораздо проще, эффективнее, и более безопасно чем HTTP или другие интернет протоколы подключений.
Всё что вам нужно это только пакет net
для запуска подключения:
- Пакет
net
предоставляет перносимый интерфейс для сети I/O, включая Unix сокеты. - Так же пакет предоставляет доступ к низкоуровневым примитивам сети, большинству клиентов требуедсят только базовый интерфейс п редоставляемый
Dial
,Listen
иAccept
функциями и связанныеConn
иListener
интферфейсы.
К сожалению эти фукнции и интерфейсы редко документированны(в частности сокеты межпроцессного взаимодействия), так же нет официального 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
в других пакетах стандартной библиотеки:
-
Reader
- интерфейс который оборачивает базовый методRead
-
Read
читаетlen(p)
байтов вp
. Он возвращяет количество прочитанных байтов(0<= n <= len(p)) в том числе любую ошибку. Даже еслиRead
вернет n < len(p), он может использовать всё изp
как испорченное пространство в время вызова. Если какие-то данные доступны, но длинной не равнойlen(p)
байт,Read
вернеть то что доступно, вместо того, чтобы чего-то ждать. - Когда
Read
встречает ошибку илиEOF
условние после успешного чтения n > 0 байтов, он возвращает количество прочитанных байтов. Может вернутьnon-nil
ошибку из того же вызова или вернуть ошибкуn == 0
из подпоследовательности вызовов. Экземпляр данного конкретного случая это то чтоReader
возвращаетnon-zero
число байтов в конце потока ввода, может так же вернуть илиerr == EOF
илиerr == nil
. СледующийRead
должен вернуть0
,EOF
. - Функции должны всегда обрабатывать
n>0
байтов возвращенные перед учитываением ошибкиerr
. Выполнение этого условия обрабатываетI/O
ошибки, которые случаются после чтения неких байтов и так же обоим позволеноEOF
поведение. - Реализация Read обескураживает от возвращения
0
байтов сnil
ошибкой, за исключением когдаlen(p) == 0
. Функция должна отнестись к возврату0
илиnil
, как к индикатору что ничег не случилось, в частности это не говорит обEOF
(конце файла) - Реализация не должна удерживать
p
:
Как при загадочном старте, буфер который вы передаете 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 кода
M должен иметь связанный P, чтобы выполнять код 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 для назначения на них работ. В этом случае P создаст M и свяжет их с ОС потоками:
Однако, как и P, M без работы - то есть не имеющая работающей горутины ожидающей запуска, возвращается из syscall
или принудительно завершенная сборщиком мусора, попадет в "холостой" список:
Во время загрузки программы, Go уже создает потоки ОС и связывает их с M. Для нашего примера, первая горутина которая выводит "привет" будет использовать главную горутину, в то время как вторая получит M и P из "холостого" списка:
Теперь у нас есть общая картина упрвления горутинами и потоками, давайте посмотрим в какомслучае 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
}
Вот рабочий процесс открытия файла:
P0 в данный момент простаивает, и потенциально доступен. Затем, как только систенмый вызов завершен, Go применяет следующий набор правил пока одно из правил не будет удовлетворено:
- Попытатся завладеть тем же P, в нашем случае это P0, и вернутся к выполнению.
- Попытаться получить P из списка "холостых" и вернуться к выполнению
- Поместить горутину в общую очередь, а связанный с ним M вернуть обратно в "холостую" очередь.
Однакой, Go, так же обрабатывает ситуации когда ресурсы еще не готовы, на случай не блокируемых I/O, например http вызовы. Тогда, первый системый вызов, который следуюет представленному выше рабочему процессу, упадет, так как нет готовых ресурсов, заставляет Go использовать сетевой опросник и остановливает горутину. Вот пример:
func main() {
http.Get(`https://httpstat.us/200`)
}
Как первый системный вызов отработает и явно скажет, что ресуср не готов, горутина остановится до тех пор пока сетевой опросник не скажет, что ресурс готов. В этом случае тред M будет разблокирован:
Горутина заново запустится когда 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)
Наша первая программа(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,наше приложение будет выглядит так:
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, но для прототипирования я нашел, что командная утилита проще в решении проблем.