Skip to content

الوحدات

كل لغة حديثة لديها أداة إدارة تبعيات ناضجة خاصة بها، مثل Gradle لـ Java، و Pip لـ Python، و Npm لـ NodeJs وغيرها. أداة إدارة تبعيات جيدة يمكن أن توفر وقتًا كبيرًا للمطورين وترفع كفاءة التطوير. ومع ذلك، في المراحل المبكرة من Go لم يكن هناك حل ناضج لإدارة التبعيات، حيث كانت جميع الأكواد مخزنة في دليل GOPATH، مما كان غير مناسب جدًا للمشاريع الهندسية، مع إصدارات فوضوية وتبعيات يصعب إدارتها. لحل هذه المشكلة، تنافس المطورون في المجتمعات المختلفة، وكان المشهد فوضويًا لفترة من الوقت، وخلال هذه الفترة ظهرت بعض الأدوات المتميزة مثل Vendor. حتى Go 1.11 أطلقت المسؤولون أخيرًا Go Mod هذه الأداة الرسمية لإدارة التبعيات، منهيةً بذلك الفوضوى السابقة، واستمرت في التحسين في الإصدارات اللاحقة، واستبدلت الأدوات القديمة. اليوم، عند كتابة هذا المقال، وصل إصدار Go إلى 1.20، وجميع مشاريع Go تقريبًا تستخدم Go Mod، لذا في هذه المقالة سأقدم Go Mod فقط. كتب المسؤولون أيضًا وثائق مفصلة جدًا لوحدات Go: Go Modules Reference.

كتابة الوحدات

Go Module يعتمد بشكل أساسي على VCS (نظام التحكم في الإصدارات)، فعند تنزيل التبعية، ما يتم تنفيذه فعليًا هو أوامر VCS، مثل git. لذا إذا أردت مشاركة المكتبة التي كتبتها، فقط تحتاج لتحقيق ثلاثة أشياء:

  • مستودع الكود المصدري يمكن الوصول إليه علنًا، و VCS واحد من التالي:
    • git
    • hg (Mercurial)
    • bzr (Bazaar)
    • svn
    • fossil
  • مشروع go mod متوافق مع المواصفات
  • يتوافق مع مواصفات الإصدار الدلالي

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

مستودع المثال: 246859/hello: say hello (github.com)

التحضير

قبل البدء، تأكد من أن إصدارك يدعم go mod بالكامل (go >= 1.17)، وتم تفعيل Go Module. نفذ الأمر التالي للتحقق من التفعيل:

bash
$ go env GO111MODULE

إذا لم يكن مفعلاً، نفذ الأمر التالي لتفعيل Go Module:

bash
$ go env -w GO111MODULE=on

الإنشاء

أولاً تحتاج مستودع كود مصدري يمكن الوصول إليه من الشبكة العامة، وهناك خيارات كثيرة، أنصح بـ Github. أنشئ مشروعًا جديدًا عليه، وسمّه hello. على الرغم من عدم وجود قيود خاصة على اسم المستودع، لكن يُنصح بعدم استخدام أحرف خاصة، لأن ذلك سيؤثر على اسم الوحدة.

بعد الإنشاء، يمكن رؤية أن URL المستودع هو https://github.com/246859/hello، واسم وحدة go المقابل هو github.com/246859/hello.

ثم استنسخه محليًا، وقم بتهيئة الوحدة من خلال أمر go mod init:

bash
$ git clone git@github.com:246859/hello.git
Cloning into 'hello'...
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (5/5), done.

$ cd hello && go mod init github.com/246859/hello
go: creating new go.mod: module github.com/246859/hello

الكتابة

ثم يمكن البدء في عمل التطوير، وظيفته بسيطة جدًا، دالة واحدة فقط:

go
// hello.go
package hello

import "fmt"

// Hello returns hello message
func Hello(name string) string {
        if name == "" {
                name = "world"
        }
        return fmt.Sprintf("hello %s!", name)
}

اكتب ملف اختبار لاختبار الوحدة:

go
// hello_test.go
package hello_test

import (
        "testing"
        "fmt"
        "github.com/246859/hello"
)

func TestHello(t *testing.T) {
        data := "jack"
        expected := fmt.Sprintf("hello %s!", data)
        result := hello.Hello(data)

        if result != expected {
                t.Fatalf("expected result %s, but got %s", expected, result)
        }

}

استمر في كتابة برنامج سطر أوامر لإخراج hello، وظيفته بسيطة جدًا أيضًا. بالنسبة لبرامج سطر الأوامر، حسب المواصفات يتم إنشاؤها في cmd/app_name/، لذا ملف برنامج hello لسطر الأوامر يُخزن في دليل cmd/hello/، ثم اكتب الكود ذي الصلة.

go
// cmd/hello/main.go
package main

import (
  "flag"
  "github.com/246859/hello"
  "os"
)

var name string

func init() {
  flag.StringVar(&name, "name", "world", "name to say hello")
}

func main() {
  flag.Parse()
  msg := hello.Hello(name)
  _, err := os.Stdout.WriteString(msg)
  if err != nil {
    os.Stderr.WriteString(err.Error())
  }
}

الاختبار

بعد الكتابة، قم بتنسيق الكود المصدري واختباره:

bash
$ go fmt && go vet ./...

$ go test -v .
=== RUN   TestHello
--- PASS: TestHello (0.00s)
PASS
ok      github.com/246859/hello 0.023s

تشغيل برنامج سطر الأوامر:

bash
$ go run ./cmd/hello -name jack
hello jack!

التوثيق

أخيرًا، تحتاج لكتابة README موجز وواضح لهذه المكتبة، ليعرف المطورون الآخرون كيفية استخدامها بنظرة واحدة:

markdown
# hello

just say hello

## Install

import code

```bash
go get github.com/246859/hello@latest
```

install cmd

```bash
go install github.com/246859/hello/cmd/hello@latest
```

## Example

Here's a simple example as follows:

```go
package main

import (
  "fmt"
  "github.com/246859/hello"
)

func main() {
  result := hello.Hello("jack")
  fmt.Println(result)
}
```

هذا ملف README بسيط، يمكنك أيضًا إثراؤه بنفسك.

الرفع

عند الانتهاء من كتابة واختبار كل الكود، يمكن تقديم التعديلات ودفعها إلى المستودع البعيد:

bash
$ git add go.mod hello.go hello_test.go cmd/ example/ README.md

$ git commit -m "chore(mod): mod init" go.mod
[main 5087fa2] chore(mod): mod init
 1 file changed, 3 insertions(+)
 create mode 100644 go.mod

$ git commit -m "feat(hello): complete Hello func" hello.go
[main 099a8bf] feat(hello): complete Hello func
 1 file changed, 11 insertions(+)
 create mode 100644 hello.go

$ git commit -m "test(hello): complete hello testcase" hello_test.go
[main 76e8c1e] test(hello): complete hello testcase
 1 file changed, 17 insertions(+)
 create mode 100644 hello_test.go

$ git commit -m "feat(hello): complete hello cmd" cmd/hello/
[main a62a605] feat(hello): complete hello cmd
 1 file changed, 22 insertions(+)
 create mode 100644 cmd/hello/main.go

$ git commit -m "docs(example): add hello example" example/
[main 5c51ce4] docs(example): add hello example
 1 file changed, 11 insertions(+)
 create mode 100644 example/main.go

$ git commit -m "docs(README): update README" README.md
[main e6fbc62] docs(README): update README
 1 file changed, 27 insertions(+), 1 deletion(-)

إجمالي ستة التزامات ليست كثيرة، بعد الانتهاء من التقديم، أنشئ علامة tag لأحدث التزام:

bash
$ git tag v1.0.0

$ git tag -l
v1.0.0

$ git log --oneline
e6fbc62 (HEAD -> main, tag: v1.0.0, origin/main, origin:HEAD) docs(README): update README
5c51ce4 docs(example): add hello example
a62a605 feat(hello): complete hello cmd
76e8c1e test(hello): complete hello testcase
099a8bf feat(hello): complete Hello func
5087fa2 chore(mod): mod init
1f422d1 Initial commit

أخيرًا ادفع إلى المستودع البعيد:

bash
$ git push --tags
Enumerating objects: 23, done.
Counting objects: 100% (23/23), done.
Delta compression using up to 16 threads
Compressing objects: 100% (17/17), done.
Writing objects: 100% (21/21), 2.43 KiB | 1.22 MiB/s, done.
Total 21 (delta 5), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (5/5), done.
To github.com:246859/hello.git
   1f422d1..e6fbc62    main -> main
  * [new tag]         v1.0.0 -> v1.0.0

بعد الدفع، أنشئ release لها (وجود tag يكفي، release فقط للتوافق مع مواصفات Github):

وبذلك، تكون كتابة الوحدة قد اكتملت، وهذا هو التدفق الأساسي لتطوير الوحدات، ويمكن للمطورين الآخرين استيراد الكود أو تثبيت أدوات سطر الأوامر من خلال اسم الوحدة.

الاستيراد

استيراد المكتبة من خلال go get:

bash
$ go get github.com/246859/hello@latest
go: downloading github.com/246859/hello v1.0.0
go: added github.com/246859/hello v1.0.0

تثبيت برنامج سطر الأوامر من خلال go install:

bash
$ go install github.com/246859/hello/cmd/hello@latest && hello -name jack
hello jack!

أو استخدم go run للتشغيل المباشر:

bash
$ go run -mod=mod github.com/246859/hello/cmd/hello -name jack
hello jack!

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

لمزيد من التفاصيل حول رفع الوحدات، انتقل إلى Add a package.

لمعرفة كيفية حذف معلومات الوحدة، انتقل إلى Removing a package.

تعيين الوكيل

على الرغم من أن Go لا تملك مستودعًا مركزيًا مثل Maven Repo أو PyPi أو NPM، إلا أن هناك مستودع وكيل رسمي: Go modules services (golang.org)، والذي يقوم بتخزين الوحدات التي قام المطورون بتنزيلها مؤقتًا حسب الإصدار واسم الوحدة. ومع ذلك، نظرًا لأن خوادمه منتشرة في الخارج، فإن سرعة الوصول للمستخدمين المحليين ليست جيدة، لذا نحتاج لتعديل عنوان الوكيل الافتراضي للوحدات. حاليًا، هناك عدة جهات تقدم خدمة جيدة محليًا:

هنا اختر وكيل Qiniu Cloud، نفذ الأمر التالي لتعديل وكيل Go، حيث direct تعني أنه عند فشل التنزيل عبر الوكيل، يتم تجاوز تخزين الوكيل المؤقت والوصول مباشرة إلى مستودع الكود المصدري:

sh
$ go env -w GOPROXY=https://goproxy.cn,direct

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

تنزيل التبعيات

بعد تعديل الوكيل، دعنا نثبت تبعية خارجية للتجربة. لدى Go موقع رسمي للبحث عن التبعيات: Go Packages.

الاستشهاد بالكود

ابحث في الموقع عن إطار الويب الشهير Gin:

ستظهر هنا العديد من نتائج البحث، عند استخدام التبعيات الخارجية، يجب الجمع بين عدد الاستشهادات ووقت التحديث لتحديد ما إذا كان سيتم استخدام هذه التبعية، هنا اختر الأول مباشرة:

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

هنا فقط انسخ عنوانها، ثم استخدم أمر go get في المشروع الذي أنشأته سابقًا، الأمر كالتالي:

sh
$ go get github.com/gin-gonic/gin

خلال العملية سيتم تنزيل العديد من التبعيات، طالما لم يكن هناك أخطاء فهذا يعني نجاح التنزيل:

sh
$ go get github.com/gin-gonic/gin
go: added github.com/bytedance/sonic v1.8.0
go: added github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311
go: added github.com/gin-contrib/sse v0.1.0
go: added github.com/gin-gonic/gin v1.9.0
...

بعد الانتهاء، اعرض ملف go.mod:

sh
$ cat go.mod
module golearn

go 1.20

require github.com/gin-gonic/gin v1.9.0

require (
  github.com/bytedance/sonic v1.8.0 // indirect
  ...
)

يمكن ملاحظة أن هناك أشياء كثيرة أكثر من ذي قبل، وستجد أيضًا أن هناك ملفًا جديدًا باسم go.sum في الدليل:

sh
$ ls
go.mod  go.sum  main.go

دعنا نؤجل هذا للحديث عنه لاحقًا، عدّل ملف main.go بالكود التالي:

go
package main

import (
  "github.com/gin-gonic/gin"
)

func main() {
  gin.Default().Run()
}

شغّل المشروع مرة أخرى:

sh
$ go run golearn
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

وبذلك، من خلال سطر كود واحد، تم تشغيل أبسط خادم ويب. عندما لا تحتاج تبعية معينة، يمكنك أيضًا استخدام أمر go get لحذفها، هنا مثال لحذف Gin:

sh
$ go get github.com/gin-gonic/gin@none
go: removed github.com/gin-gonic/gin v1.9.0

أضف @none بعد عنوان التبعية لحذفها، والنتيجة تشير أيضًا إلى نجاح الحذف، وفي هذه الحالة اعرض ملف go.mod مرة أخرى ستجد أنه لم يعد يحتوي على تبعية Gin.

عند الحاجة لترقية لأحدث إصدار، يمكنك إضافة لاحقة @latest، أو البحث بنفسك عن رقم إصدار Release المتاح:

sh
$ go get -u github.com/gin-gonic/gin@latest

تثبيت سطر الأوامر

أمر go install سيقوم بتنزيل التبعية الخارجية محليًا وتجميعها إلى ملف ثنائي، وبفضل سرعة تجميع go، هذه العملية عادة لا تستغرق وقتًا طويلاً، ثم سيقوم go بتخزينها في دليل $GOPATH/bin أو $GOBIN، لتتمكن من تنفيذ هذا الملف الثنائي عالميًا (بشرط أن تضيف هذه المسارات إلى متغيرات البيئة).

TIP

عند استخدام أمر install، يجب تحديد رقم الإصدار.

مثل تنزيل المصحح delve المكتوب بلغة Go:

bash
$ go install github.com/go-delve/delve/cmd/dlv@latest
go: downloading github.com/go-delve/delve v1.22.1
...

$ dlv -v
Error: unknown shorthand flag: 'v' in -v
Usage:
  dlv [command]

Available Commands:
  attach      Attach to running process and begin debugging.
  ...

إدارة الوحدات

كل ما ذكر أعلاه يتعلق بالاستخدام الأساسي لـ Go Mod، لكن في الواقع تعلم Go Mod فقط بهذا القدر لا يكفي تمامًا. تعريف المسؤول للوحدة هو: مجموعة من الحزم الموسومة بإصدار. في التعريف أعلاه، الحزمة مفهوم مألوف، أما الإصدار فيجب أن يتبع رقم الإصدار الدلالي، المعرّف بالتنسيق: v(major).(minor).(patch)، مثل رقم إصدار Go v1.20.1، رقم الإصدار الرئيسي هو 1، رقم الإصدار الفرعي هو 20، رقم الإصدار التصحيحي هو 1، والمجمع هو v1.20.1. فيما يلي شرح تفصيلي:

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

الأوامر الشائعة

الأمرالشرح
go mod downloadتنزيل حزم تبعيات المشروع الحالي
go mod editتعديل ملف go.mod
go mod graphإخراج رسم بياني لتبعيات الوحدة
go mod initتهيئة go mod في الدليل الحالي
go mod tidyتنظيف وحدات المشروع
go mod verifyالتحقق من صحة تبعيات المشروع
go mod whyشرح أين استُخدمت التبعيات في المشروع
go clean -modcacheحذف ذاكرة التخزين المؤقت لتبعيات المشروع
go list -mعرض الوحدات

انتقل إلى go mod cmd لمعرفة المزيد عن الأوامر

تخزين الوحدات

عند استخدام Go Mod لإدارة المشاريع، يتم تخزين ذاكرة التخزين المؤقت للوحدات افتراضيًا في دليل $GOPATH/pkg/mod، ويمكن أيضًا تعديل $GOMODCACHE لتحديد موقع تخزين آخر.

sh
$ go env -w GOMODCACHE=مسار_ذاكرة_التخزين_المؤقت_للوحدات_الخاص_بك

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

sh
$ go clean -modcache

في دليل $GOMODCACHE/cache/download تُخزن الملفات الأصلية للتبعيات، بما في ذلك ملفات الهاش، والحزم المضغوطة الأصلية وغيرها، كالمثال التالي:

bash
$ ls $(go env GOMODCACHE)/cache/download/github.com/246859/hello/@v -1
list
v1.0.0.info
v1.0.0.lock
v1.0.0.mod
v1.0.0.zip
v1.0.0.ziphash

شكل تنظيم التبعيات بعد فك الضغط كالتالي، وهو كود المصدر للوحدة المحددة:

bash
$ ls $(go env GOMODCACHE)/github.com/246859/hello@v1.0.0 -1
LICENSE
README.md
cmd/
example/
go.mod
hello.go
hello_test.go

اختيار الإصدار

عند اختيار إصدار التبعية في Go، يتم اتباع مبدأ اختيار الحد الأدنى للإصدار. فيما يلي مثال من الموقع الرسمي: الوحدة الرئيسية تستشهد بالإصدار 1.2 من الوحدة A والإصدار 1.2 من الوحدة B. في نفس الوقت، الإصدار 1.2 من الوحدة A يستشهد بالإصدار 1.3 من الوحدة C، والإصدار 1.2 من الوحدة B يستشهد بالإصدار 1.4 من الوحدة C. وكلا الإصدارين 1.3 و 1.4 من الوحدة C يستشهدان بالإصدار 1.2 من الوحدة D. وفقًا لمبدأ الحد الأدنى للإصدار المتاح، ستختار Go في النهاية الإصدارات A1.2 و B1.2 و C1.4 و D1.2. اللون الأزرق الفاتح يمثل ما تم تحميله من ملف go.mod، والمحدد يمثل الإصدار المختار نهائيًا.

قدم الموقع الرسمي أيضًا أمثلة أخرى، المعنى العام متشابه.

go.mod

كل مشروع Go Mod يُنشئ ملف go.mod، لذا فإن الإلمام بملف go.mod ضروري جدًا، لكن في معظم الحالات لا تحتاج لتعديل ملف go.mod يدويًا.

module golearn

go 1.20

require github.com/gin-gonic/gin v1.9.0

require (
   github.com/bytedance/sonic v1.8.0 // indirect
   ...
)

يمكن ملاحظة أن معظم عناوين التبعيات في الملف تحتوي على كلمات مثل github، وهذا لأنه لا توجد Go مستودع تبعيات عام، ومعظم المشاريع مفتوحة المصدر مستضافة على Github، وبعضها يستخدم مستودعات ذاتية، مثل google.golang.org/protobuf و golang.org/x/crypto. عادةً، هذه السلسلة من العناوين هي أيضًا أسماء وحدات مشروع Go، وهذا يطرح مشكلة: URL غير حساس لحالة الأحرف، لكن المجلدات التي تخزن التبعيات حساسة لحالة الأحرف، لذا go get github.com/gin-gonic/gin و go get github.com/gin-gonic/Gin يستشيران نفس التبعية لكن مسار التخزين المحلي مختلف. عند حدوث هذه الحالة، لن يتعامل Go مع الأحرف الكبيرة مباشرة كمسار تخزين، بل سيقوم بتحويلها إلى !حرف_صغير، مثل github.com\BurntSushi سيُحوّل إلى github.com\!burnt!sushi.

module

الكلمة المفتاحية module تعلن اسم الوحدة للمشروع الحالي، ويمكن أن يظهر كلمة module مرة واحدة فقط في ملف go.mod. مثال:

module golearn

يعني أن اسم الوحدة الحالية هو golearn، مثلًا افتح ملف go.mod لتبعية Gin ستجد اسم module الخاص بها:

module github.com/gin-gonic/gin

اسم وحدة Gin هو العنوان المستخدم عند تنزيل التبعية، وهذا هو التنسيق الموصى به عادة لأسماء الوحدات: النطاق/المستخدم/اسم_المستودع.

TIP

هناك نقطة يجب الانتباه لها: عندما يكون الإصدار الرئيسي أكبر من 1، يجب أن يظهر رقم الإصدار الرئيسي في اسم الوحدة، مثل:

github.com/my/example

إذا تمت ترقية الإصدار إلى v2.0.0، فيجب تعديل اسم الوحدة كالتالي:

github.com/my/example/v2

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

Deprecation

في السطر السابق لـ module، أضف تعليق Deprecated في البداية للإشارة إلى أن هذه الوحدة مهملة، مثل:

// Deprecated: use example.com/mod/v2 instead.
module example.com/mod

go

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

go 1.20

require

الكلمة المفتاحية require تعني الاستشهاد بتبعية خارجية، مثل:

require github.com/gin-gonic/gin v1.9.0

التنسيق هو require اسم_الوحدة رقم_الإصدار، عند وجود استشهادات متعددة يمكن استخدام الأقواس:

require (
   github.com/bytedance/sonic v1.8.0 // indirect
)

وجود تعليق // indirect يعني أن هذه التبعية لم يتم الاستشهاد بها مباشرة من المشروع الحالي، وقد تكون استُشهدت من تبعيات استشهد بها المشروع مباشرة، لذا بالنسبة للمشروع الحالي هي استشهاد غير مباشر. ذكرنا سابقًا أنه عند تغيير الإصدار الرئيسي يجب أن يظهر في اسم الوحدة، الوحدات التي لا تتبع هذه القاعدة تسمى وحدات غير قياسية، عند require سيتم إضافة تعليق incompatible:

require example.com/m v4.1.2+incompatible

الإصدار الزائف

في ملف go.mod أعلاه، يمكن ملاحظة أن بعض إصدارات حزم التبعية ليست أرقام إصدارات دلالية، بل سلسلة غير مفهومة، وهي في الواقع CommitID المقابل للإصدار. رقم الإصدار الدلالي عادة يشير إلى Release معين. رقم الإصدار الزائف يمكن أن يحدد Commit معين، عادة بالتنسيق vx.y.z-yyyyMMddHHmmss-CommitId، وبما أن vx.y.z قد لا يكون موجودًا فعليًا، لذا يسمى إصدار زائف، مثل v0.0.0 في المثال التالي غير موجود، والمفعول حقًا هو الـ 12 رقمًا التالية من CommitID.

// CommitID عادة يؤخذ أول 12 رقمًا
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect

بنفس الطريقة، عند تنزيل التبعية يمكن أيضًا تحديد CommitID بدلاً من رقم الإصدار الدلالي:

go get github.com/chenzhuoyu/base64x@fe3a3abad311

exclude

الكلمة المفتاحية exclude تعني عدم تحميل تبعية الإصدار المحدد، إذا كان هناك require يستشهد نفس الإصدار من التبعية، سيتم تجاهله أيضًا. هذه الكلمة المفتاحية تعمل فقط في الوحدة الرئيسية. مثل:

exclude golang.org/x/net v1.2.3

exclude (
    golang.org/x/crypto v1.4.5
    golang.org/x/text v1.6.7
)

replace

replace ستستبدل تبعية الإصدار المحدد، يمكن استخدام مسار الوحدة والإصدار للاستبدال أو مسار ملف محدد من منصة أخرى، مثال:

text
replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5

replace (
    golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
    golang.org/x/net => example.com/fork/net v1.4.5
    golang.org/x/net v1.2.3 => ./fork/net
    golang.org/x/net => ./fork/net
)

فقط الإصدار على يسار => يتم استبداله، نفس التبعية بإصدارات أخرى يمكن الوصول إليها بشكل طبيعي. سواء استخدام مسار محلي أو مسار وحدة لتحديد الاستبدال، إذا كانت الوحدة المستبدلة لديها ملف go.mod، يجب أن يتطابق توجيه module مع مسار الوحدة المستبدلة.

retract

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

سحب بعض الإصدارات:

text
retract (
    v1.0.0 // Published accidentally.
    v1.0.1 // Contains retractions only.
)

سحب نطاق من الإصدارات:

text
retract v1.0.0
retract [v1.0.0, v1.9.9]
retract (
    v1.0.0
    [v1.0.0, v1.9.9]
)

go.sum

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

دعنا نرى ما يفعله Go من البداية للنهاية عند تنزيل تبعية، أولاً استخدم الأمر التالي لتنزيل تبعية:

go get github.com/bytedance/sonic v1.8.0

أمر go get أولاً سيقوم بتنزيل حزمة التبعية إلى دليل الذاكرة المؤقتة المحلية، عادة هذا الدليل هو $GOMODCACHE/cache/download/، ويقسم هذا الدليل حسب النطاق إلى حزم تبعيات من مواقع مختلفة، لذا قد ترى هيكل الدليل التالي:

sh
$ ls
cloud.google.com/      go.opencensus.io/     gopkg.in/          nhooyr.io/
dmitri.shuralyov.com/  go.opentelemetry.io/  gorm.io/           rsc.io/
github.com/            go.uber.org/          honnef.co/         sumdb/
go.etcd.io/            golang.org/           lukechampine.com/
go.mongodb.org/        google.golang.org/    modernc.org/

إذن حزمة التبعية المحملة في المثال أعلاه موجودة في المسار:

$GOMODCACHE/cache/download/github.com/bytedance/sonic/@v/

هيكل الدليل المحتمل كالتالي، سيكون هناك عدة ملفات بأسماء إصدارات:

sh
$ ls
list         v1.8.0.lock  v1.8.0.ziphash  v1.8.3.mod
v1.5.0.mod   v1.8.0.mod   v1.8.3.info     v1.8.3.zip
v1.8.0.info  v1.8.0.zip   v1.8.3.lock     v1.8.3.ziphash

عادة، يجب أن يكون هناك ملف list في هذا الدليل، لتسجيل أرقام الإصدارات المعروفة لهذه التبعية. ولكل إصدار، ستكون هناك الملفات التالية:

  • zip: حزمة الكود المصدري المضغوطة للتبعية
  • ziphash: قيمة الهاش المحسوبة من حزمة التبعية المضغوطة
  • info: بيانات وصفية للإصدار بتنسيق json
  • mod: ملف go.mod لهذا الإصدار
  • lock: ملف مؤقت، لم يذكر المسؤول وظيفته

عادة، سيقوم Go بحساب قيمة الهاش لملف الحزمة المضغوطة وملف go.mod، ثم الاستعلام عن قيمة الهاش لحزمة التبعية من الخادم المحدد بواسطة GOSUMDB (الافتراضي هو sum.golang.org). إذا كانت قيمة الهاش المحسوبة محليًا مختلفة عن النتيجة المستعلم عنها، لن يستمر في التنفيذ. إذا كانت متطابقة، سيتم تحديث ملف go.mod، وإدراج سجلين في ملف go.sum، تقريبًا كالتالي:

github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=

TIP

إذا تم تعطيل GOSUMDB، سيقوم Go بكتابة قيمة الهاش المحسوبة محليًا مباشرة في ملف go.sum، وهذا غير موصى به عمومًا.

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

الوحدات الخاصة

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

  • GOPROXY: مجموعة خوادم الوكيل للتبعيات
  • GOPRIVATE: قائمة أنماط مسارات الوحدات الخاصة، إذا تطابق اسم الوحدة مع القاعدة، فهذا يعني أن هذه الوحدة خاصة، وسلوكها المحدد مثل GONOPROXY و GONOSUMDB
  • GONOPROXY: قائمة أنماط مسارات الوحدات التي لا يتم تنزيلها من الوكيل، إذا تطابقت مع القاعدة، عند تنزيل الوحدة لن تمر عبر GOPROXY، وستحاول التنزيل مباشرة من نظام التحكم في الإصدارات
  • GONOSUMDB: قائمة أنماط مسارات الوحدات التي لا يتم التحقق منها عبر GOSUMDB العام، إذا تطابقت، عند تنزيل الوحدة والتحقق منها لن يمر عبر قاعدة بيانات checksum العامة
  • GOINSECURE: قائمة أنماط مسارات الوحدات التي يمكن استرجاعها عبر HTTP وبروتوكولات غير آمنة أخرى

مساحة العمل

ذكرنا سابقًا أن ملف go.mod يدعم توجيه replace، مما يتيح لنا استخدام بعض التعديلات المحلية المؤقتة التي لم تصدر بعد، كالتالي:

replace (
  github.com/246859/hello v1.0.1 => ./hello
)

عند التجميع، سيستخدم Go وحدة hello المحلية، وبعد إصدار نسخة جديدة في المستقبل، يتم إزالتها.

لكن استخدام توجيه replace سيعدل محتوى ملف go.mod، وقد يتم تقديم هذا التعديل بالخطأ إلى المستودع البعيد، وهذا ما لا نريده، لأن الـ target المحدد بواسطة توجيه replace هو مسار ملف وليس URL شبكي، والمسار الذي يمكن استخدامه على هذه الآلة قد لا يمكن استخدامه على آلة أخرى، ومسارات الملفات مشكلة كبيرة أيضًا في التوافق عبر المنصات. لحل هذا النوع من المشاكل، ظهرت مساحة العمل.

مساحة العمل (workspace)، هي حل جديد لإدارة الوحدات المتعددة محليًا أطلقته Go في 1.18، وتهدف لتسهيل عمل تطوير الوحدات المتعددة محليًا بشكل أفضل. فيما يلي شرح من خلال مثال.

مستودع المثال: 246859/work: go work example (github.com)

المثال

أولاً، يوجد في المشروع وحدتا go مستقلتان، هما auth و user:

bash
$ ls -1
LICENSE
README.md
auth
go.work
user

وحدة auth تعتمد على هيكل User من وحدة user، والمحتوى كالتالي:

go
package auth

import (
  "errors"
  "github.com/246859/work/user"
)

// Verify user credentials if is ok
func Verify(user user.User) (bool, error) {
  password, err := query(user.Name)
  if err != nil {
    return false, err
  }
  if password != user.Password {
    return false, errors.New("authentication failed")
  }
  return true, nil
}

func query(username string) (string, error) {
  if username == "jack" {
    return "jack123456", nil
  }
  return "", errors.New("user not found")
}

محتوى وحدة user كالتالي:

go
package user

type User struct {
  Name     string
  Password string
  Age      int
}

في هذا المشروع، يمكننا كتابة ملف go.work كالتالي:

go 1.22

use (
  ./auth
  ./user
)

محتواه سهل الفهم جدًا، استخدم توجيه use لتحديد الوحدات التي تشارك في التجميع، ثم شغّل الكود في وحدة auth:

go
// auth/example/main.go
package main

import (
  "fmt"
  "github.com/246859/work/auth"
  "github.com/246859/work/user"
)

func main() {
  ok, err := auth.Verify(user.User{Name: "jack", Password: "jack123456"})
  if err != nil {
    panic(err)
  }
  fmt.Printf("%v", ok)
}

نفذ الأمر التالي، من النتيجة يتضح نجاح استيراد الوحدة:

bash
$ go run ./auth/example
true

في الإصدارات السابقة، لهاتين الوحدتين المستقلتين، إذا أرادت وحدة auth استخدام كود وحدة user، فهناك طريقتان فقط:

  1. تقديم تعديلات وحدة user ودفعها إلى المستودع البعيد، وإصدار نسخة جديدة، ثم تعديل ملف go.mod للإصدار المحدد
  2. تعديل ملف go.mod لإعادة توجيه التبعية إلى ملف محلي

كلتا الطريقتين تتطلبان تعديل ملف go.mod، ووجود مساحة العمل يهدف لتمكين استيراد وحدات أخرى دون تعديل ملف go.mod. لكن يجب فهم أن ملف go.work يستخدم فقط في عملية التطوير، ووجوده فقط لتسهيل التطوير المحلي، وليس لإدارة التبعيات، فهو فقط يتخطى مؤقتًا عملية التقديم والإصدار، ويتيح لك استخدام التعديلات الجديدة لوحدة user فورًا دون انتظار. بعد اختبار وحدة user، لا يزال يلزم إصدار نسخة جديدة، ووحدة auth في النهاية لا تزال تحتاج لتعديل ملف go.mod للاستشهاد بأحدث إصدار (يمكن إكمال هذه العملية بأمر go work sync)، لذلك في عملية تطوير go الطبيعية، go.work لا يجب تقديمه إلى VCS أيضًا (ملف go.work في مستودع المثال للعرض فقط)، لأن محتواه يعتمد على الملفات المحلية، ووظيفته تقتصر على التطوير المحلي.

الأوامر

فيما يلي بعض أوامر مساحة العمل:

الأمرالشرح
editتعديل go.work
initتهيئة مساحة عمل جديدة
syncمزامنة تبعيات الوحدات في مساحة العمل
useإضافة وحدة جديدة إلى go.work
vendorنسخ التبعيات بتنسيق vendor

انتقل إلى go work cmd لمعرفة المزيد عن الأوامر

التوجيهات

محتوى ملف go.work بسيط جدًا، ثلاثة توجيهات فقط:

  • go، تحديد إصدار go
  • use، تحديد الوحدات المستخدمة
  • replace، تحديد الوحدات المستبدلة

باستثناء توجيه use، التوجيهان الآخران مكافئان أساسًا للتوجيهات في go.mod، لكن replace في go.work ستعمل على جميع الوحدات. ملف go.work الكامل كالتالي:

tex
go 1.22

use(
  ./auth
  ./user
)

replace github.com/246859/hello v1.0.0 => /home/jack/code/hello

Golang تم تحريره بواسطة www.golangdev.cn