تم الإعلان عن لغة Go لأول مرة في نهاية عام 2009 وتم إصدارها رسميًا في عام 2012 ، ولكن فقط في السنوات القليلة الماضية بدأت تحظى بتقدير جدي. كان دفعة واحدة من اللغات الأسرع نموا في 2018 و الثالثة معظم لغة البرمجة شعبية في 2019 .
نظرًا لأن لغة Go نفسها جديدة إلى حد ما ، فإن مجتمع المطورين ليس صارمًا للغاية بشأن كيفية كتابة التعليمات البرمجية. إذا نظرنا إلى اصطلاحات مماثلة في مجتمعات اللغات القديمة ، مثل Java ، فقد اتضح أن معظم المشاريع لها هيكل مماثل. يمكن أن يكون هذا مفيدًا جدًا عند كتابة قواعد أكواد كبيرة ، ومع ذلك ، قد يجادل الكثير بأنه سيكون له نتائج عكسية في السياقات العملية الحديثة. بينما ننتقل إلى كتابة الأنظمة الدقيقة والحفاظ على قواعد الرموز المدمجة نسبيًا ، تصبح مرونة Go في هيكلة المشاريع جذابة للغاية.
يعرف الجميع مثالاً مع hello world http على Golang ، ويمكن مقارنته بأمثلة مشابهة بلغات أخرى ، على سبيل المثال ، في Java... لا يوجد فرق كبير بين الأول والثاني ، لا في التعقيد ولا في مقدار الكود الذي يجب كتابته لتنفيذ المثال. لكن هناك اختلاف جوهري في النهج. يشجعنا Go على " كتابة رمز بسيط كلما أمكن ذلك ". بصرف النظر عن الجوانب الموجهة للكائنات في Java ، أعتقد أن أهم الوجبات الجاهزة من مقتطفات التعليمات البرمجية هذه هي: تتطلب Java مثيلًا منفصلاً لكل عملية (مثال
HttpServer) ، بينما يشجعنا Go على استخدام المفرد العالمي.
بهذه الطريقة يجب أن تحتفظ برمز أقل وتمرير روابط أقل فيه. إذا كنت تعلم أنه عليك فقط إنشاء خادم واحد (وهذا يحدث عادة) ، فلماذا تهتم بالكثير؟ تبدو هذه الفلسفة أكثر إقناعًا مع نمو قاعدة التعليمات البرمجية الخاصة بك. ومع ذلك ، فإن الحياة أحيانًا تثير مفاجآت: (. الحقيقة أنه لا يزال لديك عدة مستويات من التجريد للاختيار من بينها ، وإذا قمت بدمجها بشكل غير صحيح ، فيمكنك صنع فخاخ خطيرة لنفسك.
ولهذا السبب أريد أن ألفت انتباهك إلى ثلاثة مقاربات لتنظيم وهيكلة كود Go. كل من هذه الأساليب تتضمن مستوى مختلف من التجريد ، وفي الختام ، سأقارن الثلاثة وأخبرك في حالات التطبيق الأكثر ملاءمة لكل من هذه الأساليب.
سنقوم بتنفيذ خادم HTTP يحتوي على معلومات حول المستخدمين (يُشار إليها باسم قاعدة البيانات الرئيسية في الشكل التالي) ، حيث يتم تعيين دور لكل مستخدم (دعنا نقول أساسيًا ، وسيطًا ، ومسؤولًا) ، وأيضًا تنفيذ قاعدة بيانات إضافية (في الشكل التالي ، يُشار إليها باسم قاعدة بيانات التكوين) ، والتي تحدد مجموعة حقوق الوصول المخصصة لكل من الأدوار (مثل القراءة والكتابة والتحرير). يجب أن يقوم خادم HTTP الخاص بنا بتنفيذ نقطة نهاية تعرض مجموعة حقوق الوصول التي يمتلكها المستخدم بالمعرف المحدد.
بعد ذلك ، لنفترض أن قاعدة بيانات التكوين تتغير بشكل غير متكرر وتستغرق وقتًا طويلاً للتحميل ، لذلك سنحتفظ بها في ذاكرة الوصول العشوائي ، ونقوم بتحميلها عند بدء تشغيل الخادم ، وتحديثها كل ساعة.
كل الأكواد موجودة في مستودع هذه المقالة الموجود على جيثب.
النهج الأول: حزمة واحدة
يستخدم نهج الحزمة الواحدة تسلسلًا هرميًا للأشقاء حيث يتم تنفيذ الخادم بالكامل داخل حزمة واحدة. كل التعليمات البرمجية .
تحذير: التعليقات في الكود مفيدة ومهمة لفهم مبادئ كل نهج.
/main.go
package main
import (
"net/http"
)
// ,
// , -,
// , .
var (
userDBInstance userDB
configDBInstance configDB
rolePermissions map[string][]string
)
func main() {
// ,
// ,
// .
//
// , , ,
// .
userDBInstance = &someUserDB{}
configDBInstance = &someConfigDB{}
initPermissions()
http.HandleFunc("/", UserPermissionsByID)
http.ListenAndServe(":8080", nil)
}
// , , .
func initPermissions() {
rolePermissions = configDBInstance.allPermissions()
go func() {
for {
time.Sleep(time.Hour)
rolePermissions = configDBInstance.allPermissions()
}
}()
}
/database.go
package main
// ,
// .
type userDB interface {
userRoleByID(id string) string
}
// `someConfigDB`.
//
// , MongoDB,
// `mongoConfigDB`.
// `mockConfigDB`.
type someUserDB struct {}
func (db *someUserDB) userRoleByID(id string) string {
// ...
}
type configDB interface {
allPermissions() map[string][]string //
}
type someConfigDB struct {}
func (db *someConfigDB) allPermissions() map[string][]string {
//
}
/handler.go
package main
import (
"fmt"
"net/http"
"strings"
)
func UserPermissionsByID(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query()["id"][0]
role := userDBInstance.userRoleByID(id)
permissions := rolePermissions[role]
fmt.Fprint(w, strings.Join(permissions, ", "))
}
يرجى ملاحظة: ما زلنا نستخدم ملفات مختلفة ، وهذا لفصل المخاوف. هذا يجعل الكود أكثر قابلية للقراءة ويسهل الحفاظ عليه.
النهج الثاني: الحزم المزدوجة
في هذا النهج ، دعنا نتعلم ما هو التجميع. يجب أن تكون الحزمة وحدها المسؤولة عن سلوك معين. هنا نسمح للحزم بالتفاعل مع بعضها البعض - وبالتالي علينا الحفاظ على كود أقل. ومع ذلك ، نحتاج إلى التأكد من أننا لا ننتهك مبدأ المسؤولية الفردية ، وبالتالي نضمن أن كل جزء من المنطق يتم تنفيذه بالكامل في حزمة منفصلة. آخر التوجيهي الهام لهذا النهج هو أن العودة لا يسمح تبعيات دائرية بين الحزم، تحتاج إلى إنشاء حزمة محايد الذي يحتوي فقط العارية واجهة تعريفات و حالات المفرد . سيؤدي ذلك إلى التخلص من تبعيات الحلقة. الكود كله...
/main.go
package main
// : main – ,
// .
import (
"github.com/myproject/config"
"github.com/myproject/database"
"github.com/myproject/definition"
"github.com/myproject/handler"
"net/http"
)
func main() {
// , ,
// , ,
// .
definition.UserDBInstance = &database.SomeUserDB{}
definition.ConfigDBInstance = &database.SomeConfigDB{}
config.InitPermissions()
http.HandleFunc("/", handler.UserPermissionsByID)
http.ListenAndServe(":8080", nil)
}
/definition/database.go
package definition
// , ,
// .
// , ;
// , , ,
// .
var (
UserDBInstance UserDB
ConfigDBInstance ConfigDB
)
type UserDB interface {
UserRoleByID(id string) string
}
type ConfigDB interface {
AllPermissions() map[string][]string //
}
/definition/config.go
package definition
var RolePermissions map[string][]string
/database/user.go
package database
type SomeUserDB struct{}
func (db *SomeUserDB) UserRoleByID(id string) string {
//
}
/database/config.go
package database
type SomeConfigDB struct{}
func (db *SomeConfigDB) AllPermissions() map[string][]string {
//
}
/config/permissions.go
package config
import (
"github.com/myproject/definition"
"time"
)
// ,
// config.
func InitPermissions() {
definition.RolePermissions = definition.ConfigDBInstance.AllPermissions()
go func() {
for {
time.Sleep(time.Hour)
definition.RolePermissions = definition.ConfigDBInstance.AllPermissions()
}
}()
}
/handler/user_permissions_by_id.go
package handler
import (
"fmt"
"github.com/myproject/definition"
"net/http"
"strings"
)
func UserPermissionsByID(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query()["id"][0]
role := definition.UserDBInstance.UserRoleByID(id)
permissions := definition.RolePermissions[role]
fmt.Fprint(w, strings.Join(permissions, ", "))
}
النهج الثالث: الحزم المستقلة
مع هذا النهج ، يتم تنظيم المشروع أيضًا في مجموعات. في هذه الحالة، يجب على كل حزمة دمج كل تبعياته محليا ، عبر واجهات و المتغيرات . وبالتالي ، فهي لا تعرف شيئًا على الإطلاق عن الحزم الأخرى . مع هذا النهج ، سيتم بالفعل تلطيخ الحزمة مع التعريفات المذكورة في النهج السابق بين جميع الحزم الأخرى ؛ تعلن كل حزمة عن واجهتها الخاصة لكل خدمة. للوهلة الأولى ، قد يبدو هذا تكرارًا مزعجًا ، لكنه في الواقع ليس كذلك. يجب على كل حزمة تستخدم خدمة أن تعلن عن واجهتها الخاصة ، والتي تحدد فقط ما تحتاجه من هذه الخدمة ولا شيء آخر. الكود كله...
/main.go
package main
// : – ,
// .
import (
"github.com/myproject/config"
"github.com/myproject/database"
"github.com/myproject/handler"
"net/http"
)
func main() {
userDB := &database.SomeUserDB{}
configDB := &database.SomeConfigDB{}
permissionStorage := config.NewPermissionStorage(configDB)
h := &handler.UserPermissionsByID{UserDB: userDB, PermissionsStorage: permissionStorage}
http.Handle("/", h)
http.ListenAndServe(":8080", nil)
}
/database/user.go
package database
type SomeUserDB struct{}
func (db *SomeUserDB) UserRoleByID(id string) string {
//
}
/database/config.go
package database
type SomeConfigDB struct{}
func (db *SomeConfigDB) AllPermissions() map[string][]string {
//
}
/config/permissions.go
package config
import (
"time"
)
// , ,
// , ,
// `AllPermissions`.
type PermissionDB interface {
AllPermissions() map[string][]string //
}
// ,
// , , ,
//
type PermissionStorage struct {
permissions map[string][]string
}
func NewPermissionStorage(db PermissionDB) *PermissionStorage {
s := &PermissionStorage{}
s.permissions = db.AllPermissions()
go func() {
for {
time.Sleep(time.Hour)
s.permissions = db.AllPermissions()
}
}()
return s
}
func (s *PermissionStorage) RolePermissions(role string) []string {
return s.permissions[role]
}
/handler/user_permissions_by_id.go
package handler
import (
"fmt"
"net/http"
"strings"
)
//
type UserDB interface {
UserRoleByID(id string) string
}
// ... .
type PermissionStorage interface {
RolePermissions(role string) []string
}
// ,
// , .
type UserPermissionsByID struct {
UserDB UserDB
PermissionsStorage PermissionStorage
}
func (u *UserPermissionsByID) ServeHTTP(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query()["id"][0]
role := u.UserDB.UserRoleByID(id)
permissions := u.PermissionsStorage.RolePermissions(role)
fmt.Fprint(w, strings.Join(permissions, ", "))
}
هذا كل شئ! لقد نظرنا إلى ثلاثة مستويات من التجريد ، أولها هو الأقل سمكًا ، ويحتوي على حالة عالمية ومنطق متقارب بإحكام ، ولكنه يوفر أسرع تنفيذ وأقل قدر من التعليمات البرمجية للكتابة والصيانة. الخيار الثاني هجين بشكل معتدل ، والثالث مستقل تمامًا ومناسب للاستخدام المتكرر ، ولكنه يأتي بأقصى جهد مع الدعم.
إيجابيات وسلبيات
النهج الأول: حزمة واحدة
ل
- كود أقل ، تنفيذ أسرع بكثير ، أعمال صيانة أقل
- لا توجد حزم ، مما يعني أنه لا داعي للقلق بشأن تبعيات الحلقة
- من السهل اختبارها حيث توجد واجهات خدمة. لاختبار جزء من المنطق ، يمكنك تحديد أي تنفيذ من اختيارك (ملموس أو تم الاستهزاء به) للمفرد ، ثم تشغيل منطق الاختبار.
ضد
- الحزمة الوحيدة أيضًا لا توفر الوصول الخاص ، فكل شيء مفتوح من كل مكان. نتيجة لذلك ، تزداد مسؤولية المطور. على سبيل المثال ، تذكر أنه لا يمكنك إنشاء مثيل لهيكل بشكل مباشر عندما تكون وظيفة المنشئ مطلوبة لتنفيذ منطق التهيئة.
- يمكن للحالة العامة (الحالات الفردية) إنشاء افتراضات غير محققة ، على سبيل المثال ، يمكن لمثيل مفرد غير مهيأ أن يؤدي إلى ذعر مؤشر فارغ في وقت التشغيل.
- نظرًا لأن المنطق مرتبط بإحكام ، فلا يمكن إعادة استخدام أي شيء في هذا المشروع بسهولة ، وسيكون من الصعب استخراج أي مكونات منه.
- عندما لا يكون لديك حزم تدير كل جزء من منطق بشكل مستقل ، يجب أن يكون المطور حذرًا للغاية ويضع كل أجزاء الكود بشكل صحيح ، وإلا فقد تحدث سلوكيات غير متوقعة.
النهج الثاني: الحزم المزدوجة
لكل
- عند تغليف مشروع ما ، يكون من الأنسب ضمان المسؤولية عن منطق معين داخل الحزمة ، ويمكن فرض ذلك باستخدام المترجم. بالإضافة إلى ذلك ، سنكون قادرين على استخدام الوصول الخاص والتحكم في عناصر الكود المفتوحة لنا.
- يتيح لك استخدام حزمة مع تعريفات العمل مع مثيلات فردية مع تجنب التبعيات الدائرية. بهذه الطريقة يمكنك كتابة تعليمات برمجية أقل ، وتجنب تمرير المراجع عند إدارة الحالات ، وتجنب إضاعة الوقت في المشكلات التي من المحتمل أن تنشأ أثناء التجميع.
- هذا النهج يفضي أيضًا إلى الاختبار ، نظرًا لوجود واجهات خدمة. مع هذا النهج ، يمكن إجراء اختبار داخلي لكل حزمة.
ضد
- هناك بعض النفقات العامة عند تنظيم مشروع في حزم - على سبيل المثال ، يجب أن يستغرق التنفيذ الأولي وقتًا أطول من نهج الحزمة الواحدة.
- يمكن أن يتسبب استخدام الحالة العامة (حالات فردية) مع هذا النهج أيضًا في حدوث مشكلات.
- ينقسم المشروع إلى حزم ، مما يسهل بشكل كبير استخراج العناصر الفردية وإعادة استخدامها. ومع ذلك ، فإن الحزم ليست مستقلة تمامًا لأنها تتفاعل جميعًا مع حزمة تعريف. مع هذا النهج ، لا يتم استخراج الكود وإعادة استخدامه تلقائيًا تمامًا.
النهج الثالث: حزم
الايجابيات المستقلة
- عند استخدام الحزم ، نضمن أن يتم تنفيذ منطق معين داخل حزمة واحدة ولدينا تحكم كامل في الوصول.
- يجب ألا يكون هناك تبعيات دائرية محتملة لأن الحزم قائمة بذاتها تمامًا.
- جميع الحزم قابلة للاسترداد وإعادة الاستخدام. في كل تلك الحالات عندما نحتاج إلى حزمة في مشروع آخر ، فإننا ببساطة ننقلها إلى مساحة مشتركة ونستخدمها دون تغيير أي شيء فيها.
- إذا لم تكن هناك دولة عالمية ، فلا توجد سلوكيات غير مقصودة.
- هذا النهج هو الأفضل للاختبار. يمكن اختبار كل حزمة بالكامل دون القلق من أنها قد تعتمد على حزم أخرى عبر واجهات محلية.
ضد
- هذا النهج أبطأ بكثير في التنفيذ من الأسلوبين السابقين.
- يحتاج الكثير من التعليمات البرمجية إلى الحفاظ عليها. نظرًا لأنه يتم تمرير الروابط ، يجب تحديث العديد من الأماكن بعد إجراء تغييرات رئيسية. أيضًا ، عندما يكون لدينا واجهات متعددة تقدم نفس الخدمة ، يتعين علينا تحديث تلك الواجهات في كل مرة نقوم فيها بإجراء تغييرات على تلك الخدمة.
استنتاجات وأمثلة للاستخدام
نظرًا لعدم وجود إرشادات لكتابة التعليمات البرمجية في Go ، فإنه يأخذ العديد من الأشكال والأشكال المختلفة ، ولكل خيار مزاياه المثيرة للاهتمام. ومع ذلك ، فإن خلط أنماط التصميم المختلفة يمكن أن يسبب مشاكل. لإعطائك فكرة عنهم ، قمت بتغطية ثلاث طرق مختلفة لكتابة وهيكلة كود Go.
إذن متى يجب استخدام كل نهج؟ أقترح هذا الترتيب:
النهج الأول : ربما يكون نهج الحزمة الواحدة هو الأنسب عند العمل في فرق صغيرة ذات خبرة عالية في مشاريع صغيرة تتطلب نتائج سريعة. هذا النهج أبسط وأكثر موثوقية لبداية سريعة ، على الرغم من أنه يتطلب اهتمامًا وتنسيقًا جادًا في مرحلة دعم المشروع.
النهج الثاني: يمكن تسمية نهج الحزمة المزدوجة بتوليف هجين للطريقتين الأخريين: من بين مزاياها البدء السريع نسبيًا وسهولة الدعم ، بينما في نفس الوقت ، فإنها تخلق شروطًا للالتزام الصارم بالقواعد. إنه مناسب للمشاريع الكبيرة نسبيًا والفرق الكبيرة ، لكن قابلية إعادة استخدام الكود محدودة وهناك بعض الصعوبات في الحفاظ عليه.
المقاربة الثالثة : نهج الحزم المستقلة هو الأكثر ملاءمة للمشاريع المعقدة في حد ذاتها ، طويلة الأجل ، التي طورتها فرق كبيرة ، وللمشاريع التي يوجد فيها أجزاء من المنطق تم إنشاؤها بهدف إعادة استخدامها. يستغرق تنفيذ هذا النهج وقتًا طويلاً ويصعب الحفاظ عليه.