تصرّح الكلمة المفتاحية interface عن مجموعة من الدوال التابعة (methods). وأي نوع يملك تلك الدوال التابعة يحقق الـ interface تلقائيًا. لا توجد كلمة مفتاحية implements، ولا قائمة interfaces مكتوبة على النوع، لذا يمكن لـ struct كُتبت قبل ظهور interface ما بسنوات أن تحققها. ومنذ Go 1.18 يمكن للـ interface أيضًا أن تسرد أنواعًا إلى جانب الدوال التابعة، فتصبح قيدًا (constraint) للكود العام (مواصفات Go).
الخلاصة
- السطر
type Notifier interface { Notify(...) error }يصرّح عن interface. وأي نوع له دالة تابعةNotifyمطابقة هوNotifier. - التحقيق ضمني. والسطر
var _ Notifier = (*SlackNotifier)(nil)يجعل المترجم يتحقق منه وقت البناء. - الدالة التابعة ذات مستقبِل المؤشر (pointer receiver) تنتمي إلى
*Tوليس إلىT. وإسناد قيمةTيفشل بالخطأmethod Notify has pointer receiver. - أبقِ الـ interfaces صغيرة، وصرّح عنها في الحزمة التي تستخدمها. اقبل interfaces وأعِد أنواعًا محددة (concrete types).
- النوع
anyاسم بديل لـinterface{}، ويحمل قيمة من أي نوع. واستعد القيمة باستخدامv, ok := x.(T)أو type switch. - لا تكون الـ interface مساوية لـ
nilإلا حين يكون نوعها وقيمتها كلاهما غير مُعيَّنين. وإعادة*ValidationErrorقيمته nil على أنهerrorتعطيكerr != nilتساويtrue. - الـ interfaces التي تحتوي قوائم أنواع مثل
~int64 | ~float64هي قيود. وهي تعمل في قوائم معاملات الأنواع (type parameters) ولا تعمل في أي مكان آخر.
كيف تصرّح عن interface في Go؟
اكتب type ثم اسمًا ثم interface تليها تواقيع الدوال التابعة. وهذا نظام تنبيهات يرسل الرسالة نفسها إلى Slack وإلى البريد الإلكتروني:
example.gogopackage main import ( "context" "fmt" ) type Notifier interface { Notify(ctx context.Context, msg string) error } type SlackNotifier struct { Channel string } func (s SlackNotifier) Notify(ctx context.Context, msg string) error { fmt.Printf("slack #%s: %s\n", s.Channel, msg) return nil } type EmailNotifier struct { To string } func (e EmailNotifier) Notify(ctx context.Context, msg string) error { fmt.Printf("email %s: %s\n", e.To, msg) return nil } func alertAll(ctx context.Context, notifiers []Notifier, msg string) { for _, n := range notifiers { if err := n.Notify(ctx, msg); err != nil { fmt.Println("notify failed:", err) } } } func main() { notifiers := []Notifier{ SlackNotifier{Channel: "oncall"}, EmailNotifier{To: "[email protected]"}, } alertAll(context.Background(), notifiers, "payment API p99 above 2s") }
example.texttextslack #oncall: payment API p99 above 2s email [email protected]: payment API p99 above 2s
لا تعرف alertAll إلا الدالة التابعة Notify. ولا يهمها هل تتعامل مع Slack أو البريد الإلكتروني أو عميل PagerDuty سيضيفه أحدهم الشهر القادم. فكل قيمة في الـ slice تحمل نوعها المحدد معها، ويذهب الاستدعاء إلى الدالة التابعة لذلك النوع وقت التشغيل.
ويمكن للـ interface أن تضمّن interfaces أخرى. فتُدمج مجموعات الدوال التابعة المضمَّنة، وبهذه الطريقة تبني المكتبة القياسية io.ReadWriter من io.Reader و io.Writer:
example.gogotype ReadWriter interface { Reader Writer }
يحقق النوع io.ReadWriter حين يملك Read و Write كليهما. وهذا ينطبق على *os.File و net.Conn، ولا ينطبق على *strings.Reader لأنه لا يملك دالة تابعة Write.
كيف يحقق نوعٌ ما interface في Go؟
بأن تكون كل دالة تابعة في الـ interface موجودة في مجموعة دواله التابعة (method set)، بالتوقيع نفسه تمامًا. تسمّي Go هذا التحقيق الضمني، وهو الفرق الأساسي عن Java أو C#، حيث يجب على الصنف (class) أن يذكر كل interface ينفّذها.
وهذا يُبقي الحزم منفصلة بعضها عن بعض. فالنوع SlackNotifier لا يستورد الحزمة التي تصرّح عن Notifier، ولا يحتاج إلى معرفة أن Notifier موجودة أصلًا. ويمكنك اليوم أن تكتب interface تحققها مسبقًا أنواع من المكتبة القياسية أو من حزمة خارجية.
وحين تكون دالة تابعة مفقودة، يخبرك المترجم بذلك في الموضع الذي تُستخدم فيه القيمة على أنها الـ interface:
example.gogotype PagerDutyNotifier struct { RoutingKey string } func (p PagerDutyNotifier) Send(ctx context.Context, msg string) error { return nil } var _ Notifier = PagerDutyNotifier{}
example.texttext./main.go:20:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (missing method Notify)
وإذا كانت الدالة التابعة موجودة لكن توقيعها مختلف، يعرض الخطأ النسختين جنبًا إلى جنب:
example.texttext./main.go:16:18: cannot use PagerDutyNotifier{} (value of struct type PagerDutyNotifier) as Notifier value in variable declaration: PagerDutyNotifier does not implement Notifier (wrong type for method Notify) have Notify(string) error want Notify(context.Context, string) error
كيف تتحقق وقت الترجمة من أن نوعًا ما يحقق interface؟
صرّح عن متغير على مستوى الحزمة باستخدام المعرّف الفارغ (blank identifier):
example.gogovar _ Notifier = (*SlackNotifier)(nil)
لا يحجز هذا السطر أي ذاكرة ولا يشغّل أي كود أبدًا. فهو يسأل المترجم فقط هل يمكن إسناد *SlackNotifier إلى Notifier، لذا يتعطل البناء في اللحظة التي يعيد فيها أحدهم تسمية Notify أو يغيّرها. ودونه لن تكتشف المشكلة إلا حيث يُستخدم النوع أول مرة على أنه Notifier، وقد يكون ذلك في حزمة أخرى. ويشرح مقال الكلمة المفتاحية var في Go الحيلة نفسها مع http.Handler.
لماذا يكسر مستقبِل المؤشر (pointer receiver) تحقيق الـ interface؟
لأن الدالة التابعة تنتمي إلى نوع المؤشر. لكل نوع مجموعة دوال تابعة، والقواعد هي (مواصفات Go):
- تحتوي مجموعة الدوال التابعة لـ
Tعلى الدوال التابعة المُصرَّح عنها بمستقبِل قيمة(t T). - وتحتوي مجموعة الدوال التابعة لـ
*Tعلى تلك الدوال، إضافةً إلى الدوال التابعة المُصرَّح عنها بمستقبِل مؤشر(t *T).
المُنبِّه الذي يجمّع الرسائل في دفعات يحتاج إلى تعديل نفسه، لذا يستخدم مستقبِل مؤشر:
example.gogotype BatchNotifier struct { mu sync.Mutex pending []string } func (b *BatchNotifier) Notify(ctx context.Context, msg string) error { b.mu.Lock() defer b.mu.Unlock() b.pending = append(b.pending, msg) return nil } func main() { var n Notifier = BatchNotifier{} fmt.Println(n) }
example.texttext./main.go:26:19: cannot use BatchNotifier{} (value of struct type BatchNotifier) as Notifier value in variable declaration: BatchNotifier does not implement Notifier (method Notify has pointer receiver)
الحل هو var n Notifier = &BatchNotifier{}. وترفض Go نسخة القيمة عن قصد. فالـ interface التي تحمل BatchNotifier ستحمل نسخة منه، وستضيف Notify إلى الـ slice المسمّاة pending في تلك النسخة بينما يبقى الأصل دون تغيير. ولن يفيد أيضًا تحويل Notify إلى مستقبِل قيمة. فهذا سينسخ sync.Mutex في كل استدعاء، ويبلّغ go vet عن ذلك بالرسالة Notify passes lock by value.
النوع الذي له دوال تابعة بمستقبِل مؤشر يجب التحقق منه عبر مؤشره، كما في var _ Notifier = (*BatchNotifier)(nil). وصيغة المؤشر تعمل أيضًا مع الأنواع ذات مستقبِل القيمة مثل SlackNotifier، لأن *T يملك كل دالة تابعة لـ T. ولهذا تُعدّ (*T)(nil) الطريقة المعتادة لكتابة هذا التحقق.
كيف تصمّم الـ interfaces في Go؟
يقول مثل Go المعروف: «كلما كبرت الـ interface، ضعف التجريد» (Go Proverbs). وأكثر الـ interfaces استخدامًا في المكتبة القياسية لها دالة تابعة واحدة أو اثنتان:
| الـ interface | الدوال التابعة | تحققها |
|---|---|---|
io.Reader | Read | الملفات، واتصالات الشبكة، وأجسام HTTP، وقارئات gzip، و bytes.Buffer |
io.Writer | Write | الملفات، و http.ResponseWriter، ودوال التجزئة (hashes)، و bytes.Buffer |
error | Error | *fs.PathError، و *json.SyntaxError، وأنواع الأخطاء الخاصة بك |
fmt.Stringer | String | أي شيء يتحكم في طريقة طباعة fmt له |
http.Handler | ServeHTTP | الموجّهات (routers)، والـ middleware، و http.HandlerFunc |
الـ interface ذات الدالة التابعة الواحدة سهلة التحقيق، لذا تحققها أنواع كثيرة. وتعمل io.Copy مع أي مصدر وأي وجهة لأنها لا تطلب إلا القليل. أما interface باسم Storage فيها اثنتا عشرة دالة تابعة فلها عادةً تنفيذ حقيقي واحد، إضافةً إلى mock يجب تعديله في كل مرة تتغير فيها الـ interface. ويعمل http.Handler بالطريقة نفسها. فالـ middleware يستقبل واحدًا ويعيد واحدًا، لذا يمكن تكديس الـ middleware في Go من أي حزمة حول أي موجّه.
ويُظهر fmt.Stringer أن النوع لا يحتاج إلا إلى القليل كي يندمج مع المكتبة القياسية. أعطِ تعدادًا (enum) دالة تابعة String، وستستخدمها كل صيغ fmt:
example.gogotype OrderStatus int const ( Pending OrderStatus = iota Paid Refunded ) func (s OrderStatus) String() string { switch s { case Pending: return "pending" case Paid: return "paid" case Refunded: return "refunded" } return fmt.Sprintf("OrderStatus(%d)", int(s)) } func main() { fmt.Println(Paid) fmt.Printf("order 1001 is %v\n", Refunded) }
example.texttextpaid order 1001 is refunded
أين ينبغي التصريح عن الـ interface في Go؟
في الحزمة التي تستخدمها، لا في الحزمة التي تنفّذها. فلأن التحقيق ضمني، يستطيع المستهلك ألا يسرد إلا الدوال التابعة التي يستدعيها.
خدمة الاسترداد لا تحتاج إلا إلى تحميل طلب. لذا تصرّح عن interface بدالة تابعة واحدة لهذا الغرض، وتأخذها في دالة الإنشاء (constructor):
example.gogotype orderGetter interface { GetOrder(ctx context.Context, id int64) (Order, error) } type RefundService struct { orders orderGetter } func NewRefundService(orders orderGetter) *RefundService { return &RefundService{orders: orders} } func (s *RefundService) CanRefund(ctx context.Context, id int64) (bool, error) { o, err := s.orders.GetOrder(ctx, id) if err != nil { return false, fmt.Errorf("load order %d: %w", id, err) } return o.Status == "paid", nil }
في بيئة الإنتاج تمرّر مخزن Postgres، الذي يملك عشرين دالة تابعة أخرى لا تراها خدمة الاسترداد أبدًا. وفي الاختبار تكفي map:
example.gogotype fakeOrders map[int64]Order func (f fakeOrders) GetOrder(ctx context.Context, id int64) (Order, error) { o, ok := f[id] if !ok { return Order{}, ErrNotFound } return o, nil } func main() { svc := NewRefundService(fakeOrders{1001: {ID: 1001, Status: "paid"}}) fmt.Println(svc.CanRefund(context.Background(), 1001)) fmt.Println(svc.CanRefund(context.Background(), 1002)) }
example.texttexttrue <nil> false load order 1002: order not found
النوع fakeOrders نوع map مُعرَّف له دالة تابعة (راجع الكلمة المفتاحية type في Go)، لذا لا يحتاج الاختبار إلى مكتبة mocking.
ماذا تعني عبارة «اقبل interfaces وأعِد structs»؟
هي النصف الآخر من الفكرة نفسها. تأخذ الدوال interfaces معاملاتٍ، فيستطيع المستدعون تمرير أي شيء يناسبها. وتعيد دوال الإنشاء أنواعًا محددة مثل *RefundService أو *PostgresStore، فيحصل المستدعون على كل دالة تابعة وكل حقل مُصدَّر، ويقررون بأنفسهم أي interface صغيرة يحتاجون إليها.
إعادة interface من دالة إنشاء تخفي الدوال التابعة التي لا تسردها تلك الـ interface، وتجبر كل مستدعٍ على استخدام التجريد الوحيد الذي اخترته. والاستثناء الأساسي هو error. فالدوال تعيد error بدلًا من *ValidationError، ويشرح قسم nil أدناه السبب.
ما any في Go؟
النوع any اسم بديل لـ interface{}، وهي الـ interface التي لا تحتوي أي دالة تابعة. ولكل نوع صفر دوال تابعة على الأقل، لذا يمكن لمتغير من النوع any أن يحمل أي قيمة. أُضيف any في Go 1.18، والصيغتان قابلتان للتبادل (مواصفات Go).
يظهر any حيث لا يكون النوع معروفًا إلا وقت التشغيل. فالدالة fmt.Println(a ...any) تأخذ وسائط من أي نوع، و json.Unmarshal إلى map[string]any ينتج قيمًا تعتمد أنواعها على محتوى JSON.
والثمن أن المترجم يتوقف عن التحقق. فكل قراءة من any تحتاج إلى تأكيد نوع، والتخمين الخاطئ يصبح خطأ وقت التشغيل بدلًا من خطأ ترجمة. وحين تعمل دالة على عدة أنواع لكن العمليات نفسها تنطبق عليها كلها، استخدم معامل نوع بدلًا من ذلك. فالدالة func Sum[T Amount](values []T) T، التي يشرحها قسم القيود أدناه، تحتفظ بالتحقق الكامل من الأنواع، بينما تدفع func Sum(values []any) any كل تحقق إلى وقت التشغيل.
كيف يعمل تأكيد النوع (type assertion) و type switch؟
يستخرج تأكيد النوع القيمة المحددة من الـ interface من جديد. وصيغة القيمتين لا تسبب panic أبدًا:
example.gogovar payload map[string]any body := `{"user_id": 42, "email": "[email protected]", "roles": ["admin", "billing"], "manager": null}` if err := json.Unmarshal([]byte(body), &payload); err != nil { log.Fatal(err) } email, ok := payload["email"].(string) fmt.Println(email, ok) id, ok := payload["user_id"].(int) fmt.Println(id, ok) fmt.Printf("%T\n", payload["user_id"])
example.texttext[email protected] true 0 false float64
يفشل التأكيد الثاني لأن encoding/json تفك ترميز كل رقم JSON إلى float64 حين يكون الهدف any. ومع ok لا يكون الفشل إلا false والقيمة الصفرية. أما صيغة القيمة الواحدة، payload["user_id"].(int)، فتسبب panic بدلًا من ذلك:
example.texttextpanic: interface conversion: interface {} is float64, not int
وتعمل التأكيدات أيضًا مع الـ interfaces. فالتعبير r.(io.Writer) يسأل هل القيمة داخل r تملك دالة تابعة Write، وتستخدم المكتبة القياسية هذا للبحث عن سلوك اختياري. فالدالة io.Copy تتحقق هل المصدر io.WriterTo، وتستخدم مساره الأسرع إن كان كذلك.
يُجري type switch عدة تأكيدات في عبارة واحدة. وهو يناسب حمولة JSON نفسها، حيث يمكن أن تكون القيمة واحدًا من بضعة أنواع:
example.gogofunc describe(v any) string { switch v := v.(type) { case nil: return "null" case float64: return fmt.Sprintf("number %g", v) case string: return fmt.Sprintf("string of %d bytes", len(v)) case []any: return fmt.Sprintf("array of %d items", len(v)) case map[string]any: return fmt.Sprintf("object with %d keys", len(v)) default: return fmt.Sprintf("other %T", v) } } func main() { // payload decoded as above for _, key := range []string{"user_id", "email", "roles", "manager"} { fmt.Println(key+":", describe(payload[key])) } }
example.texttextuser_id: number 42 email: string of 15 bytes roles: array of 2 items manager: null
داخل كل حالة يكون لـ v نوع تلك الحالة، لذا يُترجم len(v) مع السلسلة النصية والـ slice والـ map. ويعمل type switch على الأخطاء بالطريقة نفسها، لكنه لا يرى إلا الخطأ الخارجي. ويعرض مقال الكلمة المفتاحية type في Go هذه الحالة، ويشرح لماذا تكون errors.As عادةً الأداة الأفضل للأخطاء المغلّفة.
لماذا لا يساوي الخطأ nil القيمة nil في Go؟
لقيمة الـ interface جزءان، نوع ديناميكي وقيمة ديناميكية. ولا تساوي nil إلا حين يكون الجزءان كلاهما غير مُعيَّنين. فإذا خزّنت مؤشرًا قيمته nil في interface، يكون جزء النوع مُعيَّنًا، لذا لا تكون الـ interface مساوية لـ nil (الأسئلة الشائعة في Go).
وأكثر ما يظهر هذا مع أنواع الأخطاء المخصصة. هنا تحتفظ الدالة بمتغير من النوع *ValidationError وتعيده، سواء أُسندت إليه قيمة أم لا:
example.gogopackage main import ( "fmt" "strings" ) type ValidationError struct { Field string } func (e *ValidationError) Error() string { return "invalid " + e.Field } func validate(email string) error { var verr *ValidationError if !strings.Contains(email, "@") { verr = &ValidationError{Field: "email"} } return verr } func main() { err := validate("[email protected]") if err != nil { fmt.Printf("validation failed: %v (%T)\n", err, err) return } fmt.Println("ok") }
example.texttextvalidation failed: <nil> (*main.ValidationError)
البريد الإلكتروني صالح و verr قيمته nil، لكن قيمة error المُعادة تحمل النوع *main.ValidationError وبداخله مؤشر قيمته nil. والمقارنة err != nil تقارن الـ interface كلها بـ interface ليس لها نوع، لذا تكون نتيجتها true. فيرفض المستدعي تسجيلًا صالحًا، ويقول سطر السجل <nil>، وهذا يصعّب اكتشاف الخلل.
الحل هو أن تعيد nil حرفيًا في مسار النجاح، وألا تمرّر أبدًا مؤشرًا محدد النوع عبر قيمة مُعادة من النوع error:
example.gogofunc validate(email string) error { if !strings.Contains(email, "@") { return &ValidationError{Field: "email"} } return nil }
صرّح عن نوع القيمة المُعادة على أنه error وليس *ValidationError، وأعِد nil مباشرةً. والمستدعون الذين يحتاجون إلى الحقل ما زالوا يحصلون عليه باستخدام errors.As، التي تجد الخطأ أيضًا بعد تغليفه:
example.gogoerr := fmt.Errorf("signup: %w", validate("ana.example.com")) var verr *ValidationError if errors.As(err, &verr) { fmt.Println("bad field:", verr.Field) }
يطبع هذا bad field: email. والفخ نفسه موجود مع أي interface. فإذا صرّحت بـ var store *PostgresStore ولم تعيّنه، ثم مرّرته حيث يُتوقَّع orderGetter، حصلت أيضًا على interface لا تساوي nil.
والحالة المعاكسة هي interface قيمتها nil فعلًا، مثل حقل struct من النوع Notifier لم يعيّنه أحد. فاستدعاء دالة تابعة عليه يسبب panic بالرسالة invalid memory address or nil pointer dereference، لأنه لا يوجد نوع يمكن البحث فيه عن الدالة التابعة. ودالة الإنشاء التي تشترط وجود الاعتمادية (dependency)، مثل NewRefundService(orders)، تمنع ذلك.
كيف تُستخدم الـ interfaces قيودًا في الأنواع العامة؟
منذ Go 1.18 يمكن للـ interface أن تسرد أنواعًا إلى جانب الدوال التابعة. وتُسمّى هذه القائمة مجموعة الأنواع (type set)، والرمز ~T يعني «أي نوع نوعه الأساسي هو T»:
example.gogotype Cents int64 type Amount interface { ~int64 | ~float64 } func Sum[T Amount](values []T) T { var total T for _, v := range values { total += v } return total } func Dedupe[T comparable](ids []T) []T { seen := make(map[T]struct{}, len(ids)) out := make([]T, 0, len(ids)) for _, id := range ids { if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} out = append(out, id) } return out } func main() { fmt.Println(Sum([]Cents{4999, 1250, 300})) fmt.Println(Dedupe([]string{"u_42", "u_7", "u_42"})) }
example.texttext6549 [u_42 u_7]
يسمح Amount بالعامل + لأن كل نوع في مجموعته يدعمه. و comparable قيد مُصرَّح عنه مسبقًا للأنواع التي تعمل مع ==، وهذا ما يحتاج إليه مفتاح الـ map. ويرفض المترجم أي نوع خارج المجموعة ويسمّي المشكلة:
example.texttext./main.go:27:17: string does not satisfy Amount (string missing in ~int64 | ~float64)
والـ interface التي تحتوي قائمة أنواع لا يمكن استخدامها إلا قيدًا. فالتصريح عن متغير من ذلك النوع يفشل:
example.texttext./main.go:10:8: cannot use type Amount outside a type constraint: interface contains type constraints
ففي Go إذن نوعان من الـ interfaces يشتركان في كلمة مفتاحية واحدة. الـ interfaces التي تحتوي دوالًا تابعة فقط تعمل في كل مكان، أنواعًا للمتغيرات ومعاملاتٍ وقيودًا. أما الـ interfaces التي تحتوي قوائم أنواع فلا تظهر إلا بين الأقواس المربعة. ويصف مقال مدونة Go التعريفي بالأنواع العامة كيف توحّد مجموعات الأنواع بين النوعين.
أين يأتي دور LevelUpGo
يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تغطي دورة Interfaces & Polymorphism التحقيق الضمني ومجموعات الدوال التابعة و any وتأكيدات النوع وتركيب الـ interfaces. وتغطي دورة Interface Design الـ interfaces الصغيرة، والتصريح عنها حيث تُستخدم، وتجنّب الـ interfaces التي لا توجد إلا لأغراض الـ mocking. وتتعمق دورة Go Generics Masterclass أكثر في القيود ومجموعات الأنواع. ويقدّم Training Ground تمارين قصيرة مستقلة على الـ interfaces للتدرّب خارج أي دورة. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.
الأسئلة الشائعة
هل interface كلمة مفتاحية في Go؟
نعم. interface واحدة من الكلمات المفتاحية المحجوزة الـ 25 في Go، لذا لا يمكنك استخدامها اسمًا لمتغير أو دالة. وهي تظهر في تصريحات أنواع الـ interface مثل type Notifier interface { ... }، وفي الصيغة الحرفية interface{}، وفي القيود المكتوبة مباشرةً مثل [T interface{ ~int | ~string }].
هل توجد في Go كلمة مفتاحية implements؟
لا. يحقق النوع الـ interface بمجرد أن يملك كل دوالها التابعة بتواقيع مطابقة. ولجعل المترجم يتحقق من أن نوعًا ما يحقق interface، أضف var _ Notifier = (*SlackNotifier)(nil) بجانب النوع.
ما الفرق بين any و interface{} في Go؟
لا فرق. any اسم بديل مُصرَّح عنه مسبقًا لـ interface{}، أُضيف في Go 1.18، لذا فهما النوع نفسه. ويستخدم الكود الأحدث any لأنه أقصر، ويمكن للأمر gofmt -r 'interface{} -> any' أن يعيد كتابة الكود الأقدم.
لماذا تقول Go "method has pointer receiver"؟
الدالة التابعة مُصرَّح عنها على *T، لذا لا يملكها إلا *T. وقد حاولت استخدام قيمة T على أنها الـ interface. مرّر مؤشرًا بدلًا منها، كما في &BatchNotifier{}، أو حوّل الدالة التابعة إلى مستقبِل قيمة إذا كانت لا تعدّل المستقبِل.
لماذا لا يكون الخطأ nil مع أنني أعدت مؤشرًا nil؟
لا تساوي الـ interface nil إلا حين يكون نوعها وقيمتها كلاهما غير مُعيَّنين. وإعادة *ValidationError قيمته nil من دالة نوع قيمتها المُعادة error تعيّن النوع إلى *ValidationError، لذا تكون err != nil مساوية لـ true. أعِد nil حرفيًا في مسار النجاح بدلًا من متغير مؤشر محدد النوع.
هل يمكن أن تحتوي الـ interfaces في Go على حقول؟
لا. الـ interfaces لا تصف إلا الدوال التابعة، أو مجموعات الأنواع حين تُستخدم قيودًا. وإذا احتاج المستدعون إلى قيمة، فأضف دالة تابعة للقراءة (getter) مثل ID() string، أو اقبل الـ struct المحددة بدلًا من interface.
المصادر
- The Go Programming Language Specification, Interface types: https://go.dev/ref/spec#Interface_types
- The Go Programming Language Specification, Method sets: https://go.dev/ref/spec#Method_sets
- The Go Programming Language Specification, Type assertions: https://go.dev/ref/spec#Type_assertions
- The Go Programming Language Specification, Type switches: https://go.dev/ref/spec#Type_switches
- Go FAQ, Why is my nil error value not equal to nil?: https://go.dev/doc/faq#nil_error
- The Go Blog, An Introduction To Generics: https://go.dev/blog/intro-generics
- Effective Go, Interfaces and other types: https://go.dev/doc/effective_go#interfaces_and_types
- Go Proverbs: https://go-proverbs.github.io/
- io package: https://pkg.go.dev/io
