من فضلك تسجيل الدخول أو تسجيل لتفعل ذلك.

يوليو 2023

اذهب إلى: ملخص تقني | معالجة الأخطاء | أداء | مقابل بايثون | خاتمة

منذ بضع سنوات كتبت قزم، برنامج Python صغير يكفي لعميل Git لإنشاء مستودع وإضافة بعض الالتزامات ودفع نفسه إلى GitHub.

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

النتيجه هي com.gogit، وهو برنامج Go مكون من 400 سطر يمكنه تهيئة المستودع والالتزام والدفع إلى GitHub. إنه مكتوب بلغة Go العادية… باستثناء معالجة الأخطاء، وهو أمر مطول للغاية في لغة Go الاصطلاحية بحيث لا يعمل بشكل جيد في البرمجة النصية (المزيد حول ذلك) أقل).

ملخص تقني

لن أخوض في التفاصيل حول كيفية عمل Git هنا (هناك المزيد في ملفي مقالة قذرة)، ويكفي أن نقول أن نموذج بيانات Git أنيق جدًا. ويستخدم مخزن كائنات بسيطًا يعتمد على الملفات في .git/objects، حيث يحتوي كل كائن على تجزئة مكونة من 40 حرفًا ويمكن أن يكون يقترف، أ شجرة (قائمة الدليل)، أو أ سائل لزج (ملف ملتزم). هذا كل شيء – رمز جوجيت لكتابة الالتزامات والأشجار والنقط حوالي 50 سطرًا.

لقد قمت بتنفيذ أقل من pygit: فقط init, commit، و push. لا يدعم Gogit حتى الفهرس (منطقة التدريج)، لذا بدلاً من ذلك gogit add، انت فقط gogit commit مع قائمة المسارات التي تريد الالتزام بها في كل مرة. مثل رمز بيجيت يظهر أن التعامل مع الفهرس فوضوي. كما أنه غير ضروري، وأردت أن يكون gogit تمرينًا على البساطة.

يقوم Gogit أيضًا بإسقاط الأوامر cat-file, hash-object، و diff – هذه ليست مطلوبة للالتزام والدفع إلى GitHub. لقد استخدمت Git cat-file أثناء التصحيح، ولكن.

فيما يلي الأوامر التي استخدمتها لإنشاء الريبو والالتزام والدفع إلى GitHub (لاحظ استخدام go run لتجميع وتنفيذ “البرنامج النصي”):

# Initialise the repo$ go run . init# Make the first commit (other commits are similar)$ export GIT_AUTHOR_NAME='Ben Hoyt'$ export GIT_AUTHOR_EMAIL=benhoyt@gmail.com$ go run . commit -m 'Initial commit' gogit.go go.mod LICENSE.txtcommited 0580a17 to master# Push updates to GitHub$ export GIT_USERNAME=benhoyt$ export GIT_PASSWORD=...$ go run . push https://github.com/benhoyt/gogitupdating remote master from 0000000 to 0580a17 (5 objects)

معالجة الأخطاء

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

mode, err := strconv.ParseInt(modeStr, 8, 64)if err != nil {    return err}

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

mode, err := strconv.ParseInt(modeStr, 8, 64)if err != nil {    return fmt.Errorf("mode must be an octal number, not %q", modeStr)}

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

func check0(err error) {    if err != nil {        panic(err)    }}func check[T any](value T, err error) T {    if err != nil {        panic(err)    }    return value}func assert(cond bool, format string, args ...any) {    if !cond {        panic(fmt.Sprintf(format, args...))    }}

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

يسمح لك هؤلاء المساعدون بتحويل هذا الرمز:

func writeTree(paths []string) ([]byte, error) {    sort.Strings(paths) // tree object needs paths sorted    var buf bytes.Buffer    for _, path := range paths {        st, err := os.Stat(path)        if err != nil {            return nil, err        }        if st.IsDir() {            panic("sub-trees not supported")        }        data, err := os.ReadFile(path)        if err != nil {            return nil, err        }        hash, err := hashObject("blob", data)        if err != nil {            return nil, err        }        fmt.Fprintf(&buf, "%o %sx00%s", st.Mode().Perm()|0o100000, path, hash)    }    return hashObject("tree", buf.Bytes())}

فيما يلي، تقليل نص الدالة من 21 إلى 10 سطر، وهو ما يشبه إيجاز بايثون:

func writeTree(paths []string) []byte {    sort.Strings(paths) // tree object needs paths sorted    var buf bytes.Buffer    for _, path := range paths {        st := check(os.Stat(path))        assert(!st.IsDir(), "sub-trees not supported")        data := check(os.ReadFile(path))        hash := hashObject("blob", data)        fmt.Fprintf(&buf, "%o %sx00%s", st.Mode().Perm()|0o100000, path, hash)    }    return hashObject("tree", buf.Bytes())}

انها ليست مثالية، لأن الكلمة check يحجب الوظيفة التي تتصل بها قليلاً، ولكنه يجعل كتابة البرامج النصية السريعة والقذرة أجمل كثيرًا.

حتى أنك تحصل على أخطاء “أفضل” من الأخطاء العادية return errلأن تتبع المكدس يوضح لك بالضبط الوظيفة وسطر التعليمات البرمجية الذي تم تنفيذه:

$ go run . push https://github.com/benhoyt/gogitpanic: Get "https://github.com/benhoyt/gogit/info/refs?service=git-receive-pack":    context deadline exceeded (Client.Timeout exceeded while awaiting headers)goroutine 1 [running]:main.check[...](...)    /home/ben/h/gogit/gogit.go:94main.getRemoteHash(0x416ad0?, {0x7ffe1f0152d9?, 0x4b87d4?}, {0xc00001c00d, 0x7}, {0xc00001a00d, 0x28})    /home/ben/h/gogit/gogit.go:245 +0x6damain.push({0x7ffe1f0152d9, 0x20}, {0xc00001c00d, 0x7}, {0xc00001a00d, 0x28})    /home/ben/h/gogit/gogit.go:217 +0xd9main.main()    /home/ben/h/gogit/gogit.go:73 +0x21eexit status 2

التغيير من return err ل check تم تقليل عدد أسطر التعليمات البرمجية من 607 إلى 415، أي انخفاض بنسبة 32%.

إذا كنت ترغب في متابعة هذا النهج بشكل أكبر، فهناك مكتبة كتبها جو تساي وجوش بليشر سنايدر تسمى try الذي يستخدم recover للقيام بذلك “بشكل صحيح”. الاشياء! ما زلت آمل أن يكتشف فريق Go طريقة لجعل معالجة الأخطاء أقل تفصيلاً.

أداء

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

يعد استخدام الذاكرة جانبًا آخر من جوانب الأداء. مرة أخرى، نحن نتعامل مع ملفات صغيرة هنا، لذلك لا توجد مشكلة في قراءة كل شيء في الذاكرة. في Python، يمكنك إجراء البث، لكن الأمر ليس سهلاً دائمًا كما هو الحال في Go، نظرًا لما هو مذهل io.Reader و io.Writer واجهات.

ومع ذلك، لا يزال الأمر أسهل قليلًا في Go لقراءة كل شيء []byte أو string وإجراء العمليات الجراحية عليها، وهذا ما قمت به في gogit. نحن نتحدث عن بضعة كيلو بايت من الذاكرة، وجهازي يحتوي على بضعة جيجا بايت.

مقارنة مع نسخة بايثون

في الوقت الحالي، يتكون Pygit من حوالي 600 سطر من التعليمات البرمجية، وgogit حوالي 400 سطر من التعليمات البرمجية. ومع ذلك، هذا مضلل بعض الشيء، حيث قمت بإزالة العديد من الميزات عند كتابة إصدار Go: لا يوجد دعم لفهرس Git، ولا يوجد cat-file, hash-object، أو diff.

لقد أجريت اختبارًا سريعًا عن طريق إزالة هذه الوظائف من إصدار Python، وانتهى الأمر بـ 360 سطرًا من التعليمات البرمجية. أنا أعتبر 400 في Go مقابل 360 في Python ليس سيئًا – فهو أطول بنسبة 10% فقط. ويتضمن إصدار Go 20 سطرًا من الواردات و20 سطرًا لوظائف الفحص/التأكيد. لذا فهما متطابقان تقريبًا في الحجم!

دعونا نلقي نظرة على بضع وظائف محددة. أولاً، find_object، والذي يبحث في مخزن كائنات Git للعثور على كائن بالبادئة المحددة. إليك نسخة بايثون:

def find_object(sha1_prefix):    obj_dir = os.path.join('.git', 'objects', sha1_prefix[:2])    rest = sha1_prefix[2:]    objects = [name for name in os.listdir(obj_dir) if name.startswith(rest)]    if not objects:        raise ValueError('object {!r} not found'.format(sha1_prefix))    if len(objects) >= 2:        raise ValueError('multiple objects ({}) with prefix {!r}'.format(                len(objects), sha1_prefix))    return os.path.join(obj_dir, objects[0])

وإليك نسخة Go:

func findObject(hashPrefix string) string {    objDir := filepath.Join(".git/objects", hashPrefix[:2])    rest := hashPrefix[2:]    entries, _ := os.ReadDir(objDir)    var matches []string    for _, entry := range entries {        if strings.HasPrefix(entry.Name(), rest) {            matches = append(matches, entry.Name())        }    }    assert(len(matches) > 0, "object %q not found", hashPrefix)    assert(len(matches) == 1, "multiple objects with prefix %q", hashPrefix)    return filepath.Join(objDir, matches[0])}

هناك الكثير من الأشياء المتشابهة، على سبيل المثال os.path.join ضد filepath.Join, os.listdir ضد os.ReadDir، وما إلى ذلك وهلم جرا. لكن لاحظ أن فهم القائمة في بايثون – سطر واحد – يتكون من خمسة أسطر for حلقة في الذهاب. أفتقد فهم القائمة عند البرمجة النصية في Go…

دعونا ننظر إلى واحد آخر، commit الدالة الأولى في بايثون:

def commit(message, author):    tree = write_tree()    parent = get_local_master_hash()    timestamp = int(time.mktime(time.localtime()))    utc_offset = -time.timezone    author_time = '{} {}{:02}{:02}'.format(            timestamp,            '+' if utc_offset > 0 else '-',            abs(utc_offset) // 3600,            (abs(utc_offset) // 60) % 60)    lines = ['tree ' + tree]    if parent:        lines.append('parent ' + parent)    lines.append('author {} {}'.format(author, author_time))    lines.append('committer {} {}'.format(author, author_time))    lines.append('')    lines.append(message)    lines.append('')    data = 'n'.join(lines).encode()    sha1 = hash_object(data, 'commit')    master_path = os.path.join('.git', 'refs', 'heads', 'master')    write_file(master_path, (sha1 + 'n').encode())    return sha1

ثم في الذهاب:

func commit(message, author string, paths []string) string {    tree := writeTree(paths)    var buf bytes.Buffer    fmt.Fprintln(&buf, "tree", hex.EncodeToString(tree))    parent := getLocalHash()    if parent != "" {        fmt.Fprintln(&buf, "parent", parent)    }    now := time.Now()    offset := now.Format("-0700")    fmt.Fprintln(&buf, "author", author, now.Unix(), offset)    fmt.Fprintln(&buf, "committer", author, now.Unix(), offset)    fmt.Fprintln(&buf)    fmt.Fprintln(&buf, message)    data := buf.Bytes()    hash := hashObject("commit", data)    check0(os.WriteFile(".git/refs/heads/master", []byte(hex.EncodeToString(hash)+"n"), 0o664))    return hex.EncodeToString(hash)}

ومن المثير للاهتمام أن إصدار Python هذه المرة أطول: 23 سطرًا مقابل 19 سطرًا في Go. ويرجع هذا في الغالب إلى التعامل الأفضل مع الطوابع الزمنية. مكتبة Go القياسية ليست مثالية، ولكنها كذلك time الحزمة أفضل من حزمة بايثون time و datetime الحزم مجتمعة.

بشكل عام، تبدو مكتبة Go القياسية أكثر تماسكًا وأفضل تصميمًا من مكتبة Python، والتي تبدو وكأنها صممت من قبل العديد من الأشخاص المختلفين على مدار عدة عقود (لأنها كانت كذلك بالفعل).

خاتمة

عند استخدامه مع panicاستنادًا إلى معالجة الأخطاء، يُعد Go مفيدًا لكتابة نصوص برمجية سريعة لسطر الأوامر.

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

ومع ذلك، بالنسبة لأي شيء أكثر من مجرد نص سريع، سأنتقل بسرعة إلى Go. مكتبتها القياسية مصممة بشكل أفضل io.Reader و io.Writer الواجهات ممتازة، وتساعد كتابتها الثابتة خفيفة الوزن في اكتشاف الأخطاء دون إعاقة الطريق.

أحب ذلك إذا كنت رعتني على جيثب – سيحفزني ذلك على العمل في مشاريعي مفتوحة المصدر وكتابة المزيد من المحتوى الجيد. شكرًا!

اقرأ أكثر

سيحضر Duet AI من Google الاجتماعات نيابةً عنك أثناء نومك
تتهم شركة المحاماة Morgan & Morgan شركة التسويق بسرقة العملاء المحتملين

Reactions

0
0
0
0
0
0
بالفعل كان رد فعل لهذا المنصب.

ردود الفعل