مرحبا الاسم المستعار! إذا كنت مبرمجًا وتعمل ببنية خدمات مصغرة ، فتخيل أنك بحاجة إلى تكوين تفاعل خدمتك أ مع خدمة جديدة وما زالت غير معروفة ب. ماذا ستفعل أولاً؟
إذا طرحت هذا السؤال على 100 مبرمج من شركات مختلفة ، فسنحصل على الأرجح على 100 إجابة مختلفة. يصف شخص ما العقود في اختيال ، يقوم شخص ما في gRPC ببساطة بجعل العملاء لخدماتهم دون وصف عقد. وقد قام شخص ما بتخزين JSON في googleok: D. تقوم معظم الشركات بتطوير نهجها الخاص للتفاعل بين الخدمات بناءً على بعض العوامل التاريخية والكفاءات ومكدس التكنولوجيا وما إلى ذلك. أريد أن أخبرك كيف تتواصل الخدمات في Delivery Club مع بعضها البعض ولماذا اتخذنا هذا الاختيار. والأهم من ذلك ، كيف نضمن ملاءمة الوثائق بمرور الوقت. سيكون هناك الكثير من التعليمات البرمجية!
أهلا مرة أخرى! اسمي سيرجي بوبوف ، أنا قائد الفريق المسؤول عن نتائج البحث عن المطاعم في التطبيقات وعلى موقع Delivery Club ، وأيضًا عضو نشط في نقابة التطوير الداخلي الخاصة بنا لـ Go (قد نتحدث عن هذا لاحقًا ، ولكن ليس الآن).
سأقوم بالحجز على الفور ، وسنتحدث بشكل أساسي عن الخدمات المكتوبة في Go. لم ننفذ بعد إنشاء الكود لخدمات PHP ، على الرغم من أننا نحقق التوحيد في المناهج هناك بطريقة مختلفة.
ما أردنا أن ننتهي به:
- ضمان عقود الخدمة محدثة. يجب أن يؤدي ذلك إلى تسريع إدخال خدمات جديدة وتسهيل التواصل بين الفرق.
- تعال إلى طريقة موحدة للتفاعل عبر HTTP بين الخدمات (لن نفكر في التفاعلات من خلال قوائم الانتظار وتدفق الأحداث في الوقت الحالي).
- لتوحيد نهج العمل مع عقود الخدمة.
- استخدم مستودعًا واحدًا للعقود حتى لا تبحث عن أرصفة لجميع أنواع الالتقاء.
- من الناحية المثالية ، قم بإنشاء عملاء لمنصات مختلفة.
مما سبق ، يتبادر إلى الذهن Protobuf كطريقة موحدة لوصف العقود. لديه أدوات جيدة ويمكنه إنشاء عملاء لمنصات مختلفة (البند 5). ولكن هناك أيضًا عيوب واضحة: بالنسبة للكثيرين ، تظل gRPC شيئًا جديدًا وغير معروف ، وهذا من شأنه أن يعقد تنفيذه بشكل كبير. كان العامل المهم الآخر هو أن الشركة قد اعتمدت منذ فترة طويلة نهج "المواصفات أولاً" ، وأن التوثيق موجود بالفعل لجميع الخدمات في شكل تبجح أو وصف RAML.
الذهاب التباهي
من قبيل الصدفة ، في نفس الوقت ، بدأنا في تكييف Go في الشركة. لذلك ، كان مرشحنا التالي للنظر فيه هو go-swagger - أداة تتيح لك إنشاء عملاء ورمز خادم من مواصفات التباهي. العيب الواضح هو أنه يولد رمزًا لـ Go فقط. في الواقع ، إنه يستخدم إنشاء كود رائع ، ويسمح go-swagger بالعمل المرن مع القوالب ، لذلك من الناحية النظرية يمكن استخدامه لإنشاء كود PHP ، لكننا لم نجربه بعد.
لا يتعلق Go-swagger بتوليد طبقة النقل فقط. في الواقع ، إنه يولد الهيكل العظمي للتطبيق ، وهنا أود أن أذكر القليل عن ثقافة التطوير في العاصمة. لدينا مصدر داخلي ، مما يعني أن أي مطور من أي فريق يمكنه إنشاء طلب سحب لأي خدمة لدينا. لجعل هذا المخطط يعمل ، نحاول توحيد الأساليب في التنمية: نستخدم مصطلحات مشتركة ، نهج واحد للتسجيل ، والقياسات ، والعمل مع التبعيات ، وبالطبع ، في هيكل المشروع.
وبالتالي ، من خلال تنفيذ go-swagger ، فإننا نقدم معيارًا لتطوير خدماتنا في Go. هذه خطوة أخرى نحو أهدافنا ، والتي لم نتوقعها في البداية ، ولكنها مهمة للتنمية بشكل عام.
الخطوات الأولى
لذلك تبين أن go-swagger مرشح مثير للاهتمام يبدو أنه قادر على تغطية معظم متطلباتنا
ملاحظة: جميع التعليمات البرمجية الإضافية ذات صلة بالإصدار 0.24.0 ، ويمكن الاطلاع على تعليمات التثبيت في مستودعنا مع أمثلة ، ويحتوي الموقع الرسمي على إرشادات لتثبيت الإصدار الحالي.دعونا نرى ما يمكنه فعله. لنأخذ مواصفات اختيال وننشئ خدمة:
> goswagger generate server \
--with-context -f ./swagger-api/swagger.yml \
--name example1
لقد حصلنا على ما يلي:
Makefile and go.mod لقد صنعت نفسي بالفعل.
في الواقع ، انتهى بنا المطاف بخدمة تعالج الطلبات الموضحة في التباهي.
> go run cmd/example1-server/main.go
2020/02/17 11:04:24 Serving example service at http://127.0.0.1:54586
> curl http://localhost:54586/hello -i
HTTP/1.1 501 Not Implemented
Content-Type: application/json
Date: Sat, 15 Feb 2020 18:14:59 GMT
Content-Length: 58
Connection: close
"operation hello HelloWorld has not yet been implemented"
الخطوة الثانية. فهم القوالب
من الواضح أن الكود الذي أنشأناه بعيدًا عن ما نريد رؤيته أثناء التشغيل.
ما نريده من هيكل تطبيقنا:
- كن قادرًا على تكوين التطبيق: قم بنقل إعدادات الاتصال بقاعدة البيانات ، وتحديد منفذ اتصالات HTTP ، وما إلى ذلك.
- حدد كائن التطبيق الذي سيخزن حالة التطبيق واتصال قاعدة البيانات وما إلى ذلك.
- اجعل وظائف معالجات تطبيقنا ، وهذا يجب أن يبسط العمل مع الكود.
- تهيئة التبعيات في الملف الرئيسي (في مثالنا لن يحدث هذا ، لكننا ما زلنا نريده.
لحل المشاكل الجديدة ، يمكننا تجاوز بعض القوالب. للقيام بذلك ، سنصف الملفات التالية ، كما فعلت ( Github ):
نحتاج إلى وصف ملفات القوالب (
`*.gotmpl`
) وملف التكوين ( `*.yml`
) الخاص بإنشاء خدمتنا.
بعد ذلك ، بالترتيب ، سنحلل القوالب التي صنعتها. لن أتعمق في العمل معهم ، لأن وثائق go-swagger مفصلة تمامًا ، على سبيل المثال ، إليك وصف ملف التكوين. سألاحظ فقط أنه يتم استخدام Go-Templating ، وإذا كان لديك بالفعل خبرة في هذا أو كان عليك وصف تكوينات HELM ، فلن يكون من الصعب معرفة ذلك.
تكوين التطبيق
يحتوي config.gotmpl على بنية بسيطة بمعامل واحد - المنفذ الذي سيستمع إليه التطبيق لطلبات HTTP الواردة. لقد صنعت أيضًا وظيفة من
InitConfig
شأنها قراءة متغيرات البيئة وملء هذا الهيكل. سأسميها من main.go ، لذا InitConfig
جعلتها وظيفة عامة.
package config
import (
"github.com/pkg/errors"
"github.com/vrischmann/envconfig"
)
// Config struct
type Config struct {
HTTPBindPort int `envconfig:"default=8001"`
}
// InitConfig func
func InitConfig(prefix string) (*Config, error) {
config := &Config{}
if err := envconfig.InitWithPrefix(config, prefix); err != nil {
return nil, errors.Wrap(err, "init config failed")
}
return config, nil
}
لكي يتم استخدام هذا القالب عند إنشاء رمز ، يجب تحديده في تكوين YML :
layout:
application:
- name: cfgPackage
source: serverConfig
target: "./internal/config/"
file_name: "config.go"
skip_exists: false
سأخبرك قليلاً عن المعلمات:
name
- له وظيفة إعلامية بحتة ولا يؤثر على الجيل.source
- في الواقع المسار إلى ملف القالب في camelCase ، أي serverConfig يعادل ./server/config.gotmpl .target
- الدليل حيث سيتم حفظ الكود الذي تم إنشاؤه. هنا يمكنك استخدام القوالب لإنشاء مسار ديناميكيًا ( مثال ).file_name
- اسم الملف الذي تم إنشاؤه ، هنا يمكنك أيضًا استخدام القوالب.skip_exists
- إشارة إلى أن الملف سيتم إنشاؤه مرة واحدة فقط ولن يقوم بالكتابة فوق الملف الموجود. هذا مهم بالنسبة لنا ، لأن ملف التكوين سيتغير مع نمو التطبيق ويجب ألا يعتمد على الكود الذي تم إنشاؤه.
في تكوين إنشاء الشفرة ، تحتاج إلى تحديد جميع الملفات ، وليس فقط تلك التي نريد تجاوزها. للملفات التي لم نغير، بالمعنى المقصود في
source
نقطة الخروج asset:< >
، على سبيل المثال، هنا : asset:serverConfigureapi
. بالمناسبة ، إذا كنت مهتمًا بالاطلاع على القوالب الأصلية ، فهي هنا .
كائن التطبيق والمعالجات
لن أصف كائن التطبيق لتخزين الحالة واتصالات قاعدة البيانات وأشياء أخرى ، فكل شيء مشابه للتكوين الذي تم إنشاؤه للتو. ولكن مع المتعاملين ، كل شيء أكثر إثارة للاهتمام. هدفنا الرئيسي هو إنشاء دالة كعب في ملف منفصل عندما نضيف عنوان URL إلى المواصفات ، والأهم من ذلك ، أن يقوم خادمنا باستدعاء هذه الوظيفة لمعالجة الطلب.
دعنا نصف قالب الوظيفة والأوتار:
package app
import (
api{{ pascalize .Package }} "{{.GenCommon.TargetImportPath}}/{{ .RootPackage }}/operations/{{ .Package }}"
"github.com/go-openapi/runtime/middleware"
)
func (srv *Service){{ pascalize .Name }}Handler(params api{{ pascalize .Package }}.{{ pascalize .Name }}Params{{ if .Authorized }}, principal api{{ .Package }}.{{ if not ( eq .Principal "interface{}" ) }}*{{ end }}{{ .Principal }}{{ end }}) middleware.Responder {
return middleware.NotImplemented("operation {{ .Package }} {{ pascalize .Name }} has not yet been implemented")
}
دعنا نلقي نظرة فاحصة على مثال:
pascalize
- يجلب خطًا مع CamelCase (وصف الوظائف الأخرى هنا )..RootPackage
- إنشاء حزمة خادم الويب..Package
- اسم الحزمة في الكود الذي تم إنشاؤه ، والذي يصف جميع الهياكل اللازمة لطلبات واستجابات HTTP ، أي الهياكل. على سبيل المثال ، هيكل لجسم الطلب أو بنية استجابة..Name
- اسم المعالج. مأخوذ من معرف العملية في المواصفات ، إذا تم تحديده. أوصي دائمًا بتحديدoperationID
نتيجة أكثر وضوحًا.
تكوين المعالج كما يلي:
layout:
operations:
- name: handlerFns
source: serverHandler
target: "./internal/app"
file_name: "{{ (snakize (pascalize .Name)) }}.go"
skip_exists: true
كما ترى ، لن يتم الكتابة فوق رمز المعالج (
skip_exists: true
) ، وسيتم إنشاء اسم الملف من اسم المعالج.
حسنًا ، هناك وظيفة كعب روتين ، لكن خادم الويب لا يعرف حتى الآن أنه يجب استخدام هذه الوظائف لمعالجة الطلبات. لقد أصلحت هذا في main.go (لن أعطي الكود بالكامل ، يمكن العثور على النسخة الكاملة هنا ):
package main
{{ $name := .Name }}
{{ $operations := .Operations }}
import (
"fmt"
"log"
"github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi"
"github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi/operations"
{{range $index, $op := .Operations}}
{{ $found := false }}
{{ range $i, $sop := $operations }}
{{ if and (gt $i $index ) (eq $op.Package $sop.Package)}}
{{ $found = true }}
{{end}}
{{end}}
{{ if not $found }}
api{{ pascalize $op.Package }} "{{$op.GenCommon.TargetImportPath}}/{{ $op.RootPackage }}/operations/{{ $op.Package }}"
{{end}}
{{end}}
"github.com/go-openapi/loads"
"github.com/vrischmann/envconfig"
"github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/app"
)
func main() {
...
api := operations.New{{ pascalize .Name }}API(swaggerSpec)
{{range .Operations}}
api.{{ pascalize .Package }}{{ pascalize .Name }}Handler = api{{ pascalize .Package }}.{{ pascalize .Name }}HandlerFunc(srv.{{ pascalize .Name }}Handler)
{{- end}}
...
}
تبدو الشفرة في الاستيراد معقدة ، على الرغم من أنها في الواقع مجرد قوالب Go-Templating وهياكل من مستودع go-swagger. وفي الدالة ،
main
نقوم فقط بتعيين الدوال المُولَّدة إلى المعالجات.
يبقى إنشاء الرمز الذي يشير إلى التكوين الخاص بنا:
> goswagger generate server \
-f ./swagger-api/swagger.yml \
-t ./internal/generated -C ./swagger-templates/default-server.yml \
--template-dir ./swagger-templates/templates \
--name example2
يمكن الاطلاع على النتيجة النهائية في مستودعنا .
ما حصلنا عليه:
- يمكننا استخدام هياكلنا للتطبيق والتكوينات وأي شيء نريده. الأهم من ذلك ، أنه من السهل جدًا تضمينه في الكود الذي تم إنشاؤه.
- يمكننا إدارة هيكل المشروع بمرونة ، وصولاً إلى أسماء الملفات الفردية.
- يبدو استخدام القوالب معقدًا ويتطلب بعض التعود عليه ، ولكنه بشكل عام أداة قوية جدًا.
الخطوة الثالثة. توليد العملاء
يسمح لنا Go-swagger أيضًا بإنشاء حزمة عميل لخدمتنا يمكن لخدمات Go الأخرى استخدامها. هنا لن أتطرق إلى إنشاء الكود بالتفصيل ، فالنهج هو نفسه تمامًا عند إنشاء كود من جانب الخادم.
بالنسبة لمشاريع Go ، من المعتاد وضع حزم عامة
./pkg
، سنفعل الشيء نفسه: نضع العميل لخدمتنا في pkg ، وننشئ الكود نفسه على النحو التالي:
> goswagger generate client -f ./swagger-api/swagger.yml -t ./pkg/example3
مثال على الكود الذي تم إنشاؤه هنا .
الآن يمكن لجميع مستهلكي خدمتنا استيراد هذا العميل لأنفسهم ، على سبيل المثال ، بالعلامة (على سبيل المثال ، ستكون العلامة
example3/pkg/example3/v0.0.1
).
يمكن تخصيص قوالب العميل ، على سبيل المثال ، للتدفق
open tracing id
من سياق إلى رأس.
الاستنتاجات
بطبيعة الحال ، يختلف تطبيقنا الداخلي عن الكود الموضح هنا بشكل أساسي بسبب استخدام الحزم الداخلية ومناهج CI (إجراء اختبارات ونُسَلات مختلفة). في الكود الذي تم إنشاؤه خارج الصندوق ، يتم تكوين مجموعة من المقاييس الفنية والعمل مع التكوينات والتسجيل. لقد قمنا بتوحيد جميع الأدوات الشائعة. نتيجة لذلك ، قمنا بتبسيط التطوير بشكل عام وإصدار خدمات جديدة على وجه الخصوص ، وضمان مرور أسرع لقائمة مراجعة الخدمة قبل النشر إلى المنتج.
دعنا نتحقق مما إذا كنا قد حققنا أهدافنا الأولية:
- تأكد من ملاءمة العقود الموضحة للخدمات ، وهذا من شأنه أن يسرع من تنفيذ الخدمات الجديدة ويبسط التواصل بين الفرق - نعم .
- HTTP ( event streaming) — .
- , .. Inner Source — .
- , — ( — Bitbucket).
- , — ( , , ).
- Go — ( ).
ربما طرح القارئ اليقظ السؤال: كيف تدخل ملفات القوالب في مشروعنا؟ نقوم الآن بتخزينها في كل مشروع من مشاريعنا. هذا يبسط العمل اليومي ، ويسمح لك بتخصيص شيء لمشروع معين. ولكن هناك وجهًا آخر للعملة: لا توجد آلية للتحديث المركزي للقوالب وتقديم ميزات جديدة تتعلق أساسًا بـ CI.
ملاحظة: إذا أعجبتك هذه المادة ، فسنقوم في المستقبل بإعداد مقال حول البنية القياسية لخدماتنا ، وسنخبرك بالمبادئ التي نستخدمها عند تطوير الخدمات في Go.