Golang
- ldap-proxy
- Golang
- Инструкция для линтинга Go программ
- I18n in Go: Managing Translations
- Машинное обучение Golang
- Raspberry Pi Coding in Go: Traffic Lights
- Building a WebRTC video and audio Broadcaster in Golang using ION-SFU, and media devices
- 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
- GTK3 app
- Тестикулировать
- docker push без docker push(пример)
- Docker pull без docker
ldap-proxy
Getting Started With LDAP in Go
Недавно пришлось писать доброе количество кода на Go, который взаимодействует с AD, для одного моего клиента. AD использует легковесный протокол доступа (LDAP) для клиент серверного подключения. LDAP - старый и могущественный протокол для взаимодействия с сервисов, несмотря на это, многие мои друзья спорят, что это реливкия прошлого на данный момент. Я с ними не согласен, но мое объяснение может занять целый отдельный пост.
Два три года назад я столкнулся с LDAP вызовом. Пришлось писать некоторый Go код, который должен использовать LDAP для групп и некоторых других авторизационных вещей. В то время библиотеки были в плачевном состоянии. К сожалению, это не было выходом в то время, но к счатью, я быстро изучил работу в Go с LDAP, что поменялось к лучшему.
Эта статья предоставляет базовое введение в удивительный модуль go-ldap. Что-то вроде ввдения, о котором я мечта, когда я начал работать над LDAP проектом для моего клиента. Кроме того, я хотел иметь некую ссылку на инструкцию к которой я смог бы вернуться в будущем если понадобится. Я надеюсь вы не только найдете этот пост полезным, но так же научитесь чему-то новому. Давайте начнем.
Подключение
Прежде чем мы сможем сделать что-нибудь с AD нужно подключиться к серверу. Давайте, для начала, подключимся к серверу AD с помощью LDAP, мне кажется это естественно и правильно с точки зрения смысла. Еще Go модули описанные в этом посту называдются ldap-go, давайте разберемся с LDAP. Модуль go-ldap предоставляет несколько возможностей для подключения к LDAP серверу. Пройдеся по ним подробнее.
Все типи LDAP подключения обрабатыватся DialURL
функцией. Есть несколько других функции доступных в модуле, но документация предполагает что DialURL
останется единственно рабочей. Как предписывает название функции, вы передаете ей URL и функция пытается подключиться к удаленному LDAP серверу и вернуть подключение если всё прошло успешно.
Пример кода можно найти ниже:
ldapURL := "ldaps://ldap.example.com:636"
l, err := ldap.DialURL(ldapURL)
if err != nil {
log.Fatal(err)
}
defer l.Close()
Этот код пытается установить TLS подключение к удаленному серверу. DialURL выводит тип подключения из URL который был передан функции, который в этом случае является ldaps(с s - безопасный).
Если вам нужны подробности TLS конфигурации, функция принимает самоподписанные TLS конфиги через допольнительный параметр:
ldapURL := "ldaps://ldap.example.com:636"
l, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))
if err != nil {
log.Fatal(err)
}
defer l.Close()
Пример кода выше дан только в качестве демонстрации! Ни когда не пропускайте проверку TLS когда создаете её.
Если вы не хотите использовать TLS, можно просто опустить "s" в URL адресе:
ldapURL := "ldap://ldap.example.com:389"
l, err := ldap.DialURL(ldapURL)
if err != nil {
log.Fatal(err)
}
defer l.Close()
Вы можете так же опустить порт из адреса. Код выше показывает его для краткости. Если вы опустите порт DialURL
функция автоматически подставит порт 639 для ldaps или 389 для ldap подключений. По умолчанию LDAP порт так же доступен через глобальные переменные DefaultLdapsPort и DefaultLdapPort.
Как вариант, вы можете использовать NewConn(conn net.Conn, isTLS bool)
функцию которая позволяет вам использовать чистое net.Conn
подключение, которое вам может понадобиться, в том или ином случае.
Наконец, вы можете так же обновить существующее подключение до TLS используя функцию StartTLS()
:
l, err := DialURL("ldap://ldap.example.com:389")
if err != nil {
log.Fatal(err)
}
defer l.Close()
// Now reconnect with TLS
err = l.StartTLS(&tls.Config{InsecureSkipVerify: true})
if err != nil {
log.Fatal(err)
}
Теперь зная как подключиться к LDAP серверу мы можем перейти к следующему шагу.
Связывание
Связывание это шаг, где LDAP сервере уже аутентифицирует клиента. Если клиент успешно прошол аутентификацию, сервер предоставляет ему доступ основываясь на его привилегиях.
Есть несколько путей для создания LDAP связываний используя ldap-go. Начнем с простого случая: неаутентифицируемое связываение.
Иногда LDAP сервер дает ограниченный доступ только для чтения для неаутентифицируемых клиентов. Выпонить его можно следующим образом:
// Подключяемя к серверу как делали выше
err = l.UnauthenticatedBind("cn=read-only-admin,dc=example,dc=com")
if err != nil {
log.Fatal(err)
}
Если вам, всё равно нужно аутентифицироваться, в вашем распоряжении есть 2 варианта:
SimpleBind
и Bind
. Последнее - это хорошая обертка вокруг первого, поэтому я люблю использовать её.
// Подключяемя к серверу как делали выше
err = l.Bind("cn=read-only-admin,dc=example,dc=com", "p4ssw0rd")
if err != nil {
log.Fatal(err)
}
Наконец вы можете сделать External
свзяывание которое согласно официальному заявлению позволяет клиентам запрашивать у сервера использование доступов созданных во вне по отношению к механизму клиента.
Это воплощает в реальность, то что клиент связывается в UNIX сокет(используется ldapi://) и происходит SASL/TLS аутентификация "непрямо" через UNIX сокет.
Я никогда не использовать эту форму аутентификация, поэтому я не могу что-то про него рассказать, но я думаю это полезно, в качестве sidecar
когда вы подключаетесь своим сайдкаром к процессу через UNIX socket в котором ваш процесс сайдкара обрабатывает LDAP аутентификацию(через коммуникацию) от вашего имени.
LDAP CRUD
Теперь, то что мы подключились и аутентифицировались мы можем навредить. Если используемый аккаунт имеет верные доступы, вы можете начать добавлять, изменять, искать и удалять LDAP записи. Давайте посмотрим, на каждую из них в отдельности.
В общем, мы будем работать с тремя базовыми записями: groups, users и машины.
Добавление и изменение
Вы можете создать новую LDAP запись используя Add
функцию. Она принимает простой параметр AddRequest
. Вы можете собрать AddRequest
вручную(структура AddRequest
экспортируется вместе со всеми своими полями) или вы можете использовать простую функцию помощника внутри библиотеку. Мы рассмотрим оба этих случая.
Я решил сгруппировать оба примера добавление и изменение, так как они связаны очень тесно, о чем я не догадывался, а вы увидите дальше.
Добавление групп
Добавление групп в AD заставило попотеть меня, чтобы выяснить, но после прочтения различной AD страниц документации я закончил с чем-то таким:
// Тут идет код подключения
addReq := ldap.NewAddRequest("CN=testgroup,ou=Groups,dc=example,dc=com", []ldap.Control{})
var attrs []ldap.Attribute
attr := ldap.Attribute{
Type: "objectClass",
Vals: []string{"top", "group"},
}
attrs = append(attrs, attr)
attr = ldap.Attribute{
Type: "name",
Vals: []string{"testgroup"},
}
attrs = append(attrs, attr)
attr = ldap.Attribute{
Type: "sAMAccountName",
Vals: []string{"testgroup"},
}
attrs = append(attrs, attr)
// Делаем группу доступной для изменения
// https://docs.microsoft.com/en-us/windows/win32/adschema/a-instancetype
instanceType := 0x00000004
attr = ldap.Attribute{
Type: "instanceType",
Vals: []string{fmt.Sprintf("%d", instanceType},
}
attrs = append(attrs, attr)
// делаем группе домен local и то что это будет группа безопасности
// https://docs.microsoft.com/en-us/windows/win32/adschema/a-grouptype
groupType := 0x00000004 | 0x80000000
attr = ldap.Attribute{
Type: "groupType",
Vals: []string{fmt.Sprintf("%d", groupType)},
}
attrs = append(attrs, attr)
addReq.Attributes = attrs
if err := l.AddRequest(addReq); err != nil {
log.Fatal("error adding group:", addReq, err)
}
Теперь этот код выглядит более понятно. Есть более краткий метод делать тоже самое, но я хотел показать код выше для краткости, так как это был мой первоначальный код.
Сопособ пол учше, который делает тоже самое:
// Тут идет код подключения
addReq := ldp.NewAddRequest("CN=testgroup,ou=Groups,dc=example,dc=com", []ldp.Control{})
addReq.Attribute("objectClass", []string{"top", "group"})
addReq.Attribute("name", []string{"testgroup"})
addReq.Attribute("sAMAccountName", []string{"testgroup"})
addReq.Attribute("instanceType", []string{fmt.Sprintf("%d", 0x00000004})
addReq.Attribute("groupType", []string{fmt.Sprintf("%d", 0x00000004 | 0x80000000)})
if err := l.AddRequest(addReq); err != nil {
log.Fatal("error adding group:", addReq, err)
}
Выделим пару вещей. Первое, вам нужно быть уверенными в том, что атрибуты objectClass
правильного типа(top
и group
).
Второе, instanceType
hex-число выглядит пугающим, но это именно, то что ждет AD если вы хотите создать "writable" то есть изменяемую группу записи.
Наконец, атрибут groupType
выглдяит даже безумнее! Выходит. что если вы хотите, чтобы группа имела local домен масштаб хотелок так же и это так же группа безопасности(как противовес распределенной группой) вам нужно будет делать побитовые операции для флагов описаны в AD документации.
Здесь и сейчас, начинаем. Вы можете проверить, что группа создана используя знакомую ldap команду.
ldapsearch -LLL -o ldif-wrap=no -b "OU=testgroup,OU=Group,dc=example,dc=com" \
-D "${LDAP_USERNAME_DN}" -w "${LDAP_BIND_PASSWD}" -h "${LDAP_HOST}" \
'(CN=testgroup)' cn
Добавление юзера
Добавление пользователя потребует больше при разработке, и меня осинило, потому что не было достаточно очевидно, как это делать. Лучший способ научиться этому делать конкретные примеры.
Представим, вы хотите создать нового пользователя LDAP и назначить ему пароль. Давайте скажем еще вы не хотите, чтобы пароль имел срок действия. Что я обычно думаю в таком случае это хитростью сделать всё в один простой запрос AddRequest
похожим образом что был ранее.
Я думаю я найду правильные LDAP атрибуты, собрав их в AddRequest
и эта работа будет выполнена. Я был ужасно не прав и на это потребовалось некоторое время что бы это понять.
Получается ключ - это раздеть весь процесс на три шага:
- Создать отключенный аккаунт
- Установить пароль для него
- Включить аккаунт
Зная это, результирующий код для первого шага будет довольно прост:
// Тут идет подключение
addReq = ldp.NewAddRequest("CN=fooUser,OU=Users,dc=example,dc=com", []ldp.Control{})
addReq.Attribute("objectClass", []string{"top", "organizationalPerson", "user", "person"})
addReq.Attribute("name", []string{"fooUser"})
addReq.Attribute("sAMAccountName", []string{"fooUser"})
addReq.Attribute("userAccountControl", []string{fmt.Sprintf("%d", 0x0202})
addReq.Attribute("instanceType", []string{fmt.Sprintf("%d", 0x00000004})
addReq.Attribute("userPrincipalName", []string{"fooUser@example.com"})
addReq.Attribute("accountExpires", []string{fmt.Sprintf("%d", 0x00000000})
addReq.Attributes = attrs
if err := l.AddRequest(addReq); err != nil {
log.Fatal("error adding service:", addReq, err)
}
Теперь аккаунт который был создан можно перенести в второй шаг: настроить пароль пользователю.
Сервер AD хранит пароль в виде кодировки little-endian UTF16 в base64. К счастью, для меня linux предоставляет несколько удобных утилит которая могут сделать это за нас. Чтобы создать новый пароль правильного формата.
echo -n "\"password\"" | iconv -f UTF8 -t UTF16LE | base64 -w 0
Теперь вы создали праоль для нового пользователя, время добавить его в LDAP сервер. Это можно сделать изменяя у пользователя unicodePwd
атрибут. Код ниже показывает как это сделать:
// Тут идет подключение
// https://github.com/golang/text
// According to the MS docs the password needs to be enclosed in quotes o_O
utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)
pwdEncoded, err := utf16.NewEncoder().String(fmt.Sprintf("%q", userPasswd))
if err != nil {
log.Fatal(err)
}
modReq := ldap.NewModifyRequest("CN=fooUser,OU=Users,dc=example,dc=com", []ldap.Control{})
modReq.Replace("unicodePwd", []string{pwdEncoded})
if err := l.ModRequest(modReq); err != nil {
log.Fatal("error setting user password:", modReq, err)
}
юникод обрабатывает код который идет из Go текстового пакета.
Наконец нам нужно включить пользователя изменяя атрибут.
modReq := ldap.NewModifyRequest("CN=fooUser,OU=Users,dc=example,dc=com", []ldap.Control{})
modReq.Replace("userAccountControl", []string{fmt.Sprintf("%d", 0x0200})
if err := l.ModRequest(modReq); err != nil {
log.Fatal("error enabling user account:", modReq, err)
}
Снова, вы можете легко проверить что был создан пользователь.
$ ldapsearch -LLL -o ldif-wrap=no -b "OU=fooUser,OU=Users,dc=example,dc=com" \
-D "{LDAP_USERNAME_DN}" -w "${LDAP_BIND_PASSWD}" -h "${LDAP_HOST}" \
'(CN=fooUser)' cn
Добавление аккаунтов для машин
Вы можете так же создать machine(aka service) аккаунт в LDAP который часто используется в сопряжении с Kerberos для хранения атрибутов сервиса и выдача доступа различным сервисам и ресурсам.
Учетная запись для машины может быть создана так же как и пользовательские аккаунты, но есть разница.
Необходимо добавить значение computer
в список значений атрибута objectClass
- главное отличие. Всё остальное может быть тем же самым. Так же некоторые люди не указывают пароля для учетной записи для машины, поэтому вы можете пропустить эту часть полностью и просто создать новую запись похожим путем как создются группы.
Изменение DN
Иногда нужно переместить LDAP запись между различными OU. Ниже можно взглянуть на код, который это делает.
// connect code comes here
// move fooUser from OU=Users to OU=SuperUsers
req := ldap.NewModifyDNRequest("CN=fooUser,OU=Users,DC=example,DC=com", "CN=fooUser", true, "OU=SuperUsers,DC=example,DC=com")
if err = conn.ModifyDN(req); err != nil {
log.Fatalf("Failed to modify userDN: %s\n", err)
}
Первое - второй параметр и это RDN (относительный DN) как замена полному пути DN. LDAP сервер хранит записи(и другие вещи) иерархическим способом ( на самом деле это сложно структурированный граф). Каждая запись существует в строгой организационной иерархии(возможно проекция реального мира AD). Отсюда позиция записи всегда относительна других DN частей или абсолютно если указано что использовать нужно полный путь DN.
Третий параметр говорит удаленному серверу где он должен удалить оригинальную запись как только она будет уделена. Если мы решим оставить запись после копирования, необходимо укзазать значение false
. Последний парамтер новый родительский каталог.
Если вы хотите переименовать CN(или какие-то атрибут) ты можешь обойти последний параметр указав его пустрой строкой т.о. код который будет переименовывать пользователя fooUser
в barUser
без перемещения его вокруг различных оушек будет выглядит следующим образом:
// move fooUser to "OU=SuperUsers,dc=example,dc=com"
req := ldap.NewModifyDNRequest("CN=fooUser,OU=Users,DC=example,DC=com", "CN=barUser", true, "")
if err = conn.ModifyDN(req); err != nil {
log.Fatalf("Failed to modify DN: %s\n", err)
}
Изменение пароля
Другая вещь которая вам будет нужна это изменение существующего пароля. Чтобы его поменять вам нужно что-то похожее на:
passwdModReq := ldap.NewPasswordModifyRequest("", "OldPassword", "NewPassword")
if _, err = l.PasswordModify(passwdModReq); err != nil {
log.Fatalf("failed to modify password: %v", err)
}
Если вы не указалис новый пароль сервер будет генерировать случайный пароль и вернет его вам:
passwdModReq := ldap.NewPasswordModifyRequest("", "OldPassword", "")
passwdModResp, err := l.PasswordModify(passwdModReq)
if err != nil {
log.Fatalf("failed to change password: %v", err)
}
newPasswd := passwdModResp.GeneratedPassword
log.Printf("New password: %s\n", newPasswd)
В отличии от изменения пароля учетной записи при создании, вам не нужно выделать кавычками UTF-16 base64 зашифрованную строку.
Удаление
Удаление LDAP pаписи очень проста. Всё что вам нужно создать DelRequest
предоставив DN записи и затем запутстить команду следующим образом:
delReq = ldap.NewDelRequest("CN=fooUser,OU=Users,dc=example,dc=com", []ldap.Control{})
if err := l.Delete(delReq); err != nil {
log.Fatalf("Error deleting service: %v", err)
}
Опять же, вы можете легко проверить используя знакомую команду ldapsearch
показанную выше.
Запро
Давайте попговорим о запросах LDAP записей и их аттрибутов.
Чтобы запросить LDAP запись вам нужное создать SearchRequest
, который вы пошлете в LDAP сервер используя функцию.
SearchRequest
предоставляет различные возмоность для настройки запроса, но мы отметим первый 3, которые я нахожу важными:
- BaseDN - поиск DN для записи
- Filter - для отсеивания результатов
- Attributes - параметры которые вам интересны. Давайте взглянем на конкретном примере и посмотрим на детали:
// код подключения
user := "fooUser"
baseDN := "DC=example,DC=com"
filter := fmt.Sprintf("(CN=%s)", ldap.EscapeFilter(user))
// Filters must start and finish with ()!
searchReq := ldap.NewSearchRequest(baseDN, ldap.ScopeWholeSubtree, 0, 0, 0, false, filter, []string{"sAMAccountName"}, []ldap.Control{})
result, err := l.Search(searchReq)
if err != nil {
return fmt.Errorf("failed to query LDAP: %w", err)
}
log.Println("Got", len(result.Entries), "search results")
Начнем с создания нового SearchRequest
и скормим ему три параметра которые я упомянул ранее. Нужно отметить, что при создании SearchRequest
есть две вещи.
- Необходим корректный фильтр который будет передаваться в функцию. Запись CN должна быть помещена в скобки
()
. Если вы там не сделаете, то получить ошибку при запуске: LDAP Result Code 201 "Filter Compile Error": ldap: filter does not start with an '(' - При использовании
\
нужно пользоватьсяldap.Escape()
функцией, чтобы исключить случайные ошибки LDAP.
Теперь поговорим об оставшихся параметрах. Используя ldap.ScopeWholeSubtree
мы говорим LDAP серверу, что хотим искать записи по всему дереву DN.
Есть еще параметры доступные вроде ldap.ScobeBaseObject
который ищет только внутри RDN
. Но для этого примера, я хотел показать широкий доменый поиск.
Еще необходимо отметить, что мы передаем срез LDAP параметров в которых мы заинтересованны. Если вы оставите срез аттрибутов пустым поиск вернет все параметры записей LDAP, который вам понадобится, но я хотел показать как вы можете выбрать список атрибутов. Будте осторожны, размер аттрибутов вы можете запросить их все.
Есть множество других There are plenty of other options to search LDAP at your disposal. Particularly, you should have a look at SearchWithPaging function which as its name suggests lets you page the query results if you expect huge loads of them.
Display results
Now that you know how to query the records you might want to display them in the terminal in some human-readable form. There are two handy functions at your disposal: Print and PrettyPrint.
Personally I think they seem almost the same, though PrettyPrint lets you indent the result(s) so you can see the AD tree structure more clearly. See for yourself the results of using both of the functions:
This is the result of Print():
DN: CN=fooUser,OU=Users,DC=example,DC=com
sAMAccountName: [fooUser]
This is the result of PrettyPrint(2) (see the attribute 2-space indentation):
DN: CN=fooUser,OU=Users,DC=example,DC=com
sAMAccountName: [fooUser]
Conclusion
We have reached the end of this post. Congrats and thank you if you stayed with me until the end! Hopefully, you learned something new and useful which expands your Go toolbox.
When I started using ldap-go library some things were not quite obvious to me, so hopefully the examples in this blog post help whoever ends up having to interact with AD using Go.
As always, if you have any questions or find any inaccuracies in the post let me know in the comments. Until next time!
- https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol
- https://tools.ietf.org/html/rfc4513#section-5.1.2
- https://tools.ietf.org/html/rfc4513#section-6.3.1
- https://tools.ietf.org/html/rfc4422#appendix-A
- https://docs.microsoft.com/en-us/windows/win32/adschema/a-instancetype
- https://docs.microsoft.com/en-us/windows/win32/adschema/a-grouptype
- https://github.com/golang/text
- https://en.wikipedia.org/wiki/Kerberos_(protocol)
Writing a Reverse Proxy in just one line with Go
Leave your programming language hang ups at the door and come admire the best standard library I’ve ever come across.
This is all the code you actually require…
Choosing a Programming Language for a project shouldn’t be like declaring who your favourite team is. it should be a matter of pragmatism and choosing the right tool for the right job.
In this post I want to show you when and why Go shines as a language. Specifically I want to show you how rock solid their standard lib is for basic internet programming. Even more specifically… were gonna write a Reverse Proxy!
“Go has a lot going for it but where it really struts it stuff is in these lower level network plumbing tasks, there’s no better language.” What is a reverse proxy? A big fancy way of saying a traffic forwarder. I get a request send from a client, send that request to another server, receive a response from the server and forward it back to the client. The reverse part of this simply means the proxy itself determines where to send traffic and when
Why is it useful? Because the concept is so simple it can be applied to assist in many different cases: Load balancing, A/B Testing, Caching, Authentication etc…
By the end of this short post you will have learned how to:
Serve HTTP requests
Parse the body of a request Serve traffic to another server using a Reverse Proxy Our Reverse Proxy Project Lets dive into the actual project. What we are going to do is have a web server that:
- Takes requests
- Reads the body of a request, specifically the proxy_condition field
- If the proxy domain is equal to A send traffic to URL 1
- If the proxy domain is equal to B send traffic to URL 2
- If the proxy domain is neither then send traffic to the Default URL.
Prerequisites
Go for programming with. http-server for creating simple servers with. Setting up our environment First thing we want to do is input all the required configuration variables into our environment so that we can both use them in our application while keeping them out of source code.
I find the best approach is to create a .env file that contains the desired environment variables.
Below is what I have for this specific project:
export PORT=1330
export A_CONDITION_URL="http://localhost:1331"
export B_CONDITION_URL="http://localhost:1332"
export DEFAULT_CONDITION_URL="http://localhost:1333"
This is a habit I picked up from the 12 Factor App
After you save your .env file you can run:
source `.env
to configure load the config into your environment any time.
Laying the foundation of our project Next lets create a file called main.go that does the following:
- When started logs the PORT, A_CONDITION_URL, B_CONDITION_URL, and DEFAULT_CONDITION_URL environment variables to the console
- Listen for requests on the path: /
package main
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
)
// Get env var or default
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
// Get the port to listen on
func getListenAddress() string {
port := getEnv("PORT", "1338")
return ":" + port
}
// Log the env variables required for a reverse proxy
func logSetup() {
a_condtion_url := os.Getenv("A_CONDITION_URL")
b_condtion_url := os.Getenv("B_CONDITION_URL")
default_condtion_url := os.Getenv("DEFAULT_CONDITION_URL")
log.Printf("Server will run on: %s\n", getListenAddress())
log.Printf("Redirecting to A url: %s\n", a_condtion_url)
log.Printf("Redirecting to B url: %s\n", b_condtion_url)
log.Printf("Redirecting to Default url: %s\n", default_condtion_url)
}
// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
// We will get to this...
}
func main() {
// Log setup values
logSetup()
// start server
http.HandleFunc("/", handleRequestAndRedirect)
if err := http.ListenAndServe(getListenAddress(), nil); err != nil {
panic(err)
}
}
(💀Let’s get the skeletons out of the closet so we can move onto the fun stuff.)
Now you should be able to run
Parse the request body Now that we have the skeleton of our project together we want to start creating the logic that will handle parsing the request body. Start by updating handleRequestAndRedirect to parse the proxy_condition value from the request body.
type requestPayloadStruct struct {
ProxyCondition string `json:"proxy_condition"`
}
// Get a json decoder for a given requests body
func requestBodyDecoder(request *http.Request) *json.Decoder {
// Read body to buffer
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
panic(err)
}
// Because go lang is a pain in the ass if you read the body then any susequent calls
// are unable to read the body again....
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
return json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body)))
}
// Parse the requests body
func parseRequestBody(request *http.Request) requestPayloadStruct {
decoder := requestBodyDecoder(request)
var requestPayload requestPayloadStruct
err := decoder.Decode(&requestPayload)
if err != nil {
panic(err)
}
return requestPayload
}
// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
requestPayload := parseRequestBody(req)
// ... more to come
}
(Basic parsing of a JSON blob to a struct in Go.)
Use
proxy_condition to determine where we send traffic Now that we have the value of the proxy_condition from the request we will use it to decide where we direct our reverse proxy to.
Remember from earlier that we have three cases:
If proxy_condition is equal to A then we send traffic to A_CONDITION_URL If proxy_condition is equal to B then we send traffic to B_CONDITION_URL Else send traffic to DEFAULT_CONDITION_URL
// Log the typeform payload and redirect url
func logRequestPayload(requestionPayload requestPayloadStruct, proxyUrl string) {
log.Printf("proxy_condition: %s, proxy_url: %s\n", requestionPayload.ProxyCondition, proxyUrl)
}
// Get the url for a given proxy condition
func getProxyUrl(proxyConditionRaw string) string {
proxyCondition := strings.ToUpper(proxyConditionRaw)
a_condtion_url := os.Getenv("A_CONDITION_URL")
b_condtion_url := os.Getenv("B_CONDITION_URL")
default_condtion_url := os.Getenv("DEFAULT_CONDITION_URL")
if proxyCondition == "A" {
return a_condtion_url
}
if proxyCondition == "B" {
return b_condtion_url
}
return default_condtion_url
}
// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
requestPayload := parseRequestBody(req)
url := getProxyUrl(requestPayload.ProxyCondition)
logRequestPayload(requestPayload, url)
// more still to come...
}
Reverse Proxy to that URL
Finally we are onto the actual reverse proxy! In so many languages a reverse proxy would require a lot of thought and a fair amount of code or at least having to import a sophisticated library.
However Golang’s standard library makes creating a reverse proxy so simple it’s almost unbelievable. Below is essentially the only line of code you need:
httputil.NewSingleHostReverseProxy(url).ServeHTTP(res, req) Note that in the following code we add a little extra so it can fully support SSL redirection (though not necessary):
// Serve a reverse proxy for a given url
func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {
// parse the url
url, _ := url.Parse(target)
// create the reverse proxy
proxy := httputil.NewSingleHostReverseProxy(url)
// Update the headers to allow for SSL redirection
req.URL.Host = url.Host
req.URL.Scheme = url.Scheme
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
req.Host = url.Host
// Note that ServeHttp is non blocking and uses a go routine under the hood
proxy.ServeHTTP(res, req)
}
// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
requestPayload := parseRequestBody(req)
url := getProxyUrl(requestPayload.ProxyCondition)
logRequestPayload(requestPayload, url)
serveReverseProxy(url, res, req)
}
The one time in the project it felt like Go was truly getting out of my way.Start it all up
Ok now that we have this all wired up setup our application on port 1330 and our 3 simple servers on ports 1331–1333 (all in separate terminals):
source .env && go install && $GOPATH/bin/reverse-proxy-demo
http-server -p 1331
http-server -p 1332
http-server -p 1333
With all these up and ruuning we can start to send through a requests with a json body in another terminal like so: F
curl --request GET \
--url http://localhost:1330/ \
--header 'content-type: application/json' \
--data '{
"proxy_condition": "a"
}'
If your looking for a great HTTP request client I cannot recommend Insomnia enough.
and Voila we can start to see our reverse proxy directing traffic to one of our 3 servers based on what we set in the proxy_condition field!
image (Its alive!!!)
Wrap Up
Go has a lot going for it but where it really struts it stuff is in these lower level network “plumbing” tasks, there’s no better language. What we’ve written here is simple, performant, reliable and very much ready for use in production.
For simple services I can see myself reaching for Go again in the future.
🧞 This is open source! you can find it here on Github
❤️ I only write about programming and remote work. If you follow me on Twitter I won’t waste your time.
Golang
Инструкция для линтинга Go программ
Линтинг это процесс обнаружения и опвещения различных шаблонов найденных в коде, с целью улучшения состояния, и отлавливания багов на ранних стадиях разработки. Это обычно полезно при работе в комане, так как помогает делать весь код одинаковым не зависимо от того кто именно пишет, убирать излишнюю сложность, и делать код легче в обслуживании. В этой статье, я расскажу со всех сторон о настройке линтинга для Go програм, и поговорим о лучших способах введения их в эксплуатацию.
Линтинг кода один из самых простых вещей, которые вы можете делать чтобы убедиться в содержании кодовых практик в проекте. Go уже заходи дальше других языков программирования используя gofmt
, инструмент форматирования которые проверяет, что весь код go выглядит одинаково, но правда работает только с тем, как код выглядит. Инструмент vet
go языка так же может помочь с определением странных конструкций, которые могут быть пойманы компилятором, но он отлавливает только ограниченное количество потенциальных проблем.
Задача разработки более всесторнних линтинг инструментов была оставлена на широкую общественность, и общество уже принесло гору линтеров, каждый для своей цели. Изветсные примеры включают в себя:
- unused - Проврка GO кода на неиспользуемые констатны, переменные, функции и типы.
- goconst - Нахождение повторяющихся строк, которые могут быть заменены константой.
- gocyclo - Вычисления и проверки цикличной сложности функций.
- errcheck - Обнаружение непроверяемых ошибок в программе на Go
Проблема в наличии такого большго набора отдельных инструментов, в том, что вам нужно качать каждую по отдельности и управлять их версиями. В добавок, запуск каждой из них по очереди может быть очень медленно. golangci-lint сборщик, который запускает линтеры в паралели, переиспользует Go кэш, и хранит результат анализа для того, чтобы улучшить производительность паралельного запуска, являетя одним из предпочитаемым способом для настройки линтера в Go проекте.
Проект golang-lint
был разработан для сбора и запуска нескольких отдельных линтеров в паралели, для удобствао и производительности. Когда вы ставите программу, вы получаете порядка 48 линтеров включительно(во время написания), и вы можете продолжит выбирать какой из них важный для вашего проекта. Кроме того во время их локального запуска, есть возможность настроих их в качестве шага CI.
Установка golang-lint
Команда ниже используется для устаовки golang-lint
локально на любую операционную систему.
$ go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
После установки, вы должны проверить версию пакета:
$ golangci-lint version
golangci-lint has version v1.40.1 built from (unknown, mod sum: "h1:pBrCqt9BgI9LfGCTKRTSe1DfMjR6BkOPERPaXJYXA6Q=") on (unknown)
Вы можете так же посмотреть все доступные линтер следующей командой:
$ golangci-lint help linters
Если вы запустили включенные линтеры в корне проекта, вы можете увидеть ошибки. Каждая проблема содержит в себе описание того, что именно нужно исправить, и содержит краткое описание проблемы, а так же файл и строку с проблемой.
$ golangci-lint run # equivalent of golangci-lint run ./...
Вы можете выбрать, какую папку и файлы необоходимо проанализировать указав в команде:
$ golangci-lint run dir1 dir2 dir3/main.go
Настройка golangci-lint
GolangCI-Lint разработан быть гибким насколько это возможно, для различного набора случаев. Настройка golang-lint
может управляться через параметры коммандной строки или файл настройки, так же имеет преимущства старого над новым если один параметр задается несколько раз. Вот пример который использует параметры командной строки для отключения всех линтеров и указания определенных линтеров, которые должны запуститься.
$ golangci-lint run --disable-all -E revive -E errcheck -E nilerr -E gosec
Вы можете так же запустить с настройками по-умолчанию из golang-lint
. Вот как найти информацию о доступных настройках.
$ golangci-lint help linters | sed -n '/Linters presets:/,$p'
Linters presets:
bugs: asciicheck, bodyclose, durationcheck, errcheck, errorlint, exhaustive, exportloopref, gosec, govet, makezero, nilerr, noctx, rowserrcheck, scopelint, sqlclosecheck, staticcheck, typecheck
comment: godot, godox, misspell
complexity: cyclop, funlen, gocognit, gocyclo, nestif
error: errcheck, errorlint, goerr113, wrapcheck
format: gci, gofmt, gofumpt, goimports
import: depguard, gci, goimports, gomodguard
metalinter: gocritic, govet, revive, staticcheck
module: depguard, gomoddirectives, gomodguard
performance: bodyclose, maligned, noctx, prealloc
sql: rowserrcheck, sqlclosecheck
style: asciicheck, depguard, dogsled, dupl, exhaustivestruct, forbidigo, forcetypeassert, gochecknoglobals, gochecknoinits, goconst, gocritic, godot, godox, goerr113, goheader, golint, gomnd, gomoddirectives, gomodguard, goprintffuncname, gosimple, ifshort, importas, interfacer, lll, makezero, misspell, nakedret, nlreturn, nolintlint, paralleltest, predeclared, promlinter, revive, stylecheck, tagliatelle, testpackage, thelper, tparallel, unconvert, wastedassign, whitespace, wrapcheck, wsl
test: exhaustivestruct, paralleltest, testpackage, tparallel
unused: deadcode, ineffassign, structcheck, unparam, unused, varcheck
Теперь можно запустить настройки по-умолчанию передав их имена в ключе --preset
или -p
:
$ golangci-lint run -p bugs -p error
Настройку golang-lint
для проекта лучше производить через файл. Таким образом, вы сможете настравить отдельные настройки линтера, которые не доступны из командной строки. Вы можете указать yaml, toml или json формат файла настройки, но я рекомендую остановиться на yaml формате, так сказано в оф документации.
Говоря в общем, вы должны создать отдельную настроку для проекта в корне. Программа автоматически найдет его в папке и пойдет выше до корня проекта. Это значит вы можете достигнуть глобальной настройки для всех проектов поместив файл с настройками в домашней директории(не советую). Этот файл будет использован если локальные настройки не будут найдены.
Простой файл настройки доступен на веб сайте golang-lint
со всеми поддерживаемыми опциями, их описание, и стандартные значения. Вы можете использовать их как остчетную точку при создании своей настройки. Имейте в виду, что некоторые линтеры выполняют похожие функции, поэтому нужно включать линтеры обдуманно, чтобы избежать избыточных записей. Вот общая настройка которую можно использовать для проекта:
.golangci.yml
linters-settings:
errcheck:
check-type-assertions: true
goconst:
min-len: 2
min-occurrences: 3
gocritic:
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
govet:
check-shadowing: true
nolintlint:
require-explanation: true
require-specific: true
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- depguard
- dogsled
- dupl
- errcheck
- exportloopref
- exhaustive
- goconst
- gocritic
- gofmt
- goimports
- gomnd
- gocyclo
- gosec
- gosimple
- govet
- ineffassign
- misspell
- nolintlint
- nakedret
- prealloc
- predeclared
- revive
- staticcheck
- structcheck
- stylecheck
- thelper
- tparallel
- typecheck
- unconvert
- unparam
- varcheck
- whitespace
- wsl
run:
issues-exit-code: 1
Решение ошибок линтера
Иногда необхоидмо выключить определенные проблемы линтера, во время работы с кодом. Это можно получить двумя способами: через команду nolint
и через правила исключения в файле настройки. Давайте посмотрим каждый подход по очереди.
команда nolint
Предположим у нас есть следующий код, который выводит преводслучайное целое число в стандартный вывод.
main.go
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int())
}
Выполнив golang-lint run
для этого файла мы получим следущий набор ошибок, так как включен gosec
линтер:
$ golangci-lint run -E gosec
main.go:11:14: G404: Use of weak random number generator (math/rand instead of crypto/rand) (gosec)
fmt.Println(rand.Int())
^
Линтер поощряет испольлзвание метода Int
из crypto-rand
взамет, из-за криптографической безопасности. но он имеет бескомпромисно менее дружелюбный API и медленную производительность. Если вас это не беспокоит, вы можете просто проигнорировать ошибку добавив команду nolint
в нужный файл:
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int()) //nolint
}
Согласно договоренности, комментарии для машины не должны содедржать пробела. поэтому нужно использовать //nolint вместо // nolint.
Использование nolint
приведет к тому, что эта найденная проблема будет проигнорированна. Вы можете выключить проблемы определенного лиинтера указав её имя в комманде. Это позволит пропустить строку конкретному линтеру, но остальные пройдутся по ней.
main.go
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int()) //nolint:gosec
}
Когда вы используете nolint
команду в начале файла, то он выключает линтер для всего файла:
main.go
//nolint:govet,errcheck
package main
Вы можете так же отключить линтер для блока кода(например функции), используя nolint
команду в начале блока кода.
main.go
//nolint
func aFunc() {
}
После добавления nolint
команды, рекомендуем, добавить комментарий который объясняяет зачем это нужно. Этот комементарий должен быть помещен на строку с флагом:
main.go
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println(rand.Int()) //nolint:gosec // for faster performance
}
Вы можете указать правила для вашей команды относительно nolint
комментирования включив nolintlint
линтер. Он может оповещять проблемы относительно проблем nolint
без указания определенных отключенных линтеров, или без требования комментария.
$ golangci-lint run
main.go:11:26: directive `//nolint` should mention specific linter such as `//nolint:my-linter` (nolintlint)
fmt.Println(rand.Int()) //nolint
^
Правила исключения
Правила исключения могут быть указаны файле настроек для более детального контроля, какие файлы должны быть залинтены, и о какой проблеме нужно сообщать. Для примера, вы можете выключить определенный линтер из запуска файлов тестов(_test.go
) или вы можете выключить линтер для всего проекта
.golangci.yml
issues:
exclude-rules:
- path: _test\.go # disable some linters on test files
linters:
- gocyclo
- gosec
- dupl
# Exclude some gosec messages project-wide
- linters:
- gosec
text: "weak cryptographic primitive"
Интеграция в существующий проект
При добавлении golangcli-lint
в существующий проект, вы можете получить большое количество проблем и может быть трудно исправить все их одновременно. Однако, это не значит, что вы должны бросить идею линтинга для вашего проекта по этой причине.
When adding golangci-lint to an existing project, you may get a lot of issues and it may be difficult to fix all of them at once. However, that doesn’t mean that you should abandon the idea of linting your project for this reason. There is a new-from-rev setting that allows you to show only new issues created after a specific git revision which makes it easy to lint new code only until adequate time can be budgeted to fix older issues. Once you find the revision you want to start linting from (with git log), you can specify it in your configuration file as follows:
.golangci.yml
issues:
# Show only new issues created after git revision: 02270a6
new-from-rev: 02270a6
Integrating golangci-lint in your editor
GolangCI-Lint supports integrations with several editors in order to get quick feedback. In Visual Studio Code, all you need to do is install the Go extension, and add the following lines to your settings.json file:
settings.json
{
"go.lintTool":"golangci-lint",
"go.lintFlags": [
"--fast"
]
}
Vim users can integrate golangci-lint with a variety of plugins including vim-go, ALE, and Syntastic. You can also integrate it with coc.nvim, vim-lsp, or nvim.lspconfig with help of golangci-lint-langserver. Here’s how I integrated golangci-lint in my editor with coc.nvim. First, install the language server:
$ go install github.com/nametake/golangci-lint-langserver@latest
Next, open the coc.nvim
config file with :CocConfig
, and add the following lines:
coc-settings.json
{
"languageserver": {
"golangci-lint-languageserver": {
"command": "golangci-lint-langserver",
"filetypes": ["go"],
"initializationOptions": {
"command": ["golangci-lint", "run", "--out-format", "json"]
}
}
}
}
Save the config file, then restart coc.nvim with :CocRestart, or open a new instance of Vim. It should start working as soon as a Go file is open in the editor.
Refer to the golangci-lint docs for more information on how to integrate it with other editors.
Setting up a pre-commit hook
Running golangci-lint
as part of your Git pre-commit hooks is a great way to ensure that all Go code that is checked into source control is linted properly. If you haven’t set up a pre-commit hook for your project, here’s how to set one up with pre-commit, a language-agnostic tool for managing Git hook scripts.
Install the pre-commit
package manager by following the instructions on this page, then create a .pre-commit-config.yaml
file in the root of your project, and populate it with the following contents:
.pre-commit-config.yaml
repos:
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v0.8.3 # change this to the latest version
hooks:
- id: golangci-lint
This configuration file extends the pre-commit-golang repository which supports various hooks for Go projects. The golangci-lint
hook targets staged files only, which is handy for when introducing golangci-lint
to an existing project so that you don’t get overwhelmed with so many linting issues at once. Once you’ve saved the file, run pre-commit install
to set up the git hook scripts in the current repository.
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
On subsequent commits, the specified hooks will run on all staged .go files and halt the committing process if errors are discovered. You’ll need to fix all the linting issues before you’ll be allowed to commit. You can also use the pre-commit run command if you want to test the pre-commit hook without making a commit.
Continuous Integration (CI) workflow
Running your project’s linting rules on each pull request prevents code that is not up to standards from slipping through into your codebase. This can also be automated by adding golangci-lint to your Continuous Integration process. If you use GitHub Actions, the official Action should be preferred over a simple binary installation for performance reasons. After setting it up, you’ll get an inline display of any reported issues on pull requests.
During the setup process, ensure to pin the golangci-lint version that is being used so that it yields consistent results with your local environment. The project is being actively developed, so updates may deprecate some linters, or report more errors than previously detected for the same source code.
Conclusion
Linting your programs is a sure fire way to ensure consistent coding practices amongst all contributors to a project. By adopting the tools and processes discussed in this article, you’ll be well on your way to doing just that.
Thanks for reading, and happy coding!
I18n in Go: Managing Translations
Recently I've been building a fully internationalized (i18n) and localized (l10n) web application for the first time with Go's golang.org/x/text packages. I've found that the packages and tools that live under golang.org/x/text are really effective and well designed, although it's been a bit of a challenge to figure out how to put it all together in a real application.
In this tutorial I want to explain how you can use golang.org/x/text packages to manage translations in your application. Specifically:
How to use the golang.org/x/text/language and golang.org/x/text/message packages to print translated messages from your Go code. How to use the gotext tool to automatically extract messages for translation from your code into JSON files. How to use gotext to parse translated JSON files and create a catalog containing translated messages. How to manage variables in messages and provided pluralized versions of translations. Note: Just in case you're not already aware, the packages that live under golang.org/x are part of the official Go Project but outside the main Go standard library tree. They are held to looser standards that the standard library packages, which means they aren't subject to the Go compatibility promise (i.e. their APIs might change), and documentation may not always be complete. What we'll be building To help put this into context, we're going to create a simple pre-launch website for an imaginary online bookstore. We'll start off slowly and build up the code step-by-step.
Our application will have just a single home page, and we'll localize the page content based on a locale identifier at the start of the URL path. We'll set up our application to support three different locales: the United Kingdom, Germany, and the French-speaking part of Switzerland.
URL Localized for localhost:4018/en-gb United Kingdom localhost:4018/de-de Germany localhost:4018/fr-ch Switzerland (French-speaking) We're going to follow a common convention and use BCP 47 language tags as the locale identifier in our URLs. Simplifying things hugely for the sake of this tutorial, BCP 47 language tags typically take the format {language}-{region}. The language part is a ISO 639-1 code and the region is a two-letter country code from ISO_3166-1. It's conventional to uppercase the region (like en-GB), but BCP 47 tags are technically case-insensitive and it's OK for us to use all-lowercase versions in our URLs.
Scaffolding a web application If you'd like to follow along with the application build, go ahead and run the following commands to setup a new project directory.
$ mkdir bookstore $ cd bookstore $ go mod init bookstore.example.com go: creating new go.mod: module bookstore.example.com At this point, you should have a go.mod file in the root of the project directory with the module path bookstore.example.com.
Next create a new cmd/www directory to hold the code for the bookstore web application, and add main.go and handlers.go files like so:
$ mkdir -p cmd/www $ touch cmd/www/main.go cmd/www/handlers.go Your project directory should now look like this:
. ├── cmd │ └── www │ ├── handlers.go │ └── main.go └── go.mod Let's begin in the cmd/www/main.go file and add the code to declare our application routes and start a HTTP server.
Because our application URL paths will always use a (dynamic) locale as a prefix — like /en-gb/bestsellers or /fr-ch/bestsellers — it's simplest if our application uses a third-party router which supports dynamic values in URL path segments. I'm going to use pat, but feel free to use an alternative like chi or gorilla/mux if you prefer.
Note: If you're not sure which router to use in your project, you might like to take a look at my comparison of Go routers blog post. OK, open up the main.go file and add the following code:
File: cmd/www/main.go package main
import ( "log" "net/http"
"github.com/bmizerany/pat"
)
func main() { // Initialize a router and add the path and handler for the homepage. mux := pat.New() mux.Get("/:locale", http.HandlerFunc(handleHome))
// Start the HTTP server using the router.
log.Print("starting server on :4018...")
err := http.ListenAndServe(":4018", mux)
log.Fatal(err)
} Then in the cmd/www/handlers.go file, add a handleHome() function which extracts the locale identifer from the URL path and echoes it in the HTTP response.
File: cmd/www/handlers.go package main
import ( "fmt" "net/http" )
func handleHome(w http.ResponseWriter, r *http.Request) {
// Extract the locale from the URL path. This line of code is likely to
// be different for you if you are using an alternative router.
locale := r.URL.Query().Get(":locale")
// If the locale matches one of our supported values, echo the locale
// in the response. Otherwise send a 404 Not Found response.
switch locale {
case "en-gb", "de-de", "fr-ch":
fmt.Fprintf(w, "The locale is %s\n", locale)
default:
http.NotFound(w, r)
}
} Once that's done, run go mod tidy to tidy your go.mod file and download any necessary dependencies, and then run the web application.
$ go mod tidy go: finding module for package github.com/bmizerany/pat go: found github.com/bmizerany/pat in github.com/bmizerany/pat v0.0.0-20210406213842-e4b6760bdd6f
$ go run ./cmd/www/ 2021/08/21 21:22:57 starting server on :4018... If you make some requests to the application using curl, you should find that the appropriate locale is echoed back to you like so:
$ curl localhost:4018/en-gb The locale is en-gb
$ curl localhost:4018/de-de The locale is de-de
$ curl localhost:4018/fr-ch The locale is fr-ch
$ curl localhost:4018/da-DK 404 page not found Extracting and translating text content Now that we've laid the groundwork for our web application, let's get into the core of this tutorial and update the handleHome() function so that it renders a "Welcome!" message translated for the specific locale.
In this project we'll use British English (en-GB) as the default 'source' or 'base' language in our application, but we'll want to render a translated version of the welcome message in German and French for the other locales.
To do this, we'll need to import the golang.org/x/text/language and golang.org/x/text/message packages and update our handleHome() function to do the following two things:
Construct a language.Tag which identifies the target language that we want to translate the message in to. The language package contains some pre-defined tags for common language variants, but I find that it's easier to use the language.MustParse() function to create a tag. This let's you create a language.Tag for any valid BCP 47 value, like language.MustParse("fr-CH"). Once you have a language tag, you can use the message.NewPrinter() function to create a message.Printer instance that prints out messages in that specific language. If you're following along, please go ahead and update your cmd/www/handlers.go file to contain the following code:
File: cmd/www/handlers.go package main
import ( "net/http"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
func handleHome(w http.ResponseWriter, r *http.Request) { locale := r.URL.Query().Get(":locale")
// Declare variable to hold the target language tag.
var lang language.Tag
// Use language.MustParse() to assign the appropriate language tag
// for the locale.
switch locale {
case "en-gb":
lang = language.MustParse("en-GB")
case "de-de":
lang = language.MustParse("de-DE")
case "fr-ch":
lang = language.MustParse("fr-CH")
default:
http.NotFound(w, r)
return
}
// Initialize a message.Printer which uses the target language.
p := message.NewPrinter(lang)
// Print the welcome message translated into the target language.
p.Fprintf(w, "Welcome!\n")
} Again, run go mod tidy to download the necessary dependencies…
$ go mod tidy go: finding module for package golang.org/x/text/message go: finding module for package golang.org/x/text/language go: downloading golang.org/x/text v0.3.7 go: found golang.org/x/text/language in golang.org/x/text v0.3.7 go: found golang.org/x/text/message in golang.org/x/text v0.3.7 And then run the application:
$ go run ./cmd/www/ 2021/08/21 21:33:52 starting server on :4018... When you make a request to any of the supported URLs, you should now see the (untranslated) welcome message like this:
$ curl localhost:4018/en-gb Welcome!
$ curl localhost:4018/de-de Welcome!
$ curl localhost:4018/fr-ch Welcome! So in all cases we're seeing the "Welcome!" message in our en-GB source language. That's because we still need to provide Go's message package with the actual translations that we want to use. Without the actual translations, it falls back to displaying the message in the source language.
There are a number of ways to provide Go's message package with translations, but for most non-trivial applications it's probably sensible to use some automated tooling to help you manage the task. Fortunately, Go provides the gotext tool to assist with this.
Note: The gotext tool we're using is the one from golang.org/x/text/cmd/gotext. It shouldn't be confused with the github.com/leonelquinteros/gotext package (which is designed to work with GNU gettext utilities and PO/MO files). If you're following along, please use go install to install the gotext executable on your machine:
$ go install golang.org/x/text/cmd/gotext@latest All being well, the tool should be installed to your $GOBIN directory on your system path and you can run it like so:
$ which gotext /home/alex/go/bin/gotext
$ gotext gotext is a tool for managing text in Go source code.
Usage:
gotext command [arguments]
The commands are:
update merge translations and generate catalog
extract extracts strings to be translated from code
rewrite rewrites fmt functions to use a message Printer
generate generates code to insert translated messages
Use "gotext help [command]" for more information about a command.
Additional help topics:
Use "gotext help [topic]" for more information about that topic. I really like the gotext tool — it's functionality is excellent — but there are a couple of important things to point out before we carry on.
The first thing is that go text is designed to work in conjunction with go generate, not as a standalone command-line tool. You can run it as a standalone tool, but weird things happen and it's a lot smoother if you use it in the way it's intended.
The other thing is that documentation and help functionality is basically non-existent. The best guidance on how to use it are the examples in the repository and, probably, this article that you're reading right now. There is an open issue about the lack of help functionality, and hopefully this is something that will improve in the future.
In this tutorial, we're going to store the all the code relating to translations in a new internal/translations package. We could keep all the translation code for our web application under cmd/www instead, but in my (limited) experience I've found that using a separate internal/translations package is better. It helps separate concerns and also makes it possible to reuse the same translations across different applications in the same project. YMMV.
If you're following along, go ahead and create that new directory and a translations.go file like so:
$ mkdir -p internal/translations $ touch internal/translations/translations.go At this point, your project structure should look like this:
. ├── cmd │ └── www │ ├── handlers.go │ └── main.go ├── go.mod ├── go.sum └── internal └── translations └── translations.go Next, let's open up the internal/translations/translations.go file and add a go generate command which uses gotext to extract the messages for translation from our application.
File: internal/translations/translations.go package translations
//go:generate gotext -srclang=en-GB update -out=catalog.go -lang=en-GB,de-DE,fr-CH bookstore.example.com/cmd/www There's a lot going on in this command, so let's quickly break it down.
The -srclang flag contains the BCP 47 tag for the source (or 'base') language that we are using in the application. In our case, the source language is en-GB. update is thegotext function that we want to execute. As well as update there are extract, rewrite and generate functions, but in the translation workflow for a web application the only one you actually need is update. The -out flag contains the path that you want the message catalog to be output to. This path should be relative to the file containing the go generate command. In our case, we've set the value to catalog.go, which means that the message catalog will be output to a new internal/translations/catalog.go file. We'll talk more about message catalogs and explain what they are shortly. The -lang flag contains a comma-separated list of the BCP 47 tags that you want to create translations for. You don't need to include the source language here, but (as we'll demonstrate later in this article) it can be helpful for dealing with pluralization of text content. Lastly, we have the fully-qualified module path for the package(s) that you want to create translations for (in this case bookstore.example.com/cmd/www). You can list multiple packages if necessary, separated by a whitespace character. When we execute this go generate command, gotext will walk the code for the cmd/www application and look for all calls to a message.Printer†. It then extracts the relevant message strings and outputs them to some JSON files for translation.
† Important: It's critical to note when gotext walks your code it actually only looks for calls to message.Printer.Printf(), Fprintf() and Sprintf() — basically the three methods that end with an f. It ignores all other methods such as Sprint() or Println(). You can see this behavior in the gotext implementation here. OK, let's put this into action and call go generate on our translations.go file. In turn, this will execute the gotext command that we included at the top of that file.
$ go generate ./internal/translations/translations.go de-DE: Missing entry for "Welcome!". fr-CH: Missing entry for "Welcome!". Cool, this looks like we're getting somewhere. We've got some useful feedback to indicate that we are missing the necessary German and French translations for our "Welcome!" message.
If you take a look at the directory structure for your project, it should now look like this:
. ├── cmd │ └── www │ ├── handlers.go │ └── main.go ├── go.mod ├── go.sum └── internal └── translations ├── catalog.go ├── locales │ ├── de-DE │ │ └── out.gotext.json │ ├── en-GB │ │ └── out.gotext.json │ └── fr-CH │ └── out.gotext.json └── translations.go We can see that the go generate command has automatically generated an internal/translations/catalog.go file for us (which we'll look at in a minute), and a locales folder containing out.gotext.json files for each of our target languages.
Let's take a look at the internal/translations/locales/de-DE/out.gotext.json file:
File: internal/translations/locales/de-DE/out.gotext.json { "language": "de-DE", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "" } ] } In this JSON file, the relevant BCP 47 language tag is defined at the top of the file, followed by a JSON array of the messages which require translation. The message value is the text for translation in the source language, and the (currently empty) translation value is where we should enter appropriate German translation.
It's important to emphasize that you don't edit this file in place. Instead, the workflow for adding a translation goes like this:
You generate the out.gotext.json files containing the messages which need to be translated (which we've just done). You send these files to a translator, who edits the JSON to include the necessary translations. They then send the updated files back to you. You then save these updated files with the name messages.gotext.json in the folder for the appropriate language. For demonstration purposes, let's quickly simulate this workflow by copying the out.gotext.json files to messages.gotext.json files, and updating them to include the translated messages like so:
$ cp internal/translations/locales/de-DE/out.gotext.json internal/translations/locales/de-DE/messages.gotext.json $ cp internal/translations/locales/fr-CH/out.gotext.json internal/translations/locales/fr-CH/messages.gotext.json File: internal/translations/locales/de-DE/messages.gotext.json { "language": "de-DE", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "Willkommen!" } ] } File: internal/translations/locales/fr-CH/messages.gotext.json { "language": "fr-CH", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "Bienvenue !" } ] } If you like, you can also take a look at the out.gotext.json file for our en-GB source language. You'll see that the translation value for the message has been auto-filled for us.
File: internal/translations/locales/en-GB/messages.gotext.json { "language": "en-GB", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "Welcome!", "translatorComment": "Copied from source.", "fuzzy": true } ] } The next step is to run our go generate command again. This time, it should execute without any warning messages about missing translations.
$ go generate ./internal/translations/translations.go Now it's a good time to take a look at the internal/translations/catalog.go file, which is automatically generated for us by the gotext update command. This file contains a message catalog, which is — very roughly speaking — a mapping of messages and their relevant translations for each target language.
Let's take a quick look inside the file:
File: internal/translations/catalog.go // Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT.
package translations
import ( "golang.org/x/text/language" "golang.org/x/text/message" "golang.org/x/text/message/catalog" )
type dictionary struct { index []uint32 data string }
func (d *dictionary) Lookup(key string) (data string, ok bool) { p, ok := messageKeyToIndex[key] if !ok { return "", false } start, end := d.index[p], d.index[p+1] if start == end { return "", false } return d.data[start:end], true }
func init() { dict := map[string]catalog.Dictionary{ "de_DE": &dictionary{index: de_DEIndex, data: de_DEData}, "en_GB": &dictionary{index: en_GBIndex, data: en_GBData}, "fr_CH": &dictionary{index: fr_CHIndex, data: fr_CHData}, } fallback := language.MustParse("en-GB") cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback)) if err != nil { panic(err) } message.DefaultCatalog = cat }
var messageKeyToIndex = map[string]int{ "Welcome!\n": 0, }
var de_DEIndex = []uint32{ // 2 elements 0x00000000, 0x00000011, } // Size: 32 bytes
const de_DEData string = "\x04\x00\x01\n\f\x02Willkommen!"
var en_GBIndex = []uint32{ // 2 elements 0x00000000, 0x0000000e, } // Size: 32 bytes
const en_GBData string = "\x04\x00\x01\n\t\x02Welcome!"
var fr_CHIndex = []uint32{ // 2 elements 0x00000000, 0x00000010, } // Size: 32 bytes
const fr_CHData string = "\x04\x00\x01\n\v\x02Bienvenue !"
// Total table size 143 bytes (0KiB); checksum: 385F6E56 I don't want to dwell on the details here, because it's OK for use to treat this file as something of a 'black box', and — as warned by the comment at the top of the file — we shouldn't make any changes to it directly.
But the most important thing to point out is that this file contains an init() function which, when called, initializes a new message catalog containing all our translations and mappings. It then sets this as the default message catalog by assigning it to the message.DefaultCatalog global variable.
When we call one of the message.Printer functions, the printer will lookup the relevant translation from the default message catalog for printing. This is really nice, because it means that all our translations are stored in memory at runtime, and any lookups are very fast and efficient.
So, if we take a step back for a moment, we can see that the gotext update command that we're using with go generate actually does two things. One — it walks the code in our cmd/www application and extracts the necessary strings for translation into the out.gotext.json files; and two — it also parses any messages.gotext.json files (if present) and updates the message catalog accordingly.
The final step in getting this working is to import the internal/translations package in our cmd/www/handlers.go file. This will ensure that the init() function in internal/translations/translations.go is called, and the default message catalog is updated to be the one containing our translations. Because we won't actually be referencing anything in the internal/translations package directly, we'll need to alias the import path to the blank identifer _ to prevent the Go compiler from complaining.
Go ahead and do that now:
File: cmd/www/handlers.go package main
import ( "net/http"
// Import the internal/translations package, so that its init()
// function is called.
_ "bookstore.example.com/internal/translations"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
func handleHome(w http.ResponseWriter, r *http.Request) { locale := r.URL.Query().Get(":locale")
var lang language.Tag
switch locale {
case "en-gb":
lang = language.MustParse("en-GB")
case "de-de":
lang = language.MustParse("de-DE")
case "fr-ch":
lang = language.MustParse("fr-CH")
default:
http.NotFound(w, r)
return
}
p := message.NewPrinter(lang)
p.Fprintf(w, "Welcome!\n")
} Alright, let's try this out! When your restart the application and try making some requests, you should now see the "Welcome!" message translated into the appropriate language.
$ curl localhost:4018/en-GB Welcome!
$ curl localhost:4018/de-de Willkommen!
$ curl localhost:4018/fr-ch Bienvenue ! Using variables in translations Now that we've got the basic translations working in our application, let's move on to something a bit more advanced and look at how to manage translations with interpolated variables in them.
To demonstrate, we'll update the HTTP response from our handleHome() function to include a "{N} books available" line, where {N} is an integer containing the number of books in our imaginary bookstore.
File: cmd/www/handlers.go package main
...
func handleHome(w http.ResponseWriter, r *http.Request) { locale := r.URL.Query().Get(":locale")
var lang language.Tag
switch locale {
case "en-gb":
lang = language.MustParse("en-GB")
case "de-de":
lang = language.MustParse("de-DE")
case "fr-ch":
lang = language.MustParse("fr-CH")
default:
http.NotFound(w, r)
return
}
// Define a variable to hold the number of books. In a real application
// this would probably be retrieved by making a database query or
// something similar.
var totalBookCount = 1_252_794
p := message.NewPrinter(lang)
p.Fprintf(w, "Welcome!\n")
// Use the Fprintf() function to include the new message in the HTTP
// response, with the book count as in interpolated integer value.
p.Fprintf(w, "%d books available\n", totalBookCount)
} Save the changes, then use go generate to output some new out.gotext.json files. You should see warning messages for the new missing translations like so:
$ go generate ./internal/translations/translations.go de-DE: Missing entry for "{TotalBookCount} books available". fr-CH: Missing entry for "{TotalBookCount} books available". Let's take a look at the de-DE/out.gotext.json file:
File: internal/translations/locales/de-DE/out.gotext.json { "language": "de-DE", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "Willkommen!" }, { "id": "{TotalBookCount} books available", "message": "{TotalBookCount} books available", "translation": "", "placeholders": [ { "id": "TotalBookCount", "string": "%[1]d", "type": "int", "underlyingType": "int", "argNum": 1, "expr": "totalBookCount" } ] } ] } The first thing to point out here is that the translation for our "Welcome!" message has been persisted across the workflow and is already present in the out.gotext.json file. This is obviously really important, because it means that when we send the file to the translator they won't need to provide the translation again.
The second thing is that there is now an entry for our new message. We can see that this has the form "{TotalBookCount} books available", with the (capitalized) variable name from our Go code being used as the placeholder parameter. You should keep this in mind when writing your code, and try to use sensible and descriptive variable names that will make sense to your translators. The placeholders array also provides additional information about each placeholder value, the most useful part probably being the type value (which in this case tells the translator that the TotalBookCount value is an integer).
So the next step is to send these new out.gotext.json files off to a translator for translation. Again, we'll simulate that here by copying them to messages.gotext.json files and adding the translations like so:
$ cp internal/translations/locales/de-DE/out.gotext.json internal/translations/locales/de-DE/messages.gotext.json $ cp internal/translations/locales/fr-CH/out.gotext.json internal/translations/locales/fr-CH/messages.gotext.json File: internal/translations/locales/de-DE/messages.gotext.json { "language": "de-DE", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "Willkommen!" }, { "id": "{TotalBookCount} books available", "message": "{TotalBookCount} books available", "translation": "{TotalBookCount} Bücher erhältlich", "placeholders": [ { "id": "TotalBookCount", "string": "%[1]d", "type": "int", "underlyingType": "int", "argNum": 1, "expr": "totalBookCount" } ] } ] } File: internal/translations/locales/fr-CH/messages.gotext.json { "language": "fr-CH", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "Bienvenue !" }, { "id": "{TotalBookCount} books available", "message": "{TotalBookCount} books available", "translation": "{TotalBookCount} livres disponibles", "placeholders": [ { "id": "TotalBookCount", "string": "%[1]d", "type": "int", "underlyingType": "int", "argNum": 1, "expr": "totalBookCount" } ] } ] } Make sure that both messages.gotext.json files are saved, and then run go generate to update our message catalog. This should run without any warnings.
$ go generate ./internal/translations/translations.go When you restart the cmd/www application and make some HTTP requests again, you should now see the new translated messages like so:
$ curl localhost:4018/en-GB Welcome! 1,252,794 books available
$ curl localhost:4018/de-de Willkommen! 1.252.794 Bücher erhältlich
$ curl localhost:4018/fr-ch Bienvenue ! 1 252 794 livres disponibles Now this is really cool. As we'll as the translations being applied by our message.Printer, it's also smart enough to output the interpolated integer value with the correct number formatting for each language. We can see here that our en-GB locale uses the "," character as a thousands separator, whereas de-DE uses "." and fr-CH uses the whitespace " ". A similar thing is done for decimal separators too.
Dealing with pluralization This is working nicely, but what happens if there is only 1 book available in our bookstore? Let's update the handleHome() function so that the totalBookCount value is 1:
File: cmd/www/handlers.go package main
...
func handleHome(w http.ResponseWriter, r *http.Request) { locale := r.URL.Query().Get(":locale")
var lang language.Tag
switch locale {
case "en-gb":
lang = language.MustParse("en-GB")
case "de-de":
lang = language.MustParse("de-DE")
case "fr-ch":
lang = language.MustParse("fr-CH")
default:
http.NotFound(w, r)
return
}
// Set the total book count to 1.
var totalBookCount = 1
p := message.NewPrinter(lang)
p.Fprintf(w, "Welcome!\n")
p.Fprintf(w, "%d books available\n", totalBookCount)
} (I know this is a bit of a tenuous example, but it helps illustrate Go's pluralization functionality without much extra code, so bear with me!)
You can probably imagine what happens when we restart the application and make a request to localhost:4018/en-gb now.
$ curl localhost:4018/en-gb Welcome! 1 books available That's right, we see the message "1 books available", which isn't correct English because of the plural noun books. It would be better if this message read 1 book available or — even better — One book available instead.
Happily, it's possible for us to specify alternative translations based on the value of an interpolated variable in our messages.gotext.json files.
Let's start by demonstrating this for our en-GB locale. If you're following along, copy the en-GB/out.gotext.json file to en-GB/messages.gotext.json:
$ cp internal/translations/locales/en-GB/out.gotext.json internal/translations/locales/en-GB/messages.gotext.json And then update it like so:
File: internal/translations/locales/en-GB/messages.gotext.json { "language": "en-GB", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "Welcome!", "translatorComment": "Copied from source.", "fuzzy": true }, { "id": "{TotalBookCount} books available", "message": "{TotalBookCount} books available", "translation": { "select": { "feature": "plural", "arg": "TotalBookCount", "cases": { "=1": { "msg": "One book available" }, "other": { "msg": "{TotalBookCount} books available" } } } }, "placeholders": [ { "id": "TotalBookCount", "string": "%[1]d", "type": "int", "underlyingType": "int", "argNum": 1, "expr": "totalBookCount" } ] } ] } Now, rather than the translation value being a simple string we have set it to a JSON object that instructs the message catalog to use different translations depending on the value of the TotalBookCount placeholder. The key part here is the cases value, which contains the translations to use for different values of the placeholder. The supported case rules are:
Case Description "=x" Where x is an integer that equals the value of the placeholder "<x" Where x is an integer that is larger than the value of the placeholder "other" All other cases (a bit like default in a Go switch statement) Note: If you look at the documentation for the golang.org/x/text/feature/plural package (which is what gotext uses behind the scenes when generating the message catalog), you'll see that it also mentions the case rules "zero", "one", "two", "few", and "many". However, these rules aren't supported for all possible target languages, and you may get an error like gotext: generation failed: error: plural: form "many" not supported for language "de-DE" if you try to use them. It seems to be safer to stick with the three case rules in the table above. Additionally, it's important to be aware that the range of allowed values for x in the "=x" and "<x" case rules is 0 to 32767. Trying to use something outside of that range will result in an error. There's an open issue about these behaviors here. Let's complete work this by updating the messages.gotext.json files for our de-DE and fr-CH languages to include the appropriate pluralized variations, like so:
File: internal/translations/locales/de-DE/messages.gotext.json { "language": "de-DE", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "Willkommen!" }, { "id": "{TotalBookCount} books available", "message": "{TotalBookCount} books available", "translation": { "select": { "feature": "plural", "arg": "TotalBookCount", "cases": { "=1": { "msg": "Ein Buch erhältlich" }, "other": { "msg": "{TotalBookCount} Bücher erhältlich" } } } }, "placeholders": [ { "id": "TotalBookCount", "string": "%[1]d", "type": "int", "underlyingType": "int", "argNum": 1, "expr": "totalBookCount" } ] } ] } File: internal/translations/locales/fr-CH/messages.gotext.json { "language": "fr-CH", "messages": [ { "id": "Welcome!", "message": "Welcome!", "translation": "Bienvenue !" }, { "id": "{TotalBookCount} books available", "message": "{TotalBookCount} books available", "translation": { "select": { "feature": "plural", "arg": "TotalBookCount", "cases": { "=1": { "msg": "Un livre disponible" }, "other": { "msg": "{TotalBookCount} livres disponibles" } } } }, "placeholders": [ { "id": "TotalBookCount", "string": "%[1]d", "type": "int", "underlyingType": "int", "argNum": 1, "expr": "totalBookCount" } ] } ] } Once those files are saved, use go generate again to update the message catalog:
$ go generate ./internal/translations/translations.go And if you restart the web application and make some HTTP requests, you should now see the appropriate message for 1 book:
$ curl localhost:4018/en-GB Welcome! One book available
$ curl localhost:4018/de-de Willkommen! Ein Buch erhältlich
$ curl localhost:4018/fr-ch Bienvenue ! Un livre disponible If you like, you can revert the totalBookCount variable back to a larger number...
File: cmd/www/handlers.go package main
...
func handleHome(w http.ResponseWriter, r *http.Request) { ...
// Revert the total book count.
var totalBookCount = 1_252_794
p := message.NewPrinter(lang)
p.Fprintf(w, "Welcome!\n")
p.Fprintf(w, "%d books available\n", totalBookCount)
} And when you restart the application and make another request, you should see the "other" version of our message:
$ curl localhost:4018/de-de Willkommen! 1.252.794 Bücher erhältlich Creating a localizer abstraction In the final part of this article we're going to create a new internal/localizer package which abstracts all our code for dealing with languages, printers and translations.
If you're following along, go ahead and create a new internal/localizer directory containing a localizer.go file.
$ mkdir -p internal/localizer $ touch internal/localizer/localizer.go At this point, your project structure should look like this:
. ├── cmd │ └── www │ ├── handlers.go │ └── main.go ├── go.mod ├── go.sum └── internal ├── localizer │ └── localizer.go └── translations ├── catalog.go ├── locales │ ├── de-DE │ │ ├── messages.gotext.json │ │ └── out.gotext.json │ ├── en-GB │ │ ├── messages.gotext.json │ │ └── out.gotext.json │ └── fr-CH │ ├── messages.gotext.json │ └── out.gotext.json └── translations.go And then add the following code to the new localizer.go file:
File: internal/localizer/localizer.go package localizer
import ( // Import the internal/translations so that it's init() function // is run. It's really important that we do this here so that the // default message catalog is updated to use our translations // before we initialize the message.Printer instances below. _ "bookstore.example.com/internal/translations"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
// Define a Localizer type which stores the relevant locale ID (as used // in our URLs) and a (deliberately unexported) message.Printer instance // for the locale. type Localizer struct { ID string printer *message.Printer }
// Initialize a slice which holds the initialized Localizer types for // each of our supported locales. var locales = []Localizer{ { // Germany ID: "de-de", printer: message.NewPrinter(language.MustParse("de-DE")), }, { // Switzerland (French speaking) ID: "fr-ch", printer: message.NewPrinter(language.MustParse("fr-CH")), }, { // United Kingdom ID: "en-gb", printer: message.NewPrinter(language.MustParse("en-GB")), }, }
// The Get() function accepts a locale ID and returns the corresponding
// Localizer for that locale. If the locale ID is not supported then
// this returns false
as the second return value.
func Get(id string) (Localizer, bool) {
for _, locale := range locales {
if id == locale.ID {
return locale, true
}
}
return Localizer{}, false
}
// We also add a Translate() method to the Localizer type. This acts // as a wrapper around the unexported message.Printer's Sprintf() // function and returns the appropriate translation for the given // message and arguments. func (l Localizer) Translate(key message.Reference, args ...interface{}) string { return l.printer.Sprintf(key, args...) } Note: Notice here that we're initializing a single message.Printer for each locale at startup, and these will be used concurrently by our web application handlers. Although the golang.org/x/text/message documentation doesn't say that message.Printer is safe for concurrent use, I checked with Marcel van Lohuizen (the lead developer of the golang.org/x/text packages) and he confirmed that message.Printer is intended to be used concurrently and is concurrency safe (so long as access to any write destination is synchronized). Next let's update the cmd/www/handlers.go file to use our new Localizer type, and — while we're at it — let's also make our handleHome() function render an additional "Launching soon!" message.
File: cmd/www/handlers.go package main
import ( "fmt" // New import "net/http"
"bookstore.example.com/internal/localizer" // New import
)
func handleHome(w http.ResponseWriter, r *http.Request) { // Initialize a new Localizer based on the locale ID in the URL. l, ok := localizer.Get(r.URL.Query().Get(":locale")) if !ok { http.NotFound(w, r) return }
var totalBookCount = 1_252_794
// Update these to use the new Translate() method.
fmt.Fprintln(w, l.Translate("Welcome!"))
fmt.Fprintln(w, l.Translate("%d books available", totalBookCount))
// Add an additional "Launching soon!" message.
fmt.Fprintln(w, l.Translate("Launching soon!"))
} It's worth pointing out that our use of the Translate() method here isn't just some syntactic sugar. You might remember earlier that I wrote the following warning:
It's critical to note when gotext walks your code it actually only looks for calls to message.Printer.Printf(), Fprintf() and Sprintf() — basically the three methods that end with an f. It ignores all other methods such as Sprint() or Println().
By having all our translations go through the Translate() method — which uses Sprintf() behind-the-scenes — we avoid the scenario where you accidentally use a method like Sprint() or Println() and gotext doesn't extract the message to the out.gotext.json files.
Let's try this out and run go generate again:
$ go generate ./internal/translations/translations.go de-DE: Missing entry for "Launching soon!". fr-CH: Missing entry for "Launching soon!". So this is really smart. We can see that gotext has been clever enough to walk our entire codebase and identify what strings need to be translated, even when we abstract the message.Printer.Sprintf() call to a helper function in a different package. This is awesome, and one of the things that I really appreciate about the gotext tool.
If you're following along, please go ahead and copy the out.gotext.json files to message.gotext.json files, and add the necessary translations for the new "Launching soon!" message. Then remember to run go generate again and restart the web application.
When you make some HTTP requests again now, your responses should look similar to this:
$ curl localhost:4018/en-gb Welcome! 1,252,794 books available Launching soon!
$ curl localhost:4018/de-de Willkommen! 1.252.794 Bücher erhältlich Bald verfügbar!
$ curl localhost:4018/fr-ch Bienvenue ! 1 252 794 livres disponibles Bientôt disponible ! Additional information Conflicting routes At this start of this post I'd deliberately didn't recommending using httprouter, despite it being an excellent and popular router. This is because using a dynamic locale as the first part of a URL path is likely to result in conflicts with other application routes which don't require a locale prefix, like /static/css/main.css or /admin/login. The httprouter package doesn't allow conflicting routes, which makes using it awkward in this scenario. If you do want to use httprouter, or want to avoid conflicting routes in your application, you could pass the locale as a query string parameter instead like /category/travel?locale=gb.
If you enjoyed this article, you might like to check out my recommended tutorials list or check out my books Let's Go and Let's Go Further, which teach you everything you need to know about how to build professional production-ready web applications and APIs with Go.
Filed under:golang tutorial
Машинное обучение Golang
Вычисление простых статических свойств
Статистическое обучение это ветвь применения статистики которая связана с машинным обучением.
Машинное обучение, которое тесно связано с вычислительной статистикой, это часть информатики которая пробует изучить данные и на их основе делать предсказания о их поведении без специального программирования.
В этой статье, мы собираемся изучить как сосчитать базовые статистические свойства, такие как среднее значение, минимальное и максимальное значение примера, медианное значение, а также дисперсию примера. Эти значения дадут вам отличное понимание вашего примера без среёзного погружения в детали. Однако, общие значения которые описывает пример, могут легко обмануть вас, заставив вас поверить, что вы хорошо знаете пример, и без них.
Все эти статистические свойства, будут посчитаны в stats.go
, который будет представлен в пяти частях.
Каждая строка входного файла содержит одно число, которое значит, что входной файл читается построчно. Неправильный ввод будет проигнорирован без каких либо предупредительных сообщений.
Ввод будет сохранен в срезе, чтобы можно было использовать отдельную функцию для подсчета каждого свойства. Так же, увидим, значения среза будут отсортированны перед обработкой.
package main
import (
"bufio"
"flag"
"fmt"
"io"
"math"
"os"
"sort"
"strconv"
"strings"
)
func min(x []float64) float64 {
return x[0]
}
func max(x []float64) float64 {
return x[len(x)-1]
}
func meanValue(x []float64) float64 {
sum := float64(0)
for _, v := range x {
sum = sum + v
}
return sum / float64(len(x))
}
func medianValue(x []float64) float64 {
length := len(x)
if length%2 == 1 {
// Odd
return x[(length-1)/2]
} else {
// Even
return (x[length/2] + x[(length/2)-1]) / 2
}
return 0
}
func variance(x []float64) float64 {
mean := meanValue(x)
sum := float64(0)
for _, v := range x {
sum = sum + (v-mean)*(v-mean)
}
return sum / float64(len(x))
}
func main() {
flag.Parse()
if len(flag.Args()) == 0 {
fmt.Printf("usage: stats filename\n")
return
}
data := make([]float64, 0)
file := flag.Args()[0]
f, err := os.Open(file)
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
Регрессия
Регрессия это статистический метод для расчета связи между переменными. Эта часть реализует линейную регрессию, которая наиболее популяная и наиболее простая техника, а так же наилучший способ понять наши данные. Заметим, что регрессия не имеет 100% точность, даже если вы использвали многочлен высшего порядка(нелинейный). Цель регрессия, как и в большинстве ML техник - найти достаточно хорошую технику а не лучшую из лучших.
Линейная регрессия.
За этой регрессией прячется следующее: вы берете модель ваших данных используя уравнение первой степени. его можно представить как y = a x + b
.
Есть множество методов, которые позволяют найти уравнение первого порядка, которое представит модель ваших данных и вычислит a
и b
.
Реализация линейной регрессии.
Go код, этой части будет сохранен в regression.go
, который будет представлен в трех частях. Вывод программы будет два числа с плавающей запятой, которые определяют a
и b
для ураввнения первого порядка.
Первая часть regression.go
содержит следующий код:
package main
import (
"encoding/csv"
"flag"
"fmt"
"gonum.org/v1/gonum/stat"
"os"
"strconv"
)
type xy struct {
x []float64
y []float64
}
func main() {
flag.Parse()
if len(flag.Args()) == 0 {
fmt.Printf("usage: regression filename\n")
return
}
filename := flag.Args()[0]
file, err := os.Open(filename)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
r := csv.NewReader(file)
records, err := r.ReadAll()
if err != nil {
fmt.Println(err)
return
}
size := len(records)
data := xy{
x: make([]float64, size),
y: make([]float64, size),
}
for i, v := range records {
if len(v) != 2 {
fmt.Println("Expected two elements")
continue
}
if s, err := strconv.ParseFloat(v[0], 64); err == nil {
data.y[i] = s
}
if s, err := strconv.ParseFloat(v[1], 64); err == nil {
data.x[i] = s
}
}
b, a := stat.LinearRegression(data.x, data.y, nil, false)
fmt.Printf("%.4v x + %.4v\n", a, b)
fmt.Printf("a = %.4v b = %.4v\n", a, b)
}
Классификация
В статистике и ML, классификация это процесс помещения элементов в существующие наборы которые называются категориями. В ML классификация подразумевает обучение с учителем, в котором набор предполагает содержание верно определенные данные используемые для обучение перед работой с реальными данными.
Очень простой и легко реализуемая метод классификации называется "k-близжайших соседей"(k-NN). Идея метода такова, что мы можем отсортировать данные основываясь на их схожести с другими предметами. "k" в "k-NN" обозначает число соседей которые должны быть включены в решение, которое значит что k это положительное целое. которое довольно маленькое.
Ввод алгоритма состоит из k-близжайших примеров обучения, в свойстве пространства. Объект классифицируется множеством голосов его соседей, с объектом назначенным классу который в большинстве обобщен среду его k-NN. Если значение k = 1, значит элемент просто назначен классу, который ближе всего согласно расстоянию используемой метрики. Удаленная метрика зависит от данных с которыми работаете. Как пример вам нуно различное расстояние метрик, когда работает со сложными числами и другими, когда работаете в трехмерном пространстве.
package main
import (
"flag"
"fmt"
"strconv"
"github.com/sjwhitworth/golearn/base"
"github.com/sjwhitworth/golearn/evaluation"
"github.com/sjwhitworth/golearn/knn"
)
func main() {
flag.Parse()
if len(flag.Args()) < 2 {
fmt.Printf("usage: classify filename k\n")
return
}
dataset := flag.Args()[0]
rawData, err := base.ParseCSVToInstances(dataset, false)
if err != nil {
fmt.Println(err)
return
}
k, err := strconv.Atoi(flag.Args()[1])
if err != nil {
fmt.Println(err)
return
}
cls := knn.NewKnnClassifier("euclidean", "linear", k)
Метода knn.NewKnnClassifier()
возвращает новый классификатор. Последний параметтр функции это количество соседей которые классификатор будет иметь.
Последняя часть classify.go
показана ниже:
train, test := base.InstancesTrainTestSplit(rawData, 0.50)
cls.Fit(train)
p, err := cls.Predict(test)
if err != nil {
fmt.Println(err)
return
}
confusionMat, err := evaluation.GetConfusionMatrix(test, p)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(evaluation.GetSummary(confusionMat))
Работа с tensorflow
TensorFlow известная открытая платформа для ML. Для того чтобы использовать TensorFlow в Golang, нам нужно для начала скачать этот пакет.
$ go get github.com/tensorflow/tensorflow/tensorflow/go
Однако, для работы вышеупомянутой команды, интерфейс C в TensorFlow должны быть уже установленны. На macOS машине, его можно установить таким образом:
$ brew install tensorflow
Если C интерфейс не установлен, и вы хотите установить Go пакет для TensorFlow, вы получите следующую ошибку:
$ go get github.com/tensorflow/tensorflow/tensorflow/go
# github.com/tensorflow/tensorflow/tensorflow/go
ld: library not found for -ltensorflow clang: error: linker command failed with exit code 1 (use -v to see invocation)
Так как TensorFlow достаточно сложен, он может быть хорош для выполнения следующей команды для проверки установки:
$ go test github.com/tensorflow/tensorflow/tensorflow/go
ok github.com/tensorflow/tensorflow/tensorflow/go 0.109s
Теперь начнем с некоторого кода:
package main
import (
tf "github.com/tensorflow/tensorflow/tensorflow/go"
"github.com/tensorflow/tensorflow/tensorflow/go/op"
"fmt"
)
func main() {
s := op.NewScope()
c := op.Const(s, "Using TensorFlow version: " + tf.Version())
graph, err := s.Finalize()
if err != nil {
fmt.Println(err)
return
}
sess, err := tf.NewSession(graph, nil)
if err != nil {
fmt.Println(err)
return
}
output, err := sess.Run(nil, []tf.Output{c}, nil)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(output[0].Value())
}
Raspberry Pi Coding in Go: Traffic Lights
I’ve been learning the Go programming language recently. As an exercise, I decided to revisit a past article that looked at working with traffic lights on the Raspberry Pi in Python in order to rewrite it in Go. To make this a standalone guide, there will be some re-use of content from the prior article here. Since writing this article, I’ve also written up the same exercise using Swift (Swift version), Node.js (read about that here), Node RED (try here), Java (try here), Bash scripting (Bash article), C (check it out here), Rust, .NET/C# and also for Arduino.
Shopping List
To try this out, you will need the following (links here mostly go to Adafruit, UK customers may want to consider Pimoroni as a UK based alternative, Amazon has most if not all of this stuff too):
- A Raspberry Pi (I’ll use the Pi 3 Model B here, but any model with GPIO pins will work — if you want to use the Pi Zero you’ll need to solder some headers onto it). I’m going to assume you have a Pi 2 or 3 with 40 pins
- A power supply for your Pi (Raspberry Pi 4 requires a different USB C power supply)
- Some sort of case is probably a good idea to protect the Pi (but you’ll need to leave the lid off to expose the GPIO pins to connect your lights to)
- A Micro SD card to install your operating system on (or get one with the OS pre-installed). If you want to install the operating system yourself, you’ll need a Mac, PC, Linux machine with an SD card reader
- A set of traffic lights from Low Voltage Labs (the two pack is good value)
- Any USB keyboard to type on the Pi, you might want a mouse too
- Any HDMI display to show output from the Pi
Attaching the Traffic Lights
The Low Voltage Labs traffic lights connect to the Pi using four pins. One of these needs to be ground, the other three being actual GPIO pins used to control each of the individual LEDs.
Before powering up the Pi, attach the traffic lights so that the pins connect to the GPIO pins highlighted in red:
When you’re done it’s going to look something like this… (an easy way to make sure you have it right is to locate the lights on the left hand row of pins as you look at the Pi with the USB ports to the bottom, then count 8 pins up and attach the lights there).
Don’t turn the Pi on yet, you’ll need to prepare an operating system image for it first…
Operating System Setup
Install the Raspberry Pi OS which can be downloaded from the official Raspberry Pi site. You can also find an excellent installation guide there should you need help.
Once you’ve got the operating system installed, make sure you can login, and have a working wired or wifi internet connection.
Now you can go ahead and start turning lights on and off!
Installing Go
Go code can be compiled and distributed as a binary, it can also be cross compiled (where the compiler generates a binary to run on a different operating system / processor architecture than the one it was built on). We’ll look at both options here so will need to install the Go distribution on the Pi as it’s not included with Raspbian Lite.
Go installation is a simple matter of downloading the distribution (check here for latest ARM v6 version that the Pi uses), then expanding it into /usr/local:
$ wget https://storage.googleapis.com/golang/go1.10.1.linux-armv6l.tar.gz $ sudo tar -C /usr/local -xvf go1.10.1.linux-armv6l.tar.gz Amend your PATH by editing ~/.profile and adding the following at the bottom:
PATH=$PATH:/usr/local/go/bin Having saved your profile, source it to get the new value for path in your current terminal session:
$ . ~/.profile Clean up the downloaded archive to save space on the micro SD card:
$ rm go1.10.1.linux-armv6l.tar.gz Finally, verify Go was installed by checking its version:
$ go version
go version go1.10.1 linux/arm (Official Go installation instructions can be found here).
Installing Dependencies
We’ll also need git, which isn’t installed with Raspbian Lite but is simple to add:
$ sudo apt-get install git $ git --version git version 2.11.0 We’ll also use a Go package for accessing the GPIO pins on the Pi. This is installed with the go get command:
$ go get github.com/stianeikeland/go-rpio/... The package will be installed to:
~/go/src/github.com/stianeikeland/go-rpio If you’d prefer your Go code to live somewhere else on the filesystem, you’ll want to look at setting your GOPATH environment variable to specify an alternative location (see documentation).
Programming the Traffic Lights
Similarly, go get my example code that uses the go-rpio package to make the lights work:
$ go get github.com/simonprickett/gopitrafficlights/... We’ve now got everything we need to start seeing some action, so without further ado…
$ cd ~/go/src/github.com/simonprickett/gopitrafficlights $ go run main.go If the lights are connected to the correct GPIO pins, they should start to flash on and off in the UK traffic light pattern (red, red + amber, green, amber, red). If you don’t see anything, make sure that you have the lights connected to the right pins.
You can also compile the code to a binary file, then run it:
$ go build -o trafficlights $ ./trafficlights To exit, press Ctrl + C. This will cause all of the lights to turn off, and the program will exit.
How it Works
Here’s a brief walkthrough of the complete source code…
package main
import (
"fmt"
"github.com/stianeikeland/go-rpio"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
if err := rpio.Open(); err != nil {
fmt.Println(err)
os.Exit(1)
}
// Get the pin for each of the lights
redPin := rpio.Pin(9)
yellowPin := rpio.Pin(10)
greenPin := rpio.Pin(11)
// Set the pins to output mode
redPin.Output()
yellowPin.Output()
greenPin.Output()
// Clean up on ctrl-c and turn lights out
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
redPin.Low()
yellowPin.Low()
greenPin.Low()
os.Exit(0)
}()
defer rpio.Close()
// Turn lights off to start.
redPin.Low()
yellowPin.Low()
greenPin.Low()
// A while true loop.
for {
// Red
redPin.High()
time.Sleep(time.Second * 3)
// Red and yellow
yellowPin.High()
time.Sleep(time.Second)
// Green
redPin.Low()
yellowPin.Low()
greenPin.High()
time.Sleep(time.Second * 5)
// Yellow
greenPin.Low()
yellowPin.High()
time.Sleep(time.Second * 2)
// Yellow off
yellowPin.Low()
}
}
view rawmain.go hosted with ❤ by GitHub The program checks that it can open the Pi’s GPIO pins at line 13. If it can’t, it will exit. Assuming that’s successful lines 19–26 assign GPIO pins 9, 10 and 11 to more meaningful variable names and tell the Pi to use them as output pins.
Lines 28–39 set up a channel for the SIGTERM signal… this signal is sent to the program whenever the user gets bored of watching the lights and hits Ctrl+C. The program sets up a channel that will be notified when the termination signal occurs, then runs a goroutine at line 31. The goroutine runs concurrently with the rest of the program, and waits for a message to be sent to the channel. When it receives one, the program is attempting to exit because Ctrl+C was pressed. The code in the goroutine then turns off all of the lights and exits cleanly.
As part of cleanup we also free up resources associated with the GPIO pins at line 39, using the defer keyword to ensure that it will happen whenever the program exits.
Each light turns on when its associated pin it set high, and off when set low. Lines 42–44 make sure that all the lights are off to begin with, just in case something else was using the GPIO pins before and left them on.
At line 47, the program enters an infinite loop in which it turns the lights on .High() and off .Low() in the right sequence for a traffic light. In between phases, “time.Sleep” pauses execution.
Cross Compiling
One of the features of the Go toolset is that it allows you to cross compile code to binary executables. This means that you can create a binary for a platform other than the one you’re compiling the code on.
Go makes this very simple: for example I can compile the traffic lights example code on my Intel Mac OS computer and output a binary that will run on the ARM based Raspberry Pi running Linux. This is simply a matter of setting some environment variables when compiling:
$ go get github.com/stianeikeland/go-rpio/... $ go get github.com/simonprickett/gopitrafficlights/... $ cd ~/go/src/github.com/simonprickett/gopitrafficlights $ env GOOS=linux GOARCH=arm GOARM=7 go build -o trafficlights (For a list of possible values for GOOS and GOARCH, see Installing Go from Source)
The resulting trafficlights binary will work on a Raspberry Pi 3, but not on the Mac OS machine that compiled it. If you have an older model Pi you may need to set GOARM to 6. To try it out, FTP the compiled binary trafficlights over to the Pi then start it up with:
$ ./trafficlights You should see the lights work as before. I’ve put the source code on GitHub for your enjoyment.
Building a WebRTC video and audio Broadcaster in Golang using ION-SFU, and media devices
Gabriel Tanne Gabriel Tanner
Building a WebRTC video and audio Broadcaster in Golang using ION-SFU, and media devices
Table of Contents
In this tutorial, you will build a video broadcasting application that reads the camera in Golang and sends it to the ION-SFU (Selective forwarding unit) which allows WebRTC sessions to scale more efficiently.
WebRTC, short for Web Real-Time Communication, is a communication protocol that enables real-time audio, video and data transmission on the web by utilizing peer to peer connections.
WebRTC also provides a Javascript API that is available by default in most browsers and helps developers implement the protocol in their applications. But there are also some implementations of the WebRTC protocol in other languages.
In this tutorial, you will build a video broadcasting application that reads the camera in Golang and sends it to the ION-SFU (Selective forwarding unit) which allows WebRTC sessions to scale more efficiently.
The application will also feature a small frontend that lets you watch the video you published by reading it from the ION-SFU server.
Prerequisites
Before you begin this guide, you’ll need the following:
A valid Golang installation.
Camera connected to your computer that can be read using Video for Linux as a source for the video stream.
(Optional) If you want to connect with devices that are not on your network you will need to add a TURN server to your application. If you want to know more about TURN and how to set up your own check out this article.
Technology Stack
Now that you have an overview of what you are going to build let's take a closer look at the tools in use and how they work with each other.
Let's break the different components down:
Pion - Pure Golang implementation of the WebRTC protocol. Used to establish a peer connection to ION-SFU and send the video stream.
ION SFU - ION SFU (Selective Forwarding Unit) is a video routing service that allows Webrtc sessions to scale more efficiently.
Pion mediadevices - Golang implementation of the Mediadevices API which is used to read the camera as a Mediastream that can be sent using the peer connection.
One main benefit of this is that you can read the camera without the need to open a browser tab. Using a selective forwarding unit will also help a lot with performance and scaling the application for a large size of users.
This article assumes a basic knowledge of WebRTC. If you do not have any previous experience, I recommend reading the free book WebRTC for the curious.
Setting up ION-SFU
In this section, you will clone and configure the ION-SFU server so that you can use it with your application.
First, you will clone the repository so you have all the resources needed to start setting up your selective forwarding unit:
git clone --branch v1.10.6 https://github.com/pion/ion-sfu.git
This command will clone the ION-SFU repository from Github and create a folder with the name of ion-sfu in your directory. Now enter the directory using the following command:
cd ion-sfu
Next you can edit the configuration of the sfu by changing the config.toml file. The standard configurations are fine for testing and local use but I would recommend adding a STUN and TURN server if you try to access the server from a device in another network.
If you are not sure how to create a TURN server I would recommend reading this guide.
Once you are done with the configuration you can start the server using the following command:
go build ./cmd/signal/json-rpc/main.go && ./main -c config.toml
Alternatively you can also start the server using Docker if you prefer that over starting it using Golang.
docker run -p 7000:7000 -p 5000-5020:5000-5020/udp pionwebrtc/ion-sfu:v1.10.6-jsonrpc
You have now successfully set up your ION-SFU server and should see the following output in the console.
config config.toml load ok!
[2020-10-12 19:04:19.017] [INFO] [376][main.go][main] => --- Starting SFU Node ---
[2020-10-12 19:04:19.018] [INFO] [410][main.go][main] => Listening at http://[:7000]
Creating the project
Now that the setup and configuration of the ion-sfu server are done it is time to create the project
First, you will need to create a directory and enter it.
mkdir mediadevice-broadcast && cd mediadevice-broadcast
After that you can continue by creating all the files needed for the project using the following command:
mkdir public
touch main.go public/index.html public/index.js public/style.css
There are also two packages that need to be installed to follow this article.
sudo apt-get install -y v4l-utils
sudo apt-get install -y libvpx-dev
If you are not on Linux you might need to download different packages. Look at the media devices documentation for more information.
Establishing a WebRTC connection
Before any data can be exchanged using WebRTC, there must first be an established peer-to-peer connection between two WebRTC agents. Since the peer-to-peer connection often cannot be established directly there needs to be some signaling method.
Signaling to the ion-sfu will be handled over the Websockets protocol. For that, we will implement a simple Websockets boilerplate using the gorilla/websocket library that connects to the Websockets server and allows us to receive the incoming message and send our own.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/url"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
var addr string
func main() {
flag.StringVar(&addr, "a", "localhost:7000", "address to use")
flag.Parse()
u := url.URL{Scheme: "ws", Host: addr, Path: "/ws"}
log.Printf("connecting to %s", u.String())
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
log.Fatal("dial:", err)
}
defer c.Close()
// Read incoming Websocket messages
done := make(chan struct{})
go readMessage(c, done)
<-done
}
func readMessage(connection *websocket.Conn, done chan struct{}) {
defer close(done)
for {
_, message, err := connection.ReadMessage()
if err != nil || err == io.EOF {
log.Fatal("Error reading: ", err)
break
}
fmt.Printf("recv: %s", message)
}
}
Now let's walk through the code for better understanding:
The flag is used to dynamically provide the URL of the Websockets server when starting the script and has a standard value of localhost:7000
The URL is used to create a Websockets client using the Dial method. Then we check if the connection resulted in an error and print a log if that is the case.
The readMessage function then reads the incoming messages by calling ReadMessage() on the Websocket connection and is run as a Go routine so it doesn't block the main thread and can run in the background.
The last line of the main() function makes sure that the script runs as long as the done variable is not closed.
The next step is creating a peer connection to the ion-sfu and handling the incoming WebRTC signaling events.
var peerConnection *webrtc.PeerConnection
func main() {
...
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
/*{
URLs: []string{"turn:TURN_IP:3478?transport=tcp"},
Username: "username",
Credential: "password",
},*/
},
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
}
// Create a new RTCPeerConnection
mediaEngine := webrtc.MediaEngine{}
vpxParams, err := vpx.NewVP8Params()
if err != nil {
panic(err)
}
vpxParams.BitRate = 500_000 // 500kbps
codecSelector := mediadevices.NewCodecSelector(
mediadevices.WithVideoEncoders(&vpxParams),
)
codecSelector.Populate(&mediaEngine)
api := webrtc.NewAPI(webrtc.WithMediaEngine(mediaEngine))
peerConnection, err = api.NewPeerConnection(config)
if err != nil {
panic(err)
}
}
Here we first create a WebRTC config where we define our STUN and TURN server that will be used in the signaling process. After, that we create a MediaEngine that lets us define the codecs supported by the peer connection.
With all that configuration done we can create a new peer connection by calling the NewPeerConnection function on the WebRTC API we just created.
Before sending the offer to the ion-sfu server over Websockets we first need to add the video and audio stream. This is where the media device library comes into play to read the video from the camera.
fmt.Println(mediadevices.EnumerateDevices())
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
c.Width = prop.Int(640)
c.Height = prop.Int(480)
},
Codec: codecSelector,
})
if err != nil {
panic(err)
}
for _, track := range s.GetTracks() {
track.OnEnded(func(err error) {
fmt.Printf("Track (ID: %s) ended with error: %v\n",
track.ID(), err)
})
_, err = peerConnection.AddTransceiverFromTrack(track,
webrtc.RtpTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly,
},
)
if err != nil {
panic(err)
}
}
Once an instance of the media devices library is created using the peer connection you can get the user media using the GetUserMedia function and passing the parameters.
One configuration change you might need to make is altering the FrameFormat to support your connected camera. You can check the frame format of your camera with the following command:
v4l2-ctl --all
All supported formats can also be found in the media devices Github repository.
The offer can now be created and saved into the local description of the peer connection.
// Creating WebRTC offer
offer, err := peerConnection.CreateOffer(nil)
// Set the remote SessionDescription
err = peerConnection.SetLocalDescription(offer)
if err != nil {
panic(err)
}
The next step is to send the offer over to the sfu using Websockets. The Websockets message is JSON and needs a specific structure to be recognized by the sfu.
Therefore we need to create a struct holding our offer and the required sid that specifies the room we want to join that we can then convert into JSON.
type SendOffer struct {
SID string `json:sid`
Offer *webrtc.SessionDescription `json:offer`
}
Now we convert our offer object into JSON using the json.Marshal() function and then use the JSON offer object as a parameter in the request.
After converting the request to a byte array the message can finally be send over Websockets using the WriteMessage() function.
offerJSON, err := json.Marshal(&SendOffer{
Offer: peerConnection.LocalDescription(),
SID: "test room",
})
params := (*json.RawMessage)(&offerJSON)
connectionUUID := uuid.New()
connectionID = uint64(connectionUUID.ID())
offerMessage := &jsonrpc2.Request{
Method: "join",
Params: params,
ID: jsonrpc2.ID{
IsString: false,
Str: "",
Num: connectionID,
},
}
reqBodyBytes := new(bytes.Buffer)
json.NewEncoder(reqBodyBytes).Encode(offerMessage)
messageBytes := reqBodyBytes.Bytes()
c.WriteMessage(websocket.TextMessage, messageBytes)
Now that the offer is sent we need to correctly respond to the WebRTC events and the response from the Websockets server.
The OnICECandidate event is called whenever a new ICE candidate is found. The method is then used to negotiate a connection with the remote peer by sending a trickle request to the sfu.
// Handling OnICECandidate event
peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
if candidate != nil {
candidateJSON, err := json.Marshal(&Candidate{
Candidate: candidate,
Target: 0,
})
params := (*json.RawMessage)(&candidateJSON)
if err != nil {
log.Fatal(err)
}
message := &jsonrpc2.Request{
Method: "trickle",
Params: params,
}
reqBodyBytes := new(bytes.Buffer)
json.NewEncoder(reqBodyBytes).Encode(message)
messageBytes := reqBodyBytes.Bytes()
c.WriteMessage(websocket.TextMessage, messageBytes)
}
})
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed to %s \n", connectionState.String())
})
The readMessage function created earlier is used to receive and react to the incoming Websockets messages send by the sfu.
For that we first need to create the structs that contain the received messages so we can use the data. Then we will determine which event the message is for and handle them accordingly.
// SendAnswer object to send to the sfu over Websockets
type SendAnswer struct {
SID string `json:sid`
Answer *webrtc.SessionDescription `json:answer`
}
type ResponseCandidate struct {
Target int `json:"target"`
Candidate *webrtc.ICECandidateInit `json:candidate`
}
// TrickleResponse received from the sfu server
type TrickleResponse struct {
Params ResponseCandidate `json:params`
Method string `json:method`
}
// Response received from the sfu over Websockets
type Response struct {
Params *webrtc.SessionDescription `json:params`
Result *webrtc.SessionDescription `json:result`
Method string `json:method`
Id uint64 `json:id`
}
func readMessage(connection *websocket.Conn, done chan struct{}) {
defer close(done)
for {
_, message, err := connection.ReadMessage()
if err != nil || err == io.EOF {
log.Fatal("Error reading: ", err)
break
}
fmt.Printf("recv: %s", message)
var response Response
json.Unmarshal(message, &response)
if response.Id == connectionID {
result := *response.Result
remoteDescription = response.Result
if err := peerConnection.SetRemoteDescription(result); err != nil {
log.Fatal(err)
}
} else if response.Id != 0 && response.Method == "offer" {
peerConnection.SetRemoteDescription(*response.Params)
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
log.Fatal(err)
}
peerConnection.SetLocalDescription(answer)
connectionUUID := uuid.New()
connectionID = uint64(connectionUUID.ID())
offerJSON, err := json.Marshal(&SendAnswer{
Answer: peerConnection.LocalDescription(),
SID: "test room",
})
params := (*json.RawMessage)(&offerJSON)
answerMessage := &jsonrpc2.Request{
Method: "answer",
Params: params,
ID: jsonrpc2.ID{
IsString: false,
Str: "",
Num: connectionID,
},
}
reqBodyBytes := new(bytes.Buffer)
json.NewEncoder(reqBodyBytes).Encode(answerMessage)
messageBytes := reqBodyBytes.Bytes()
connection.WriteMessage(websocket.TextMessage, messageBytes)
} else if response.Method == "trickle" {
var trickleResponse TrickleResponse
if err := json.Unmarshal(message, &trickleResponse); err != nil {
log.Fatal(err)
}
err := peerConnection.AddICECandidate(*trickleResponse.Params.Candidate)
if err != nil {
log.Fatal(err)
}
}
}
}
As you can see we are handling two different events:
Offer - The sfu sends an offer and we react by saving the send offer into the remote description of our peer connection and sending back an answer with the local description so we can connect to the remote peer.
Trickle - The sfu sends a new ICE candidate and we add it to the peer connection
All this configuration will result in the following file:
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/url"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/codec/vpx"
"github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v3"
"github.com/sourcegraph/jsonrpc2"
// Note: If you don't have a camera or microphone or your adapters are not supported,
// you can always swap your adapters with our dummy adapters below.
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
// _ "github.com/pion/mediadevices/pkg/driver/audiotest"
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
_ "github.com/pion/mediadevices/pkg/driver/microphone" // This is required to register microphone adapter
)
type Candidate struct {
Target int `json:"target"`
Candidate *webrtc.ICECandidate `json:candidate`
}
type ResponseCandidate struct {
Target int `json:"target"`
Candidate *webrtc.ICECandidateInit `json:candidate`
}
// SendOffer object to send to the sfu over Websockets
type SendOffer struct {
SID string `json:sid`
Offer *webrtc.SessionDescription `json:offer`
}
// SendAnswer object to send to the sfu over Websockets
type SendAnswer struct {
SID string `json:sid`
Answer *webrtc.SessionDescription `json:answer`
}
// TrickleResponse received from the sfu server
type TrickleResponse struct {
Params ResponseCandidate `json:params`
Method string `json:method`
}
// Response received from the sfu over Websockets
type Response struct {
Params *webrtc.SessionDescription `json:params`
Result *webrtc.SessionDescription `json:result`
Method string `json:method`
Id uint64 `json:id`
}
var peerConnection *webrtc.PeerConnection
var connectionID uint64
var remoteDescription *webrtc.SessionDescription
var addr string
func main() {
flag.StringVar(&addr, "a", "localhost:7000", "address to use")
flag.Parse()
u := url.URL{Scheme: "ws", Host: addr, Path: "/ws"}
log.Printf("connecting to %s", u.String())
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
log.Fatal("dial:", err)
}
defer c.Close()
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
/*{
URLs: []string{"turn:TURN_IP:3478"},
Username: "username",
Credential: "password",
},*/
},
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
}
// Create a new RTCPeerConnection
mediaEngine := webrtc.MediaEngine{}
vpxParams, err := vpx.NewVP8Params()
if err != nil {
panic(err)
}
vpxParams.BitRate = 500_000 // 500kbps
codecSelector := mediadevices.NewCodecSelector(
mediadevices.WithVideoEncoders(&vpxParams),
)
codecSelector.Populate(&mediaEngine)
api := webrtc.NewAPI(webrtc.WithMediaEngine(&mediaEngine))
peerConnection, err = api.NewPeerConnection(config)
if err != nil {
panic(err)
}
// Read incoming Websocket messages
done := make(chan struct{})
go readMessage(c, done)
fmt.Println(mediadevices.EnumerateDevices())
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
c.Width = prop.Int(640)
c.Height = prop.Int(480)
},
Codec: codecSelector,
})
if err != nil {
panic(err)
}
for _, track := range s.GetTracks() {
track.OnEnded(func(err error) {
fmt.Printf("Track (ID: %s) ended with error: %v\n",
track.ID(), err)
})
_, err = peerConnection.AddTransceiverFromTrack(track,
webrtc.RtpTransceiverInit{
Direction: webrtc.RTPTransceiverDirectionSendonly,
},
)
if err != nil {
panic(err)
}
}
// Creating WebRTC offer
offer, err := peerConnection.CreateOffer(nil)
// Set the remote SessionDescription
err = peerConnection.SetLocalDescription(offer)
if err != nil {
panic(err)
}
// Handling OnICECandidate event
peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) {
if candidate != nil {
candidateJSON, err := json.Marshal(&Candidate{
Candidate: candidate,
Target: 0,
})
params := (*json.RawMessage)(&candidateJSON)
if err != nil {
log.Fatal(err)
}
message := &jsonrpc2.Request{
Method: "trickle",
Params: params,
}
reqBodyBytes := new(bytes.Buffer)
json.NewEncoder(reqBodyBytes).Encode(message)
messageBytes := reqBodyBytes.Bytes()
c.WriteMessage(websocket.TextMessage, messageBytes)
}
})
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("Connection State has changed to %s \n", connectionState.String())
})
offerJSON, err := json.Marshal(&SendOffer{
Offer: peerConnection.LocalDescription(),
SID: "test room",
})
params := (*json.RawMessage)(&offerJSON)
connectionUUID := uuid.New()
connectionID = uint64(connectionUUID.ID())
offerMessage := &jsonrpc2.Request{
Method: "join",
Params: params,
ID: jsonrpc2.ID{
IsString: false,
Str: "",
Num: connectionID,
},
}
reqBodyBytes := new(bytes.Buffer)
json.NewEncoder(reqBodyBytes).Encode(offerMessage)
messageBytes := reqBodyBytes.Bytes()
c.WriteMessage(websocket.TextMessage, messageBytes)
<-done
}
func readMessage(connection *websocket.Conn, done chan struct{}) {
defer close(done)
for {
_, message, err := connection.ReadMessage()
if err != nil || err == io.EOF {
log.Fatal("Error reading: ", err)
break
}
fmt.Printf("recv: %s", message)
var response Response
json.Unmarshal(message, &response)
if response.Id == connectionID {
result := *response.Result
remoteDescription = response.Result
if err := peerConnection.SetRemoteDescription(result); err != nil {
log.Fatal(err)
}
} else if response.Id != 0 && response.Method == "offer" {
peerConnection.SetRemoteDescription(*response.Params)
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
log.Fatal(err)
}
peerConnection.SetLocalDescription(answer)
connectionUUID := uuid.New()
connectionID = uint64(connectionUUID.ID())
offerJSON, err := json.Marshal(&SendAnswer{
Answer: peerConnection.LocalDescription(),
SID: "test room",
})
params := (*json.RawMessage)(&offerJSON)
answerMessage := &jsonrpc2.Request{
Method: "answer",
Params: params,
ID: jsonrpc2.ID{
IsString: false,
Str: "",
Num: connectionID,
},
}
reqBodyBytes := new(bytes.Buffer)
json.NewEncoder(reqBodyBytes).Encode(answerMessage)
messageBytes := reqBodyBytes.Bytes()
connection.WriteMessage(websocket.TextMessage, messageBytes)
} else if response.Method == "trickle" {
var trickleResponse TrickleResponse
if err := json.Unmarshal(message, &trickleResponse); err != nil {
log.Fatal(err)
}
err := peerConnection.AddICECandidate(*trickleResponse.Params.Candidate)
if err != nil {
log.Fatal(err)
}
}
}
}
Note: You might need to enable go modules so that the dependencies are downloaded automatically when starting the script.
The finished script can now be started using the following command:
You might need to add sudo to access your camera
go run main.go
You should see the following output:
recv: {"method":"trickle","params":{"candidate":"candidate:3681230645 1 udp 2130706431 10.0.0.35 49473 typ host","sdpMid":"","sdpMLineIndex":0,"usernameFragment":null},"jsonrpc":"2.0"}
recv: {"method":"trickle","params":{"candidate":"candidate:233762139 1 udp 2130706431 172.17.0.1 57218 typ host","sdpMid":"","sdpMLineIndex":0,"usernameFragment":null},"jsonrpc":"2.0"}
Connection State has changed to checking
recv: {"method":"trickle","params":{"candidate":"candidate:2890797847 1 udp 2130706431 172.22.0.1 41179 typ host","sdpMid":"","sdpMLineIndex":0,"usernameFragment":null},"jsonrpc":"2.0"}
recv: {"method":"trickle","params":{"candidate":"candidate:3528925834 1 udp 2130706431 172.18.0.1 58906 typ host","sdpMid":"","sdpMLineIndex":0,"usernameFragment":null},"jsonrpc":"2.0"}
recv: {"method":"trickle","params":{"candidate":"candidate:3197649470 1 udp 1694498815 212.197.155.248 36942 typ srflx raddr 0.0.0.0 rport 36942","sdpMid":"","sdpMLineIndex":0,"usernameFragment":null},"jsonrpc":"2.0"}
recv: {"method":"trickle","params":{"candidate":"candidate:2563076625 1 udp 16777215 104.248.140.156 11643 typ relay raddr 0.0.0.0 rport 42598","sdpMid":"","sdpMLineIndex":0,"usernameFragment":null},"jsonrpc":"2.0"}
Connection State has changed to connected
Client-side
Now that we are successfully sending the video from the camera to the sfu it is time to create a frontend to receive it.
The HTML file is very basic and will only contain a video object and a button to subscribe to the stream. It will also print the current WebRTC logs into a div.
<meta charset="utf-8"/>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<style>
#remotes video {
width: 320px;
}
</style>
<title>WebRTC test frontend</title>
<div id="remotes">
<span
style="position: absolute; margin-left: 5px; margin-top: 5px"
class="badge badge-primary"
>Remotes</span
>
</div>
The javascript file will then connect to the sfu similar to the Golang script above. The only difference is that instead of reading the camera and sending the video to the sfu it will receive the video instead.
I will not go into much detail since all the functionality is already covered above.
const remotesDiv = document.getElementById("remotes");
const config = {
codec: 'vp8',
iceServers: [
{
"urls": "stun:stun.l.google.com:19302",
},
/*{
"urls": "turn:TURN_IP:3468",
"username": "username",
"credential": "password"
},*/
]
};
const signalLocal = new Signal.IonSFUJSONRPCSignal(
"ws://127.0.0.1:7000/ws"
);
const clientLocal = new IonSDK.Client(signalLocal, config);
signalLocal.onopen = () => clientLocal.join("test room");
clientLocal.ontrack = (track, stream) => {
console.log("got track", track.id, "for stream", stream.id);
if (track.kind === "video") {
track.onunmute = () => {
const remoteVideo = document.createElement("video");
remoteVideo.srcObject = stream;
remoteVideo.autoplay = true;
remoteVideo.muted = true;
remotesDiv.appendChild(remoteVideo);
track.onremovetrack = () => remotesDiv.removeChild(remoteVideo);
};
}
};
The only thing you have to keep in mind here is that a peer connection cannot be sent without offering or requesting some kind of stream. That is why add two receivers (one for audio and one for video) before sending the offer.
You can now start the frontend by opening the HTML file in your browser. Alternatively, you can open the HTML file using an Express server by creating a new file in the project's root directory.
touch server.js
The express dependency needs to be installed before adding the code.
npm init -y
npm install express --save
Then you can run your frontend as a static site using the following code.
const express = require("express");
const app = express();
const port = 3000;
const http = require("http");
const server = http.createServer(app);
app.use(express.static(__dirname + "/public"));
server.listen(port, () => console.log(Server is running on port ${port}
));
Start the application using the following command.
node server.js
You should now be able to access the frontend on localhost:3000 of your local machine.
Ion-SDK-Go
As mentioned above, the same video streaming functionality can also be implemented using a helper library created by the ion team, which abstracts the WebRTC signaling and therefore makes the implementation shorter and more concise. Still, knowing how to implement the signaling yourself is very important and leaves you with more customization for more complex projects.
package main
import (
"flag"
"fmt"
ilog "github.com/pion/ion-log"
sdk "github.com/pion/ion-sdk-go"
"github.com/pion/mediadevices"
"github.com/pion/mediadevices/pkg/codec/vpx"
"github.com/pion/mediadevices/pkg/frame"
"github.com/pion/mediadevices/pkg/prop"
"github.com/pion/webrtc/v3"
// Note: If you don't have a camera or microphone or your adapters are not supported,
// you can always swap your adapters with our dummy adapters below.
// _ "github.com/pion/mediadevices/pkg/driver/videotest"
// _ "github.com/pion/mediadevices/pkg/driver/audiotest"
_ "github.com/pion/mediadevices/pkg/driver/camera" // This is required to register camera adapter
_ "github.com/pion/mediadevices/pkg/driver/microphone" // This is required to register microphone adapter
)
var (
log = ilog.NewLoggerWithFields(ilog.DebugLevel, "", nil)
)
func main() {
// parse flag
var session, addr string
flag.StringVar(&addr, "addr", "localhost:50051", "Ion-sfu grpc addr")
flag.StringVar(&session, "session", "test room", "join session name")
flag.Parse()
// add stun servers
webrtcCfg := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
webrtc.ICEServer{
URLs: []string{"stun:stun.stunprotocol.org:3478", "stun:stun.l.google.com:19302"},
},
},
}
config := sdk.Config{
Log: log.Config{
Level: "debug",
},
WebRTC: sdk.WebRTCTransportConfig{
Configuration: webrtcCfg,
},
}
// new sdk engine
e := sdk.NewEngine(config)
// get a client from engine
c, err := sdk.NewClient(e, addr, "client id")
c.GetPubTransport().GetPeerConnection().OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
log.Infof("Connection state changed: %s", state)
})
if err != nil {
log.Errorf("client err=%v", err)
panic(err)
}
e.AddClient(c)
// client join a session
err = c.Join(session, nil)
if err != nil {
log.Errorf("join err=%v", err)
panic(err)
}
vpxParams, err := vpx.NewVP8Params()
if err != nil {
panic(err)
}
vpxParams.BitRate = 500_000 // 500kbps
codecSelector := mediadevices.NewCodecSelector(
mediadevices.WithVideoEncoders(&vpxParams),
)
fmt.Println(mediadevices.EnumerateDevices())
s, err := mediadevices.GetUserMedia(mediadevices.MediaStreamConstraints{
Video: func(c *mediadevices.MediaTrackConstraints) {
c.FrameFormat = prop.FrameFormat(frame.FormatYUY2)
c.Width = prop.Int(640)
c.Height = prop.Int(480)
},
Codec: codecSelector,
})
if err != nil {
panic(err)
}
for _, track := range s.GetTracks() {
track.OnEnded(func(err error) {
fmt.Printf("Track (ID: %s) ended with error: %v\n",
track.ID(), err)
})
_, err = c.Publish(track)
if err != nil {
panic(err)
} else {
break // only publish first track, thanks
}
}
select {}
}
As you can see, the library handles the signaling and therefore abstracts a lot of the code we wrote above. One difference between the library and the code we implemented above is that the library uses GRPC for signaling, whereas we use JSONRPC. You will therefore have to start ion-sfu in AllRPC mode instead of JSONRPC. This can be done by either starting the AllRPC version using Golang or by using the AllRPC image tag (e.g. latest-allrpc) when using Docker.
You can now start the application using the go run command:
go run ./main.go
You should now also be able to see the video of your camera on localhost:3000 of your local machine.
Conclusion
In this article, you learned what a sfu is and how you can utilize the ion-sfu to build a video broadcasting application. You also learned how to use the Golang media device library to read your camera without opening a browser window.
If you are interested in learning more, consider subscribing to my email list to never miss another article. Also, feel free to leave some feedback or check out my other articles.
Go lang
Доступ к K8S CRD из go-клиента
Kubernetes API сервер легко расширяется с помощью Custom Resource Defenition. Однако, доступ к этом ресурсу из популярных библиотек go-клиентов сложна и плохо задокументированна. Эта статья содержит маленькую инструкцию как получить доступ к такому ресурсу из вашего кода Go.
Цель
Я пришел к этой задаче, когда хотел синтегрировать внешнее хранилище в кубернетес кластер. План был использовать CRD, что бы определять резервы систем хранения данных. Потом, самодельный оператор может слушать все эти ресурсы чтобы создавать и удалять и управлять текущим состоянием этих ресурсов.
Определим и создадим CRD
Для этой статьи будем работать над простым примером: CRD может быть леко создан используя kubectl
для этого примера, мы начнем с одиночного простого определения ресурса:
apiVersion: "apiextensions.k8s.io/v1beta1"
kind: "CustomResourceDefinition"
metadata:
name: "projects.example.martin-helmich.de"
spec:
group: "example.martin-helmich.de"
version: "v1alpha1"
scope: "Namespaced"
names:
plural: "projects"
singular: "project"
kind: "Project"
validation:
openAPIV3Schema:
required: ["spec"]
properties:
spec:
required: ["replicas"]
properties:
replicas:
type: "integer"
minimum: 1
Для определения CRD, нам понадобиться озаботиться об API Group Name( в этом случае, example.martin-helmich.de
). По соглашению, это обычно доменное, которым вы владеете(например домер организации), чтобы предотвратить конфликты наименования. CRD именя обычно выглядят так: <plural-resource-name>.<api-group-name>
, в нашем примере: projects.example.martin-helmich.de
.
Так же, будьте внимательны когда выбираете версию CRD(spec.version
в примере выше). Пока еще рабоатает над CRD, то будет хорошей идей поместить CRD в группу alpha версии API. Для пользователей вашего самодельного ресурса, это будет значить, что что-то может измениться.
Часто, нужно проверить что данные которые хранит пользователень в вашем CRD содержит опредленные схемы. За это отвечает spec.validation.openAPIV3Schema
. Она содержит JSON схему которая описывает формат который должны иметь CRD.
После сохранения CRD в файл, применим его в кластере:
> kubectl apply -f projects-crd.yaml
customresourcedefinition "projects.example.martin-helmich.de" created
После создания CRD вы можете создать объект этого типа. Работает это так же как с обычными Kubernetes Объектами(pods, deploymens и так далее). Отличается только kind
и apiVersion
:
apiVersion: "example.martin-helmich.de/v1alpha1"
kind: "Project"
metadata:
name: "example-project"
namespace: "default"
spec:
replicas: 1
Можно создать CRD как любой другой объект через kubectl
> kubectl apply -f project.yaml
project "example-project" created
Можно даже использовать kubectl
чтобы получить самодельный ресурс обратно из K8S.
> kubectl get projects
NAME AGE
example-project 2m
Cоздание Golang клиента
Теперь, будем использовать пакет go-клиента, для доступа к этим CRD. Для примера, я буду считать, что мы работаем над Go проектом, с названием github.com/martin-helmich/kubernetes-crd-example
(репозиторий существует) и у него есть go-клиент и apimachinery установленну библиотеку в качестве модуля Go.
go mod init github.com/martin-helmich/kubernetes-crd-example
go get k8s.io/client-go@v0.17.0
go get k8s.io/apimachinery@v0.17.0
Множество документаций работая с CRD предполагают, что вы работаете с некоторым типом генерации кода, чтобы собрать клиентскую библиотеку автоматически. Однако, этот процесс задокументирован редко, и после прочтения нескольких ненавистных дискуссий на github, создалось впечатление, что всё ещё в "прогрессе". В общем, я встял в самостоятельную реализацую клиентат.
Шаг 1: Определение типов
Начав с определения типов для самодельного ресурса. Я нашел, что это хорошая практика, организовывать эти типа группируя по версии API. Для примера, можно создать файл api/types/v1alpha1/project.go
содержащий следующее:
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type ProjectSpec struct {
Replicas int `json:"replicas"`
}
type Project struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ProjectSpec `json:"spec"`
}
type ProjectList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Project `json:"items"`
}
Тип metav1.ObjectMeta
содержит типичные свойства метадаты, которые вы можете найти в любом K8s ресурсе.
Шаг 2: Определим метод DeepCopy
Каждый тип, который будет обслуживаться K8S API(в нашем случае, Project и ProjectList) требует реализацию k8s.io/apimachinery/pkg/runtime.Object
интерфейса. Этот интерфейс определяет 2 метода GetObjectKind()
и DeepCopyObject()
. Первый метод уже предоставлен встронной структурой metav1.TypeMeta
, второй нужно реализовать самостоятельно.
Метод DeepCopyObject
предназначен для создания полной копии объекта. Так как это требует шаблонного кода, этот метод часто генерируется автоматически. Для этой статьи мы сделаем это ручками. Продолжим с добавления второго файла deepcopy.go
в тот же покет:
package v1alpha1
import "k8s.io/apimachinery/pkg/runtime"
// DeepCopyInto copies all properties of this object into another object of the
// same type that is provided as a pointer.
func (in *Project) DeepCopyInto(out *Project) {
out.TypeMeta = in.TypeMeta
out.ObjectMeta = in.ObjectMeta
out.Spec = ProjectSpec{
Replicas: in.Spec.Replicas,
}
}
// DeepCopyObject returns a generically typed copy of an object
func (in *Project) DeepCopyObject() runtime.Object {
out := Project{}
in.DeepCopyInto(&out)
return &out
}
// DeepCopyObject returns a generically typed copy of an object
func (in *ProjectList) DeepCopyObject() runtime.Object {
out := ProjectList{}
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
out.Items = make([]Project, len(in.Items))
for i := range in.Items {
in.Items[i].DeepCopyInto(&out.Items[i])
}
}
return &out
}
Интерлюдия: Автоматическое создание DeepCopy метода
Так, мы могли заметить, что определение всех этих различных DeepCopy методов вовсе не веселое занятие. Есть множество различных инструментов и фреймворков около автогенерации этих методов(все зависит от уровня документации и в целом зрелости). То что нашел я, работает отлично в инструменте controller-gen
, что является частью файмеворка Kuberbuilder
:
$ go get -u github.com/kubernetes-sigs/controller-tools/cmd/controller-gen
Чтобы использовать controller-gen
, опишите ваш CRD тип через +k8s:deepcopy-gen annotation
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Project struct {
// ...
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type ProjectList struct {
// ...
}
Затем, выполните команду, для автоматического создания метода deepcopy
controller-gen object paths=./api/types/v1alpha1/project.go
Можно еще проще, вы можете добавить go:generate
выражение в целый файл:
package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
//go:generate controller-gen object paths=$GOFILE
// ...
И чтобы сгенерировать код, нужно выполнить команду в корневой папке:
go generate ./...
Шаг 3: Зарегистрируем типы на схеме компоновщика
Теперь, нам нужно сделать новый тип известным для библиотеки клиента. Это позволить клиенту(более или менее) автоматически обрабатывать ваши новые типы после подключения к API серверу.
Для этого, добавим новый файл register.go
в наш пакет:
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const GroupName = "example.martin-helmich.de"
const GroupVersion = "v1alpha1"
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: GroupVersion}
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Project{},
&ProjectList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
Как можно заметить, этот код не делает что-то реальное, пока еще(за исключением создания нового runtime.SchemeBuilder
). Важная часть в том. что AddToScheme
функция(строка 16)б которая экспортирует членов структуры созданых с типом runtime.SchemeBuilder
в строке 15. Вы можете вызвать эту функцию позже, из любой части вашего клиентского кода как только клиент K8S будет готов к регистрации вашего определенного типа.
Шаг 4: создание HTTP клиента
После определения типов и добавления метода для регистрации их в глобальной схеме компоновщика, вы можете создат HTTP клиента, который может загружать ваши собственные ресурсы.
Для этого, добавим следующий код в ваш main.go
вашего пакета
package main
import (
"flag"
"log"
"time"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"github.com/martin-helmich/kubernetes-crd-example/api/types/v1alpha1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
var kubeconfig string
func init() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "path to Kubernetes config file")
flag.Parse()
}
func main() {
var config *rest.Config
var err error
if kubeconfig == "" {
log.Printf("using in-cluster configuration")
config, err = rest.InClusterConfig()
} else {
log.Printf("using configuration from '%s'", kubeconfig)
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
}
if err != nil {
panic(err)
}
v1alpha1.AddToScheme(scheme.Scheme)
crdConfig := *config
crdConfig.ContentConfig.GroupVersion = &schema.GroupVersion{Group: v1alpha1.GroupName, Version: v1alpha1.GroupVersion}
crdConfig.APIPath = "/apis"
crdConfig.NegotiatedSerializer = serializer.NewCodecFactory(scheme.Scheme)
crdConfig.UserAgent = rest.DefaultKubernetesUserAgent()
exampleRestClient, err := rest.UnversionedRESTClientFor(&crdConfig)
if err != nil {
panic(err)
}
}
Теперь можно использовать exampleRestClient
созданный в строке 48, для запроса всех самостоятельных ресурсов внутри example.martin-helmich.de/v1alpha1
API группы. Пример может выглядить сдедующим образом:
result := v1alpha1.ProjectList{}
err := exampleRestClient.
Get().
Resource("projects").
Do().
Into(&result)
Чтобы использовать API типобезопасным способом, обычно хорошая идея обернуть эти операции внутри своего клиентского набора. Для этого, создаём новый подпакет clientset/v1alpha1
. Для начала, реализуем интерфейс который определяет типы для вашей группы API. И Переносим настройки конфигурации из вашего главного метода в эту функцию коснтуктора клиенсткого набора(NewForConfig
для примера ниже):
package v1alpha1
import (
"github.com/martin-helmich/kubernetes-crd-example/api/types/v1alpha1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
)
type ExampleV1Alpha1Interface interface {
Projects(namespace string) ProjectInterface
}
type ExampleV1Alpha1Client struct {
restClient rest.Interface
}
func NewForConfig(c *rest.Config) (*ExampleV1Alpha1Client, error) {
config := *c
config.ContentConfig.GroupVersion = &schema.GroupVersion{Group: v1alpha1.GroupName, Version: v1alpha1.GroupVersion}
config.APIPath = "/apis"
config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
config.UserAgent = rest.DefaultKubernetesUserAgent()
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
return &ExampleV1Alpha1Client{restClient: client}, nil
}
func (c *ExampleV1Alpha1Client) Projects(namespace string) ProjectInterface {
return &projectClient{
restClient: c.restClient,
ns: namespace,
}
}
Код ниже, всё еще, не будет компилироваться, так как в нем всё еще отсутствуют ProjectInterface
и projectClient
типы. Мы сейчас до них доберемся.
ExampleV1Alpha1Interface
и его реализация, ExampleV1Alpha1Client
структура это главная точка входа для доступа к самодельным ресурсам. Вы можете легко создать новый клиентский набор в вашем main.go
, просто вызывая clientset, err := v1alpha1.NewForConfig(config)
.
Дальше, вам нужно реализовать определенный клиентский набор для доступа к самодельному ресурсу Project
(пример выше уже использует ProjectInterface
и projectClient
типы которые всё еще нужно поддерживать). Создадим втрой файл в том же пакете prjects.go
:
package v1alpha1
import (
"github.com/martin-helmich/kubernetes-crd-example/api/types/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
)
type ProjectInterface interface {
List(opts metav1.ListOptions) (*v1alpha1.ProjectList, error)
Get(name string, options metav1.GetOptions) (*v1alpha1.Project, error)
Create(*v1alpha1.Project) (*v1alpha1.Project, error)
Watch(opts metav1.ListOptions) (watch.Interface, error)
// ...
}
type projectClient struct {
restClient rest.Interface
ns string
}
func (c *projectClient) List(opts metav1.ListOptions) (*v1alpha1.ProjectList, error) {
result := v1alpha1.ProjectList{}
err := c.restClient.
Get().
Namespace(c.ns).
Resource("projects").
VersionedParams(&opts, scheme.ParameterCodec).
Do().
Into(&result)
return &result, err
}
func (c *projectClient) Get(name string, opts metav1.GetOptions) (*v1alpha1.Project, error) {
result := v1alpha1.Project{}
err := c.restClient.
Get().
Namespace(c.ns).
Resource("projects").
Name(name).
VersionedParams(&opts, scheme.ParameterCodec).
Do().
Into(&result)
return &result, err
}
func (c *projectClient) Create(project *v1alpha1.Project) (*v1alpha1.Project, error) {
result := v1alpha1.Project{}
err := c.restClient.
Post().
Namespace(c.ns).
Resource("projects").
Body(project).
Do().
Into(&result)
return &result, err
}
func (c *projectClient) Watch(opts metav1.ListOptions) (watch.Interface, error) {
opts.Watch = true
return c.restClient.
Get().
Namespace(c.ns).
Resource("projects").
VersionedParams(&opts, scheme.ParameterCodec).
Watch()
}
Этот клиент очевидно еще не закончен и не имеет методы типа Delete
, Update
и другие. Однако, это можно реализовать похожим на существующий метод образом. Посмотрите на существующий клиенсткий набор(для примера Pod client set
) для вдохновения.
После создания вашего клиентского набора и используя его, вывести список существующих ресурсов становится доволно легко.
import clientV1alpha1 "github.com/martin-helmich/kubernetes-crd-example/clientset/v1alpha1"
// ...
func main() {
// ...
clientSet, err := clientV1alpha1.NewForConfig(config)
if err != nil {
panic(err)
}
projects, err := clientSet.Projects("default").List(metav1.ListOptions{})
if err != nil {
panic(err)
}
fmt.Printf("projects found: %+v\n", projects)
}
Шаг 5: Созадем оповещятель
При создании оператора Kubernetes. Вы обычно хотите иметь возможность реагировать на вновь созданные или обновленные ресурсы. В теории, вы можете просто переодически вызывать List()
метод и проверять добавленны ли новые ресусры. На практике, это не оптимальное решение, особенно когда у вас есть множество подобных ресурсов.
Большинство операторов работает изначала загрузив все актуальные экземпляры ресурсов используя начальный вызов List()
, и затем подписываясь на обновления с помощью Watch()
вызова. Начальный список объетов и обновления полученные от Watch()
далее используются для создания локального кэша, что позволяет иметь быстрый доступ к любому самодельному ресурсу без надобности хождения к API серверу каждый раз.
Этот шаблон широко распространнен, что библиотеки go-клиентов предлагают готовое решение: пакет k8s.io/client-go/tools/cache. Вы можете создать новый оповещатель для ваших ресурсов:
package main
import (
"time"
"github.com/martin-helmich/kubernetes-crd-example/api/types/v1alpha1"
client_v1alpha1 "github.com/martin-helmich/kubernetes-crd-example/clientset/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
)
func WatchResources(clientSet client_v1alpha1.ExampleV1Alpha1Interface) cache.Store {
projectStore, projectController := cache.NewInformer(
&cache.ListWatch{
ListFunc: func(lo metav1.ListOptions) (result runtime.Object, err error) {
return clientSet.Projects("some-namespace").List(lo)
},
WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) {
return clientSet.Projects("some-namespace").Watch(lo)
},
},
&v1alpha1.Project{},
1*time.Minute,
cache.ResourceEventHandlerFuncs{},
)
go projectController.Run(wait.NeverStop)
return projectStore
}
Метод NewInformer возвращает два объекта: второй - значение, controller
- управляет List()
и Watch()
вызывает и наполняет первое значение, храня некоторое количество кэшированных ресурсов с API сервера(в нашем случае CRD).
Теперь можно использовать хранилище, для легкого доступа к вашему CRD, либо слушая их или иметь доступ к ним по именам. Помните, что функции хранения возвращаяют interface
типа, поэтому вам нужно будет самостоятельно приводить их к CRD типам.
store := WatchResource(clientSet)
project := store.GetByKey("some-namespace/some-project").(*v1alpha1.Project)
Вывод
Создание клиентов для CRD - это что-то что мало задокументированно(на данный момент) и подчас может быть довольно сложным.
Клиентская библиотека для CRD, что показана в статье, вместе с оповещятелем это отличный старт для создания вашего собственного K8S оператора который реагирует на изменения который делают CRD.
Как мокать? Go способ.
У Go есть встроенный фреймворк тестирования предоставленный testing
пакетом, это позволяет писать тесты проще, но тогда как мы пишем более сложные тесты которые требуют моков?
В этой статье, мы изучим как взять преимущества структур и интерфейсов в Go, чтобы смокать любой сервис или библиотеку которую используем, без использования сторонних инструментов и библиотек.
Начнем с определения нашей системы для понимания того. что мы будем делать с тестом и моком.
Система.
Наша система имеет 2 компонента:
- "Наш сервис" который у нас есть и мы создаем
- "Сторонние сервисы и библиотеки" которые взаимодействуют с базой данных и мы используем для работоспособности нашего сервиса. Теперь так как мы строим "Наш сервис", мы хотим написать независимую единицу для "Нашего сервиса" но так как мы используем функционал стороннего сервиса или библиотеки в нашем сервисе, если мы тестируем без моков, мы будет производить интеграционное тестирование, что иногда сложно и более временно затратно. Для демонстрации, мы напишем простую библиотеку которая проверяет существует ли пользователь внутри 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, но для прототипирования я нашел, что командная утилита проще в решении проблем.
GTK3 app
package main
import (
"log"
"github.com/gotk3/gotk3/gtk"
)
func main() {
gtk.Init(nil)
//новое окно
win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
if err != nil {
log.Fatal("Unable to create window:", err)
}
win.SetTitle("MP3 Player")
win.Connect("destroy", func() {
//при событии закрытия окна отключаемся от gtk
gtk.MainQuit()
})
ui(win)
win.SetDefaultSize(400, 300)
//отображение окна и запуск цикла событий
win.ShowAll()
gtk.Main()
}
func ui(win *gtk.Window) {
layout, _ := gtk.GridNew()
title, _ := gtk.LabelNew("test")
layout.Attach(title, 0, 0, 1, 1) //верхний ряд
button, _ := gtk.ButtonNew()
button.Connect("clicked", func() {
//toggle state
})
button_label, _ := gtk.LabelNew("Play/Pause")
button.Add(button_label)
//кнопка под меткой
layout.AttachNextTo(button, title, gtk.POS_BOTTOM, 1, 1)
win.Add(layout)
}
Тестикулировать
Simple nats
https://nats.io/
https://github.com/nats-io/nats.go
Neo4j + triplet
package main
import (
"context"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
"log"
"os"
"fmt"
)
type Triplet struct {
Object string
Predicate string
Subject string
}
func main() {
ctx := context.TODO()
uri := "bolt://localhost:7687"
username := "neo4j"
password := "neo4jneo4j"
var driver neo4j.DriverWithContext
var session neo4j.SessionWithContext
var triplet Triplet
triplet.Object = os.Args[1]
triplet.Predicate = os.Args[2]
triplet.Subject = os.Args[3]
driver, err := createDriver(uri, username, password)
if err != nil {
log.Fatal(err)
}
session, err = createSession(ctx, driver)
if err != nil {
log.Fatal(err)
}
message, err := createTriplet(ctx, triplet, session)
if err != nil {
log.Fatal(err)
}
log.Println("message: ", message)
defer driver.Close(ctx)
defer session.Close(ctx)
}
func createDriver(uri, username, password string) (neo4j.DriverWithContext, error) {
driver, err := neo4j.NewDriverWithContext(uri, neo4j.BasicAuth(username, password, ""))
if err != nil {
return nil, err
}
return driver, nil
}
func createSession(ctx context.Context, driver neo4j.DriverWithContext) (neo4j.SessionWithContext, error) {
session := driver.NewSession(ctx, neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite})
return session, nil
}
func createTriplet(ctx context.Context, t Triplet, session neo4j.SessionWithContext) (string, error) {
var template = "MERGE (o:Node {name: \"%s\"}) " +
"MERGE (s:Node {name: \"%s\"}) " +
"MERGE (o)-[:%s]-(s)"
request := fmt.Sprintf(template, t.Object, t.Subject, t.Predicate)
_, err := session.ExecuteWrite(
ctx,
func(transaction neo4j.ManagedTransaction) (any, error) {
result, err := transaction.Run(ctx, request, map[string]any{})
if err != nil {
return nil, err
}
if result.Next(ctx) {
return result.Record().Values[0], nil
}
return nil, result.Err()
})
if err != nil {
return "", err
}
return "", nil
}
Running list
//show.go
package main
import (
"fmt"
"time"
)
func PrintTasks(tasks []Task) {
printCanvas()
for _, t := range tasks {
printTasks(t)
}
}
func printCanvas() {
day := time.Now()
fmt.Printf("\tsun\tmon\ttue\twen\tthu\tfri\tsat\t\t Weeknumber %d\n", (day.YearDay() / 7))
fmt.Printf("\t#############################################################################################\n")
}
func printTasks(t Task) {
day := time.Now()
week := [7]int{0, 0, 0, 0, 0, 0, 0}
switch t.Done {
case -1:
for i := t.CreateDay; i < day.Weekday(); i++ {
week[i] = 2
}
default:
for i := t.CreateDay; i < t.Done; i++ {
week[i] = 2
}
week[t.Done] = 1
}
fmt.Printf(
"\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
printDays(week[0]),
printDays(week[1]),
printDays(week[2]),
printDays(week[3]),
printDays(week[4]),
printDays(week[5]),
printDays(week[6]),
t.Description,
)
}
func printDays(i int) string {
switch i {
case 1:
return "[X]"
case 2:
return "[ ]"
default:
return " "
}
}
//struct.go
package main
import "time"
type Task struct {
Description string
Done time.Weekday
CreateDay time.Weekday
Week int
}
func (t *Task) NewTask(description string) {
day := time.Now()
t.Description = description
t.Done = -1
t.CreateDay = day.Weekday()
t.Week = day.YearDay() / 7
}
//main.go
package main
import "time"
func main() {
week := time.Now().YearDay()
tasks := []Task{
Task{
Description: "Test 123",
Done: time.Wednesday,
CreateDay: time.Monday,
Week: week,
},
Task{
Description: "Test 32",
Done: -1,
CreateDay: time.Monday,
Week: week,
},
Task{
Description: "Test 32",
Done: time.Tuesday,
CreateDay: time.Tuesday,
Week: week,
},
Task{
Description: "Test 32",
Done: time.Monday,
CreateDay: time.Sunday,
Week: week,
},
}
PrintTasks(tasks)
}
package main
import (
"log"
"net"
)
func echoServer(c net.Conn) {
for {
buf := make([]byte, 512)
nr, err := c.Read(buf)
if err != nil {
return
}
data := buf[0:nr]
println("Server got:", string(data))
_, err = c.Write(data)
if err != nil {
log.Fatal("Write: ", err)
}
}
}
func main() {
l, err := net.Listen("unix", "/tmp/echo.sock")
if err != nil {
log.Fatal("listen error:", err)
}
for {
fd, err := l.Accept()
if err != nil {
log.Fatal("accept error:", err)
}
go echoServer(fd)
}
}
package main
import (
"io"
"log"
"net"
"time"
)
func reader(r io.Reader) {
buf := make([]byte, 1024)
for {
n, err := r.Read(buf[:])
if err != nil {
return
}
println("Client got:", string(buf[0:n]))
}
}
func main() {
c, err := net.Dial("unix", "/tmp/echo.sock")
if err != nil {
panic(err)
}
defer c.Close()
go reader(c)
for {
_, err := c.Write([]byte("hi"))
if err != nil {
log.Fatal("write error:", err)
break
}
time.Sleep(1e9)
}
}
package main
import "github.com/martinlindhe/notify"
func main() {
// show a notification
notify.Notify("app name", "notice", "some text", "path/to/icon.png")
// show a notification and play a alert sound
notify.Alert("app name", "alert", "some text", "path/to/icon.png")
}
ponzu
https://github.com/ponzu-cms/ponzu
docker push без docker push(пример)
запуск:
./uploadImage "~/path/to/saved/image" "http://localhost:8081/link/to/docker/registry" myRepoName 1.0
Код приложения:
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
)
func main() {
imageDir := os.Args[1]
url := os.Args[2]
repoName := os.Args[3]
tag := os.Args[4]
manifestFile := filepath.Join(imageDir, "manifestCopy")
configFile := findFileWithExtension(imageDir, ".json")
prepareLayersForUpload(imageDir, manifestFile)
setConfigProps(manifestFile, configFile)
manifestContent, err := ioutil.ReadFile(manifestFile)
if err != nil {
panic(err)
}
uuid := initiateUpload(url, repoName)
layersNames := findLayerFiles(imageDir)
layersSizes := findLayerSizes(imageDir, layersNames)
for i, layerName := range layersNames {
pathToLayer := findFileWithExactName(imageDir, layerName)
patchLayer(url, repoName, uuid, pathToLayer, layersSizes[i])
putLayer(url, repoName, uuid, layerName, pathToLayer, layersSizes[i])
}
configName := getFileNameWithoutExtension(configFile)
patchLayer(url, repoName, uuid, configFile, fileSize(configFile))
putLayer(url, repoName, uuid, configName, configFile, fileSize(configFile))
putManifest(url, repoName, tag, manifestContent)
fmt.Println("Upload completed successfully")
}
func findFileWithExtension(dir, extension string) string {
files, err := ioutil.ReadDir(dir)
if err != nil {
panic(err)
}
for _, file := range files {
if filepath.Ext(file.Name()) == extension {
return filepath.Join(dir, file.Name())
}
}
panic("File with extension not found")
}
func findFileWithExactName(dir, fileName string) string {
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if info.Name() == fileName {
foundPath = path
return filepath.SkipDir
}
return nil
})
if err != nil {
panic(err)
}
return foundPath
}
func findLayerFiles(imageDir string) []string {
var layerNames []string
err := filepath.Walk(imageDir, func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(info.Name(), "layer.tar") {
layerNames = append(layerNames, info.Name())
}
return nil
})
if err != nil {
panic(err)
}
return layerNames
}
func findLayerSizes(imageDir string, layerNames []string) []int64 {
var layerSizes []int64
for _, layerName := range layerNames {
pathToLayer := findFileWithExactName(imageDir, layerName)
fileInfo, err := os.Stat(pathToLayer)
if err != nil {
panic(err)
}
layerSizes = append(layerSizes, fileInfo.Size())
}
return layerSizes
}
func prepareLayersForUpload(imageDir, manifestFile string) {
infoFile := filepath.Join(imageDir, "info")
layersNames := findLayerFiles(imageDir)
layersSizes := findLayerSizes(imageDir, layersNames)
var buffer bytes.Buffer
buffer.WriteString("{")
for i, layerName := range layersNames {
layerSize := layersSizes[i]
layerDigest := getSha256Sum(findFileWithExactName(imageDir, layerName))
buffer.WriteString(fmt.Sprintf("{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": %d,
"digest": \"sha256:%s\"
},", layerSize, layerDigest))
}
buffer.Truncate(buffer.Len() - 1)
buffer.WriteString("\n\t]\n}")
err := ioutil.WriteFile(manifestFile, buffer.Bytes(), 0644)
if err != nil {
panic(err)
}
}
func setConfigProps(manifestFile, configFile string) {
configSize := fileSize(configFile)
configName := getFileNameWithoutExtension(configFile)
manifestContent, err := ioutil.ReadFile(manifestFile)
if err != nil {
panic(err)
}
manifestContent = bytes.ReplaceAll(manifestContent, []byte("config_size"), []byte(fmt.Sprintf("%d", configSize)))
manifestContent = bytes.ReplaceAll(manifestContent, []byte("config_hash"), []byte(fmt.Sprintf("%s", configName)))
err = ioutil.WriteFile(manifestFile, manifestContent, 0644)
if err != nil {
panic(err)
}
}
func initiateUpload(url, repoName string) string {
resp, err := http.Post(fmt.Sprintf("%s/v2/%s/blobs/uploads/", url, repoName), "", nil)
if err != nil {
panic(err)
}
uuid := resp.Header.Get("Docker-Upload-Uuid")
if uuid == "" {
panic("Failed to initiate upload")
}
return uuid
}
func patchLayer(url, repoName, uuid, filePath string, fileSize int64) {
file, err := os.Open(filePath)
if err != nil {
panic(err)
}
defer file.Close()
_, err = httpPatch(fmt.Sprintf("%s/v2/%s/blobs/uploads/%s", url, repoName, uuid), file, fileSize, "Content-Type: application/octet-stream")
if err != nil {
panic(err)
}
}
func putLayer(url, repoName, uuid, layerName, filePath string, fileSize int64) {
_, err := httpPut(fmt.Sprintf("%s/v2/%s/blobs/uploads/%s?digest=sha256:%s", url, repoName, uuid, layerName), filePath, fileSize, "Content-Type: application/octet-stream")
if err != nil {
panic(err)
}
}
func putManifest(url, repoName, tag string, manifestContent []byte) {
_, err := httpPut(fmt.Sprintf("%s/v2/%s/manifests/%s", url, repoName, tag), bytes.NewReader(manifestContent), int64(len(manifestContent)), "Content-Type: application/vnd.docker.distribution.manifest.v2+json")
if err != nil {
panic(err)
}
}
func fileSize(filePath string) int64 {
fileInfo, err := os.Stat(filePath)
if err != nil {
panic(err)
}
return fileInfo.Size()
}
func getSha256Sum(filePath string) string {
file, err := os.Open(filePath)
if err != nil {
panic(err)
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
panic(err)
}
return hex.EncodeToString(hash.Sum(nil))
}
func getFileNameWithoutExtension(filePath string) string {
return strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
}
func httpPatch(url string, body io.Reader, contentLength int64, headers ...string) ([]byte, error) {
req, err := http.NewRequest(http.MethodPatch, url, body)
if err != nil {
return nil, err
}
addHeaders(req, contentLength, headers...)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
return responseBody, nil
}
func httpPut(url string, filePath string, contentLength int64, headers ...string) ([]byte, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
req, err := http.NewRequest(http.MethodPut, url, file)
if err != nil {
return nil, err
}
addHeaders(req, contentLength, headers...)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
}
return responseBody, nil
}
func addHeaders(req *http.Request, contentLength int64, headers ...string) {
req.ContentLength = contentLength
for _, header := range headers {
parts := strings.SplitN(header, ":", 2)
if len(parts) != 2 {
continue
}
req.Header.Set(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
}
}
Docker pull без docker
Команда запуска:
./script dirName “http://localhost:8081/link/to/docker/registry” myAwesomeImage 1.0
Пример кода:
package main
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
func main() {
downloadDir := os.Args[1]
url := os.Args[2]
imageName := os.Args[3]
tag := os.Args[4]
layers := getLayers(url, imageName, tag)
for _, layer := range layers {
fmt.Println("Downloading", layer)
downloadLayer(url, imageName, layer, downloadDir)
}
untarLayers(downloadDir)
fmt.Println("Download completed successfully")
}
func getLayers(url, imageName, tag string) []string {
resp, err := http.Get(fmt.Sprintf("%s/v2/%s/manifests/%s", url, imageName, tag))
if err != nil {
panic(err)
}
defer resp.Body.Close()
var layers []string
for _, header := range resp.Header.Values("Docker-Content-Digest") {
if strings.HasPrefix(header, "sha256:") {
layers = append(layers, header[7:])
}
}
return layers
}
func downloadLayer(url, imageName, layer, downloadDir string) {
resp, err := http.Get(fmt.Sprintf("%s/v2/%s/blobs/%s", url, imageName, layer))
if err != nil {
panic(err)
}
defer resp.Body.Close()
filePath := filepath.Join(downloadDir, layer+".tar")
file, err := os.Create(filePath)
if err != nil {
panic(err)
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
panic(err)
}
}
func untarLayers(downloadDir string) {
err := filepath.Walk(downloadDir, func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(info.Name(), ".tar") {
cmd := exec.Command("tar", "xvf", path, "-C", downloadDir)
err := cmd.Run()
if err != nil {
return err
}
}
return nil
})
if err != nil {
panic(err)
}
err = filepath.Walk(downloadDir, func(path string, info os.FileInfo, err error) error {
if strings.HasSuffix(info.Name(), ".tar") {
err := os.Remove(path)
if err != nil {
return err
}
}
return nil
})
if err != nil {
panic(err)
}
}