Golang API Framework

في عملية التعرف على Golang ، قررت أن أجعل إطار التطبيق ، والذي سيكون مناسبًا لي للعمل معه في المستقبل. كانت النتيجة ، في رأيي ، قطعة عمل جيدة ، قررت مشاركتها ، وفي الوقت نفسه ناقش اللحظات التي نشأت أثناء إنشاء الإطار.


صورة


من حيث المبدأ ، يشير تصميم لغة Go إلى أنه لا يحتاج إلى تقديم تطبيقات كبيرة الحجم (أعني الافتقار إلى الأدوية العامة وآلية معالجة الأخطاء ليست قوية جدًا). لكننا ما زلنا نعرف أن حجم التطبيقات عادة لا ينخفض ​​، ولكن في كثير من الأحيان العكس تماما. لذلك ، من الأفضل وضع إطار على الفور سيكون من الممكن توحيد وظائف جديدة دون التضحية بدعم الكود.


حاولت إدراج رمز أقل في المقالة ، وبدلاً من ذلك أضفت روابط إلى سطور محددة من التعليمات البرمجية على جيثب على أمل أن تكون الصورة أكثر ملاءمة.


أولاً ، لقد وضعت خطة لما يجب أن يكون في التطبيق. نظرًا لأنني سأتحدث في كل مقالة عن كل عنصر على حدة ، سأقدم أولاً العنصر الرئيسي من هذه القائمة باعتباره المحتوى.


  • اختيار مدير الحزم
  • اختر إطار عمل لإنشاء واجهة برمجة التطبيقات
  • اختر أداة لحقن التبعية (DI)
  • طرق طلب الويب
  • استجابات JSON / XML وفقًا لرؤوس الطلب
  • ORM
  • هجرة
  • جعل الطبقات الأساسية لطبقات النموذج الخدمة> مستودع> الكيان
  • مستودع CRUD الأساسي
  • خدمة CRUD الأساسية
  • تحكم CRUD الأساسي
  • طلب التحقق من الصحة
  • المتغيرات ومتغيرات البيئة
  • أوامر وحدة التحكم
  • تسجيل
  • التكامل المسجل مع ترقب أو نظام تنبيه آخر
  • ضبط التنبيه للأخطاء
  • اختبارات الوحدة مع إعادة تعريف الخدمات من خلال DI
  • النسبة المئوية واختبار خريطة رمز التغطية
  • اختيال
  • عامل الميناء يؤلف

مدير الحزمة


بعد قراءة الأوصاف للتطبيقات المختلفة ، اخترت govendor وفي الوقت الحالي كنت راضيًا عن هذا الاختيار. السبب بسيط - فهو يسمح لك بتثبيت التبعيات داخل الدليل مع التطبيق ، وتخزين المعلومات حول الحزم وإصداراتها.


يتم تخزين المعلومات حول الحزم وإصداراتها في ملف vendor.json واحد. هناك ناقص في هذا النهج أيضا. إذا قمت بإضافة حزمة مع التبعيات الخاصة بها ، ثم مع معلومات حول الحزمة ، فإن المعلومات حول التبعيات الخاصة بها ستدخل أيضًا في الملف. ينمو الملف بسرعة ولم يعد من الممكن تحديد التبعيات الرئيسية والمشتقات بشكل واضح.


في مؤلف PHP أو في npm ، يتم وصف التبعيات الرئيسية في ملف واحد ، ويتم تسجيل جميع التبعيات الرئيسية والمشتقة وإصداراتها تلقائيًا في ملف القفل. هذا النهج هو أكثر ملاءمة في رأيي. لكن في الوقت الحالي ، كان تنفيذ govendor كافيًا بالنسبة لي.


الإطار


من الإطار ، لست بحاجة إلى جهاز توجيه مناسب والتحقق من صحة الطلبات. كل هذا وجد في الجن الشعبي. توقف عن ذلك.


حقن التبعية


مع DI كان علي أن أعاني قليلاً. اختار أولا حفر. وفي البداية كان كل شيء رائعًا. الخدمات الموصوفة ، يحفر الحفر بشكل أكبر من التبعيات. ولكن اتضح بعد ذلك أنه لا يمكن إعادة تعريف الخدمات ، على سبيل المثال ، أثناء الاختبار. لذلك ، في النهاية ، توصلت إلى استنتاج مفاده أنني أخذت sarulabs حاوية خدمة بسيطة / دي .


كان علي فقط أن أتخلى عن ذلك ، لأنه من خارج الصندوق يسمح لك بإضافة خدمات وتمنع إعادة تعريفها. عند كتابة الاختبارات التلقائية ، في رأيي ، يكون أكثر ملاءمة لتهيئة الحاوية كما هو الحال في التطبيق ، ثم إعادة تعريف بعض الخدمات ، وتحديد كعب الروتين بدلاً من ذلك. في مفترق طرق ، أضاف طريقة لتجاوز وصف الخدمة.


ولكن في النهاية ، في حالة الحفر وفي حالة حاوية الخدمة ، اضطررت إلى وضع الاختبارات في حزمة منفصلة. خلاف ذلك ، اتضح أن الاختبارات يتم تشغيلها بشكل منفصل في حزم ( go test model/service ) ، لكنها لا تبدأ فورًا للتطبيق بالكامل ( go test ./... ) ، نظرًا لوجود تبعيات دورية تنشأ.


استجابات JSON / XML وفقًا لرؤوس الطلب


في Gin ، لم أجد هذا ، لذلك أضفت للتو طريقة إلى وحدة التحكم الأساسية التي تولد استجابة وفقًا لرأس الطلب.


 func (c BaseController) response(context *gin.Context, obj interface{}, code int) { switch context.GetHeader("Accept") { case "application/xml": context.XML(code, obj) default: context.JSON(code, obj) } } 

ORM


مع ORM لم يشعر العذاب الطويل في الاختيار. كان هناك الكثير للاختيار من بينها. ولكن وفقًا لوصف الميزات ، أعجبتني GORM ، التي تعد واحدة من أكثر الميزات شعبية في وقت الاختيار. هناك دعم ل DBMS الأكثر استخداما. على الأقل PostgreSQL و MySQL بالتأكيد هناك. كما أن لديها طرق لإدارة المخطط الأساسي الذي يمكنك استخدامه عند إنشاء عمليات الترحيل.


هجرة


بالنسبة للهجرات ، استقرت على عبوة gorm-goose . أضع حزمة منفصلة على مستوى العالم وابدأ الترحيل إليها. في البداية ، كان هذا التطبيق محرجًا ، حيث كان من الضروري وصف الاتصال بقاعدة البيانات في ملف db / dbconf.yml منفصل. ولكن اتضح بعد ذلك أن سلسلة الاتصال الموجودة بها يمكن وصفها بطريقة تؤخذ القيمة من متغير البيئة.


 development: driver: postgres open: $DB_URL 

وهذا مريح جدا. على الأقل من خلال عامل الإنشاء ، لم أكن مضطرًا لتكرار سلسلة الاتصال .


يدعم Gorm-goose أيضًا استرجاع الترحيل ، والذي أجده مفيدًا للغاية.


مستودع CRUD الأساسي


أفضل كل ما يشير إلى الموارد ليتم وضعها في طبقة مستودع منفصلة. في رأيي ، مع هذا النهج ، يكون رمز منطق العمل أكثر نظافة. في هذه الحالة ، يعرف رمز منطق العمل فقط أنه يحتاج إلى التعامل مع البيانات التي يأخذها من المستودع. وما يحدث في المستودع ، منطق الأعمال ليس مهمًا. يمكن أن يعمل المستودع مع قاعدة بيانات علائقية ، مع تخزين KV ، مع قرص ، أو ربما مع واجهة برمجة التطبيقات لخدمة أخرى. سيكون رمز منطق العمل هو نفسه في كل هذه الحالات.


يقوم مستودع CRUD بتنفيذ الواجهة التالية


 type CrudRepositoryInterface interface { BaseRepositoryInterface GetModel() (entity.InterfaceEntity) Find(id uint) (entity.InterfaceEntity, error) List(parameters ListParametersInterface) (entity.InterfaceEntity, error) Create(item entity.InterfaceEntity) entity.InterfaceEntity Update(item entity.InterfaceEntity) entity.InterfaceEntity Delete(id uint) error } 

وهذا يعني أن CRUD يطبق عمليات Create() ، Find() ، List() ، Update() ، Delete() وطريقة GetModel() .


حول GetModel () . يوجد مستودع CrudRepository أساسي CrudRepository بتنفيذ عمليات CRUD الأساسية. في المستودعات التي قامت بتضمينها في نفسها ، يكفي الإشارة إلى النموذج الذي يجب أن تعمل به. للقيام بذلك ، يجب أن ترجع الأسلوب GetModel() نموذج GORM. ثم كان علينا استخدام نتيجة GetModel() باستخدام الانعكاس في طرق CRUD.


على سبيل المثال


 func (c CrudRepository) Find(id uint) (entity.InterfaceEntity, error) { item := reflect.New(reflect.TypeOf(c.GetModel()).Elem()).Interface() err := c.db.First(item, id).Error return item, err } 

هذا هو ، في الواقع ، في هذه الحالة كان من الضروري التخلي عن الكتابة الثابتة لصالح الكتابة الديناميكية. في مثل هذه اللحظات ، يكون هناك نقص في الأدوية الوراثية في اللغة.


من أجل المستودعات التي تعمل مع نماذج محددة لتطبيق القواعد الخاصة بها لتصفية القوائم في طريقة List() ، قمت أولاً بتطبيق الربط المتأخر بحيث يتم استدعاء الطريقة المسؤولة عن بناء الاستعلام من طريقة List() . ويمكن تنفيذ هذه الطريقة في مستودع محدد. من الصعب التخلي عن أنماط التفكير التي تشكلت عند العمل بلغات أخرى. ولكن ، بالنظر إلى ذلك بنظرة جديدة ، وتقدير "أناقة" المسار المختار ، ثم أعاده إلى مقاربة أقرب إلى Go. للقيام بذلك ، ببساطة في CrudRepository خلال الواجهة يتم الإعلان عن منشئ الاستعلام ، والذي يتم List() بالفعل List() .


 listQueryBuilder ListQueryBuilderInterface 

اتضح مضحك جدا. إن قصر اللغة على الربط المتأخر ، والذي يبدو في البداية وكأنه عيب ، يشجع على الفصل الواضح بين الشفرة.


خدمة CRUD الأساسية


لا يوجد شيء مثير للاهتمام هنا ، لأنه لا يوجد منطق عمل في الإطار. يتم استدعاء مكالمات طرق CRUD إلى المستودع.


في طبقة الخدمات ، يجب تنفيذ منطق الأعمال.


تحكم CRUD الأساسي


تطبق وحدة التحكم أساليب CRUD . يقومون بمعالجة المعلمات من الطلب ، ويتم نقل التحكم إلى طريقة الخدمة المقابلة ، وبناءً على استجابة الخدمة ، يتم تشكيل استجابة للعميل.


مع وحدة التحكم كان لدي نفس القصة كما هو الحال مع مستودع بشأن قوائم التصفية. كنتيجة لذلك ، قمت بإعادة تنفيذ التطبيق مع الربط المتأخر محلي الصنع وأضفت جهاز هيدروليكي ، والذي ، بناءً على معلمات الطلب ، يشكل هيكلًا مع معلمات لتصفية القائمة.


في جهاز التحكم المائي الذي يأتي مع وحدة تحكم CRUD ، تتم معالجة معلمات ترقيم الصفحات فقط. في وحدات التحكم المحددة التي يتم فيها دمج وحدة التحكم CRUD ، يمكن إعادة تعريف جهاز إعادة الترطيب .


طلب التحقق من الصحة


يتم التحقق من صحة بواسطة الجن. على سبيل المثال ، عند إضافة سجل (طريقة ( Create() ) ، يكفي أن تزين عناصر هيكل الكيان


 Name string `binding:"required"` 

ShouldBindJSON() طريقة ShouldBindJSON() الخاصة ShouldBindJSON() بفحص معلمات الطلب للتأكد من توافقها مع المتطلبات الموضحة في الديكور.


المتغيرات ومتغيرات البيئة


أعجبني حقًا تطبيق Viper ، خاصةً بالاشتراك مع Cobra.


قراءة التكوين الذي وصفته في main.go. يتم وصف المعلمات الأساسية التي لا تحتوي على أسرار في ملف base.env . يمكنك تجاوزها في ملف .env الذي تمت إضافته إلى .gitignore. في .env ، يمكنك وصف القيم السرية للبيئة.


متغيرات البيئة لها أولوية أعلى.


أوامر وحدة التحكم


لوصف أوامر وحدة التحكم ، اخترت كوبرا . مما هو جيد لاستخدام كوبرا جنبا إلى جنب مع الافعى. يمكننا وصف الأمر


 serverCmd.PersistentFlags().StringVar(&serverPort, "port", defaultServerPort, "Server port") 

وربط متغير البيئة بقيمة المعلمة الأمر


 viper.BindPFlag("SERVER_PORT", serverCmd.PersistentFlags().Lookup("port")) 

في الواقع ، فإن التطبيق الكامل لهذا الإطار هو وحدة التحكم. يتم تشغيل خادم الويب بواسطة أحد أوامر وحدة التحكم بالخادم.


 gin -i run server 

تسجيل


لقد اخترت حزمة logrus للتسجيل ، حيث يوجد كل ما أحتاج إليه عادة: تعيين مستويات التسجيل ، ومكان تسجيل الدخول ، وإضافة الخطافات ، على سبيل المثال ، لإرسال السجلات إلى نظام التنبيه.


التكامل المسجل مع نظام الإنذار


لقد اخترت Sentry ، لأن كل شيء أصبح بسيطًا جدًا بفضل التكامل التام مع logrus: logrus_sentry . لقد قمت بعمل المعلمات باستخدام عنوان url الخاص بـ Sentry SENTRY_DSN المحددة للإرسال إلى Sentry SENTRY_TIMEOUT . اتضح أن المهلة صغيرة ، إن لم تكن مخطئة ، 300 مللي ثانية ، ولم يتم تسليم العديد من الرسائل.


ضبط التنبيه للأخطاء


لقد قمت بمعالجة حالة من الذعر بشكل منفصل لخادم الويب ولأوامر وحدة التحكم .


اختبارات الوحدة مع إعادة تعريف الخدمات من خلال DI


كما ذكر أعلاه ، كان لا بد من تخصيص حزمة منفصلة لاختبارات الوحدة. نظرًا لأن المكتبة المحددة لإنشاء حاوية خدمة لم تسمح بإعادة تعريف الخدمات ، أضافت الشوكة طريقة لإعادة تعريف وصف الخدمات. نتيجة لهذا ، في اختبار الوحدة ، يمكنك استخدام نفس وصف الخدمات كما في التطبيق


 dic.InitBuilder() 

وإعادة تعريف فقط بعض أوصاف الخدمة في بذرة بهذه الطريقة


 dic.Builder.Set(di.Def{ Name: dic.UserRepository, Build: func(ctn di.Container) (interface{}, error) { return NewUserRepositoryMock(), nil }, }) 

بعد ذلك ، يمكنك إنشاء حاوية واستخدام الخدمات اللازمة في الاختبار:


 dic.Container = dic.Builder.Build() userService := dic.Container.Get(dic.UserService).(service.UserServiceInterface) 

وبالتالي ، سنختبر خدمة المستخدم ، والتي بدلاً من المستودع الحقيقي ستستخدم كعب الروتين المتوفر.


النسبة المئوية واختبار خريطة رمز التغطية
كنت راضيا تماما عن أداة الاختبار القياسية.


يمكنك إجراء الاختبارات بشكل فردي


 go test test/unit/user_service_test.go -v 

يمكنك تشغيل جميع الاختبارات في وقت واحد


 go test ./... -v 

يمكنك إنشاء خريطة تغطية وحساب النسبة المئوية للتغطية


 go test ./... -v -coverpkg=./... -coverprofile=coverage.out 

وشاهد خريطة تغطية الرمز مع الاختبارات في المستعرض


 go tool cover -html=coverage.out 

اختيال


يوجد مشروع gin-swagger لـ Gin ، والذي يمكن استخدامه لإنشاء مواصفات لـ Swagger ولإنشاء الوثائق بناءً عليه. ولكن ، كما اتضح فيما بعد ، من أجل إنشاء مواصفات لعمليات محددة ، من الضروري الإشارة إلى تعليقات على وظائف محددة لوحدة التحكم. تبين أن هذا ليس مناسبًا جدًا بالنسبة لي ، حيث أنني لم أرغب في تكرار كود عمليات CRUD في كل وحدة تحكم. بدلاً من ذلك ، في وحدات التحكم المحددة ، قمت ببساطة بتضمين وحدة تحكم CRUD كما هو موضح أعلاه. لم أكن أريد حقًا إنشاء وظائف كعب روتين لهذا أيضًا.


لذلك ، توصلت إلى استنتاج مفاده أنه يتم إنشاء المواصفات باستخدام goswagger ، لأنه في هذه الحالة يمكن وصف العمليات دون ربطها بوظائف محددة .


 swagger generate spec -o doc/swagger.yml 

بالمناسبة ، مع goswagger ، يمكنك الانتقال من الجهة الأخرى وإنشاء رمز خادم الويب استنادًا إلى مواصفات Swagger. ولكن مع هذا النهج ، كانت هناك صعوبات في استخدام ORM ، وتجاهلت في النهاية.


يتم إنشاء الوثائق باستخدام gin-swagger ، لهذا يشار إلى ملف مواصفات تم إنشاؤه مسبقًا.


عامل الميناء يؤلف


في الإطار ، أضفت وصفًا لحاويتين - للرمز وللقاعدة . في بداية الحاوية مع الرمز ، ننتظر حتى يتم تشغيل الحاوية مع القاعدة بالكامل. وفي كل بداية ، نقوم بترحيل عمليات الترحيل إذا لزم الأمر. يتم وصف معلمات الاتصال بقاعدة البيانات لعمليات الترحيل ، على النحو المذكور أعلاه ، في dbconf.yml ، حيث كان من الممكن استخدام متغير البيئة لنقل إعدادات الاتصال بقاعدة البيانات.


شكرا لاهتمامكم في هذه العملية ، كان علي أن أتكيف مع ميزات اللغة. سأكون مهتمًا بمعرفة رأي الزملاء الذين قضوا وقتًا أطول مع Go. بالتأكيد يمكن جعل بعض اللحظات أكثر أناقة ، لذلك سأكون سعيدًا بالنقد المفيد. رابط إلى الإطار: https://github.com/zubroide/go-api-boilerplate

Source: https://habr.com/ru/post/ar455302/


All Articles