Go: معالجة الأخطاء وإدارة الحزم في Go
آخر تحديث: 2026-08-26
الأخطاء هي قيم، وليست استثناءات — تعالج لغة Go الأخطاء كقيم إرجاع عادية، وهذا التصميم يجعل معالجة الأخطاء واضحة وقابلة للتحكم وقابلة للتركيب.
تُعد فلسفة معالجة الأخطاء في لغة Go وإدارة الحزم ركائز أساسية للكود المخصص للاستخدام في بيئات الإنتاج. في هذا الدرس، ستتقن أهم قدرتين أساسيتين في لغة Go يتم التقليل من شأنهما بشكل كبير.
1. ستتعلم
- واجهة
errorو4 طرق لإنشائها errors.Is/errors.Asللتحقق من سلسلة الأخطاء- أنواع الأخطاء المخصصة
- حالات الاستخدام لـ
panic/recover go modإدارة التبعيات (init/tidy/add)- قواعد تصدير الحزم (الأحرف الكبيرة = عامة، الأحرف الصغيرة = خاصة)
- إنشاء خدمات المستخدم مع معالجة أخطاء فعالة
2. قصة حقيقية لمهندس خدمات مصغرة
(1) المشكلة: تسبب الذعر الذي انتشر عبر الإنترنت في تعطل الخدمة، وامتلأت مجموعة التنبيهات برسائل الخطأ 500
أليس هي مهندسة برمجيات خلفية في فريق الخدمات الصغيرة. وقد واجهت خدمة المستخدم التي تتولى صيانتها مؤخرًا مشكلة كبيرة:
«تعطلت خدمة المستخدم ثلاث مرات الأسبوع الماضي، وكان السبب في كل مرة هو محاولة الوصول إلى مؤشر nil. وكلما دخلت خدمة Go في حالة ذعر، تتوقف العملية بأكملها عن العمل، ولا يتمكن أي مستخدم من تسجيل الدخول — وقد قال مدير المنتج إنه في حال تعطل الخدمة مرة أخرى، سيتم خصم المكافآت».
قامت بفتح رمز الخطأ في موقع العطل:
// Bad code: No error handling; it just panics.
func getUserByID(db *sql.DB, id int) *User {
rows, _ := db.Query("SELECT * FROM users WHERE id = ?", id)
// If the ID does not exist, rows.Next() returns false.
// However, directly accessing the value below—the rows.Scan operation on nil—causes a panic.
var user User
for rows.Next() {
rows.Scan(&user.Name, &user.Age)
}
return &user
}
ثلاث مشكلات: (1) يتم تجاهل الأخطاء في db.Query؛ (2) لا يتم التحقق من وجود النتيجة؛ (3) يؤدي حدوث حالة ذعر إلى تعطل العملية بأكملها.
(2) حل لعبة «غو»: الأخطاء هي قيم
// user_service.go
package main
import (
"errors"
"fmt"
)
// Custom Error Types
type NotFoundError struct {
ID int
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("user %d not found", e.ID)
}
// Error Sentinel
var ErrInvalidInput = errors.New("invalid input")
// Robust Query Function
func findUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("findUser: %w", ErrInvalidInput)
}
users := map[int]User{
1: {Name: "Alice", Age: 28},
2: {Name: "Bob", Age: 32},
}
user, ok := users[id]
if !ok {
return nil, NotFoundError{ID: id}
}
return &user, nil
}
type User struct {
Name string
Age int
}
func main() {
for _, id := range []int{1, -1, 999} {
user, err := findUser(id)
if err != nil {
// Determining the Type of Error
if errors.Is(err, ErrInvalidInput) {
fmt.Printf("Input error (skipped): %v\n", err)
continue
}
var nf NotFoundError
if errors.As(err, &nf) {
fmt.Printf("User %d does not exist\n", nf.ID)
continue
}
fmt.Printf("Unknown error: %v\n", err)
continue
}
fmt.Printf("Found: %s (%d)\n", user.Name, user.Age)
}
}
الناتج:
Found: Alice (28)
Input error (skipped): findUser: invalid input
User 999 does not exist
(3) المزايا: مقارنة بين طرق معالجة الأخطاء
| البعد | لغة try-catch | خطأ Go |
|---|---|---|
| الخطأ هو | تدفق التحكم في الاستثناءات | قيمة الإرجاع العادية |
| صريح | ضمني (من السهل أن يفوتك كتلة catch) | صريح if err != nil |
| الأداء | عبء فك التكديس | لا يوجد عبء إضافي |
| قابلية التركيب | ضعيفة (تؤدي إلى مقاطعة غير طبيعية للتدفق) | جيدة (يمكن تمرير err بحرية) |
error هي مجرد قيمة واجهة (16 بايت)، لذا فإن تمريرها لا ينطوي عمليًّا على أي عبء إضافي.
3. واجهة الأخطاء
(1) ما هو الخطأ؟
type error interface {
Error() string
}
أي نوع يُنفِّذ الطريقة Error() string يُعتبر خطأً.
▶ مثال: 4 طرق لإنشاء خطأ
package main
import (
"errors"
"fmt"
)
// Method 1: errors.New (most commonly used)
var ErrNotFound = errors.New("resource not found")
// Method 2: fmt.Errorf (with formatting)
func validate(age int) error {
if age < 0 {
return fmt.Errorf("invalid age: %d (must be >= 0)", age)
}
return nil
}
// Method 3: Wrap the error in fmt.Errorf (%w)
func loadConfig(path string) error {
if path == "" {
return fmt.Errorf("loadConfig: %w", ErrNotFound)
}
return nil
}
// Method 4: Customizing the error type
type TimeoutError struct {
DurationMs int
Operation string
}
func (e TimeoutError) Error() string {
return fmt.Sprintf("%s timed out after %dms", e.Operation, e.DurationMs)
}
func main() {
// Method 1
fmt.Println(ErrNotFound) // resource not found
// Method 2
fmt.Println(validate(-5)) // invalid age: -5 (must be >= 0)
// Method 3
fmt.Println(loadConfig("")) // loadConfig: resource not found
// Method 4
err := TimeoutError{DurationMs: 5000, Operation: "DB query"}
fmt.Println(err) // DB query timed out after 5000ms
}
(3) مقارنة بين طرق الإنشاء الأربع
| الطريقة | الوظيفة/الصيغة | الغرض | هل تدعم تسلسل الأخطاء؟ |
|---|---|---|---|
| errors.New | errors.New("msg") |
خطأ ثابت بسيط | ❌ |
| fmt.Errorf | fmt.Errorf("msg %d", n) |
خطأ في التنسيق | ❌ |
| fmt.Errorf(%w) | fmt.Errorf("ctx: %w", err) |
خطأ ملفوف | ✅ errors.Is/As |
| النوع المخصص | struct { ... Error() string } |
خطأ في الحقول الإضافية | ✅ مخصص |
4. سلسلة الأخطاء errors.Is / errors.As
(1) errors.Is: يتحقق مما إذا كان حارس معين مدرجًا في سلسلة الأخطاء
package main
import (
"errors"
"fmt"
)
var ErrDB = errors.New("database error")
var ErrConn = fmt.Errorf("connection failed: %w", ErrDB)
func main() {
err := fmt.Errorf("query failed: %w", ErrConn)
// errors.Is searches layer by layer along the %w chain
fmt.Println(errors.Is(err, ErrDB)) // true
fmt.Println(errors.Is(err, ErrConn)) // true
// == Can only match the outermost level
fmt.Println(err == ErrDB) // false (different objects)
fmt.Println(err == ErrConn) // false
}
▶ مثال: errors.As: يستخرج أنواعًا محددة من الأخطاء من السلسلة
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Value interface{}
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed: %s = %v", e.Field, e.Value)
}
func process(input string) error {
if input == "" {
return ValidationError{Field: "input", Value: ""}
}
return nil
}
func main() {
err := process("")
// errors.As: Extracts the ValidationError type from the chain
var valErr ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field %s is invalid, value=%v\n", valErr.Field, valErr.Value)
}
// Also works with wrapping
wrapped := fmt.Errorf("process failed: %w", err)
var valErr2 ValidationError
if errors.As(wrapped, &valErr2) {
fmt.Printf("(After wrapping) Field %s is invalid\n", valErr2.Field)
}
}
الناتج:
Field input is invalid, value=
(After wrapping) Field input is invalid
(3) errors.Is مقابل errors.As
| الوظيفة | طريقة المطابقة | الغرض |
|---|---|---|
errors.Is(err, target) |
يساوي (==) | يتحقق مما إذا كان خطأ حراسة معين قد حدث أم لا |
errors.As(err, &target) |
مطابقة الأنواع | استرداد خطأ من نوع معين من سلسلة الأخطاء |
5. حالة الذعر / التعافي
(1) حالة الذعر: خطأ لا يمكن إصلاحه
package main
import "fmt"
func main() {
fmt.Println("Start")
// A panic immediately terminates the current function and begins stack unwinding.
panic("something went terribly wrong")
// This line will not be executed
fmt.Println("End")
}
الناتج:
Start
panic: something went terribly wrong
goroutine 1 [running]:
main.main()
/tmp/main.go:8 +0x...
exit status 2
▶ مثال: recover (التعافي من حالة الذعر)
package main
import (
"fmt"
)
// recover is only useful in defer
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
// Intentionally triggering a panic
if b == 0 {
panic("division by zero")
}
return a / b, nil
}
func main() {
// Normal call
if r, err := safeDivide(10, 2); err == nil {
fmt.Printf("10/2 = %d\n", r)
}
// A panic is caught by recover and does not cause a crash
if r, err := safeDivide(10, 0); err != nil {
fmt.Printf("Error: %v (result=%d)\n", err, r)
}
fmt.Println("Program ended normally—panic was recovered")
}
الناتج:
10/2 = 5
Error: panic recovered: division by zero (result=0)
Program ended normally—panic was recovered
(3) حالات استخدام «panic» مقابل «error»
| السيناريو | استخدام error |
استخدام panic |
|---|---|---|
| خطأ في الإدخال من قبل المستخدم | ✅ | ❌ |
| الملف غير موجود | ✅ | ❌ |
| انتهت مهلة الشبكة | ✅ | ❌ |
| إشارة مؤشر نيل | ❌ (لا يمكن إصلاحها) | ✅ (خطأ في الكود) |
| مؤشر المصفوفة خارج النطاق | ❌ (لا يتحقق منه المُجمِّع) | ✅ (خطأ في الكود) |
| فشل التهيئة (شرط إلزامي) | ❌ | ✅ |
panic + recover لمحاكاة كتلة try-catch. تتمثل فلسفة لغة Go في «الاستخدام المقتصد لـ panic، والاستخدام المتكرر لـ error». يجب استخدام panic فقط في حالات الاستثناء الحقيقية (أخطاء في الكود، أو فشل في التهيئة، أو حالات لا يمكن استردادها).
6. إدارة حزم Go Mod
(1) الأوامر الثلاثة الرئيسية في Go Mod
| الأمر | الوظيفة | حالات الاستخدام الشائعة |
|---|---|---|
go mod init <module> |
تهيئة وحدة | بدء مشروع جديد |
go mod tidy |
تنظيف التبعيات (إضافة التبعيات المفقودة، وإزالة التبعيات غير الضرورية) | بعد تعديل عبارات الاستيراد |
go mod add <path>@<ver> |
إضافة تبعيات (ميزة جديدة في Go 1.22+) | لإضافة حزمة خارجية |
go get <path>@<ver> |
إضافة/تحديث التبعيات | الطريقة التقليدية |
▶ مثال: إنشاء وحدة نمطية + إضافة التبعيات
# 1. Initialize module
$ go mod init github.com/alice/user-service
go: creating new go.mod: module github.com/alice/user-service
# 2. Import an external package in the code
package main
import (
"fmt"
"github.com/google/uuid" // external dependency
)
func main() {
id := uuid.New()
fmt.Printf("Generated UUID: %s\n", id)
}
# 3. Add dependencies and organize
$ go mod tidy
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.6.0
# 4. View the generated go.mod
$ cat go.mod
module github.com/alice/user-service
go 1.22
require github.com/google/uuid v1.6.0
(3) قواعد تصدير الحزم
// math/calculator.go
package math
// Uppercase = Public (accessible to other packages)
func Add(a, b int) int { return a + b }
var Version = "1.0"
// Lowercase first letter = private (visible only within the package)
func helper(x int) int { return x * 2 }
var internalVersion = "0.5"
// Public Structure
type Calculator struct {
// Public field
Name string
// Private field (cannot be accessed directly from outside the package)
precision int
}
package main
import "yourmodule/math"
func main() {
math.Add(1, 2) // ✅ Public
math.Version // ✅ Public variable
// math.helper(5) // ❌ Private function; compilation error
// math.internalVersion // ❌ Private variable
c := math.Calculator{Name: "basic"} // ✅ Public struct
// c.precision = 2 // ❌ Private field; compilation error
}
▶ مثال: تصدير الحزمة + تمرير نوع الخطأ
// apperrors/errors.go
package apperrors
import "fmt"
// Public Error Type (Uppercase)
type BusinessError struct {
Code int
Message string
}
func (e BusinessError) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Public Sentinel
var ErrUnauthorized = BusinessError{Code: 401, Message: "unauthorized"}
// Private error (external packages cannot reference directly)
type internalError struct {
detail string
}
func (e internalError) Error() string {
return fmt.Sprintf("internal: %s", e.detail)
}
// Public factory function (external packages use internalError indirectly through this function)
func NewInternalError(detail string) error {
return internalError{detail: detail}
}
7. مثال كامل: خدمة مستخدم متينة
الربط بين معالجة الأخطاء وإدارة الحزم والأخطاء المخصصة:
// user_service.go
package main
import (
"errors"
"fmt"
)
// ---------- Error Definitions ----------
type NotFoundError struct {
Resource string
ID int
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID)
}
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Message)
}
type DBError struct {
Operation string
Err error
}
func (e DBError) Error() string {
return fmt.Sprintf("db %s failed: %v", e.Operation, e.Err)
}
func (e DBError) Unwrap() error {
return e.Err
}
// Sentinel Error
var ErrInternal = errors.New("internal server error")
// ---------- Data Layer (Simulated DB) ----------
type User struct {
ID int
Name string
Age int
}
func queryUserFromDB(id int) (*User, error) {
db := map[int]User{
1: {ID: 1, Name: "Alice", Age: 28},
2: {ID: 2, Name: "Bob", Age: 32},
}
user, ok := db[id]
if !ok {
return nil, NotFoundError{Resource: "user", ID: id}
}
return &user, nil
}
// ---------- Service Layer ----------
func GetUser(id int) (*User, error) {
// panic protection
defer func() {
if r := recover(); r != nil {
fmt.Printf("[PANIC] recovered: %v\n", r)
}
}()
if id <= 0 {
return nil, ValidationError{
Field: "id",
Message: "must be positive",
}
}
user, err := queryUserFromDB(id)
if err != nil {
var nf NotFoundError
if errors.As(err, &nf) {
return nil, nf
}
return nil, DBError{
Operation: "queryUserFromDB",
Err: err,
}
}
if user.Age < 0 || user.Age > 150 {
return nil, ValidationError{
Field: "age",
Message: fmt.Sprintf("unexpected age: %d", user.Age),
}
}
return user, nil
}
// ---------- HTTP Layer ----------
func HandleGetUser(id int) {
user, err := GetUser(id)
if err != nil {
var nf NotFoundError
var ve ValidationError
var de DBError
switch {
case errors.As(err, &nf):
fmt.Printf("[404] %v\n", err)
case errors.As(err, &ve):
fmt.Printf("[400] %v\n", err)
case errors.As(err, &de):
fmt.Printf("[500] db error: %v\n", de)
fmt.Printf("[500] Internal: %+v\n", de.Err)
default:
fmt.Printf("[500] %v\n", err)
}
return
}
fmt.Printf("[200] User: %+v\n", user)
}
func main() {
// Normal
HandleGetUser(1)
// Input error (ValidationError with additional information)
HandleGetUser(0)
// User does not exist (custom NotFoundError)
HandleGetUser(999)
fmt.Println("\n=== Program Exited Normally ===")
}
النتيجة المتوقعة:
[200] User: &{ID:1 Name:Alice Age:28}
[400] validation failed: id - must be positive
[404] user with id 999 not found
=== Program Exited Normally ===
flowchart TD
A[Function returns error] --> B{err == nil?}
B -->|Yes| C[Normal processing]
B -->|No| D[Determine error type]
D --> E[errors.Is / == sentinel]
D --> F[errors.As / type assertion]
D --> G[type switch]
E --> H[Handle specific sentinel error]
F --> I[Extract structured error info]
G --> J[Branch by type]
H --> K[Return or retry]
I --> K
J --> K
Unwrap() error في البنية DBError عاملاً أساسياً للسماح للأخطاء المخصصة بالمشاركة في سلسلة الأخطاء. وإذا كان النوع المخصص لا يحتوي على طريقة Unwrap()، فإن errors.Is وerrors.As ستقومان بالتحقق من الطبقة الخارجية فقط.
❓ أسئلة شائعة
error؟error هي واجهة مدمجة: type error interface { Error() string }. أي نوع ينفذ الطريقة Error() string يُعتبر error — وهي قيمة واجهة مكونة من 16 بايت.Error() string. إذا كنت ترغب في دعم تسلسل الأخطاء (التجول errors.Is/As)، أضف طريقة Unwrap() error التي تُرجع الخطأ الداخلي.recover بعد panic؟recover لا يكون مفيدًا إلا داخل كتلة defer، ويجب وضعه فقط عند مدخل goroutine (go func() { defer recover() }). لا تستخدم recover في منطق العمل الخاص بك — فذلك يخفي الأخطاء بدلاً من إصلاحها.errors.Is وerrors.As؟errors.Is(err, target) بإجراء مقارنات القيم على طول سلسلة %w، مستوىً تلو الآخر (==)؛ بينما يقوم errors.As(err, &target) بإجراء عمليات التحقق من النوع خطوة بخطوة على طول السلسلة ويملأ target. ببساطة: Is يتحقق من القيمة، بينما As يستخرج النوع.go mod التبعيات؟go mod init للتهيئة → كتابة الكود واستخدام import → go mod tidy للتنزيل والتنظيم تلقائيًا → تثبيت الإصدارات باستخدام go.mod وgo.sum. يقدم Go 1.22+ الأمر go mod add، وهو أكثر سهولة في الاستخدام.public/private.fmt.Errorf(%w) وfmt.Errorf(%v)؟%w تُنشئ خطأً يتضمن سلسلة أخطاء يمكن لـ errors.Is/As تتبعها؛ أما %v فتقوم ببساطة بتنسيق سلسلة نصية وإنشاء خطأ جديد لا علاقة له بالخطأ الأصلي.fmt.Errorf("context: %w", err) للحفاظ على سلسلة الأخطاء؛ (2) حدد أنواع أخطاء الأعمال باستخدام حقول إضافية؛ (3) قم بمعالجة الأخطاء بشكل موحد على مستوى معالج HTTP → رموز حالة HTTP؛ (4) قم بتسجيل السلسلة الكاملة (%+v).📖 ملخص
errorهي واجهة مدمجة؛ وأي نوع يُنفِّذError() stringيُعتبرerror- 4 طرق لإنشاء أخطاء:
errors.New/fmt.Errorf/%w/ التفاف / الأنواع المخصصة errors.Isيتحقق مما إذا كان الهدف مدرجًا في سلسلة الأخطاء (مطابقة القيمة)errors.Asيستخرج الأخطاء من نوع معين من سلسلة الأخطاء (بناءً على مطابقة النوع)- يُستخدم
panicفي حالة الأخطاء التي لا يمكن إصلاحها؛ أماrecoverفهو صالح فقط داخل كتلةdefer go mod init/tidy/addلإدارة التبعيات الخارجية- هناك قاعدة واحدة فقط لتصدير الحزم: الأحرف الكبيرة = عامة، والأحرف الصغيرة = خاصة
- كود الإنتاج: الأخطاء المتداخلة + أنواع الأخطاء المخصصة + المعالجة الموحدة
📝 تمارين
-
المسألة الأساسية (الصعوبة ⭐): عرّف دالة
Dividefunc Divide(a, b float64) (float64, error)التي تُرجعerrors.New("division by zero")إذا كان القاسم يساوي 0، وتُرجع الناتج في الحالات الأخرى. -
مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ
ConfigLoaderالذي يدعم تحميل الإعدادات من ملف JSON واللجوء إلى متغيرات البيئة في حالة الفشل. المتطلبات: قم بتغليف كل مستوى خطأ بعلامةfmt.Errorf(%w)، واسمح للمستدعي باستخدامerrors.Isلتحديد ما إذا كان الخطأ هو «ملف غير موجود» أم «خطأ في تحليل JSON». -
مشكلة التحدي (الصعوبة ⭐⭐⭐): قم ببناء بنية من ثلاث طبقات لمعالجة الأخطاء: (1) طبقة البيانات
Repository→ تُرجعNotFoundError/DBError؛ (2) طبقة الخدمةService→ تغلف أخطاء طبقة البيانات + تضيفValidationError؛ (3) معالج HTTP → تحليل الأخطاء طبقةً طبقةً باستخدامerrors.Asوربطها برموز حالة HTTP (404/400/500). يجب أن تتضمن بنية الخطأ الحقول الخاصة بالأعمال (ID/الحقل/العملية).