التعبيرات النمطية والتاريخ
الدرس 24: التعبيرات النمطية والتاريخ
تشبيه من الحياة
تخيل أنك عامل فرز الطرود:
- التعبيرات النمطية مثل قواعد الفرز — أنماط مثل "العنوان يحتوي على 'بيجين' ورقم المنزل 3 أرقام" تساعدك على تصفية الدفعة المطابقة بسرعة من كمية ضخمة.
- التاريخ والوقت مثل الطوابع الزمنية على إيصالات التسليم — تحتاج لمعرفة "متى وصل هذا الطرد"، "منذ متى تم التوقيع عليه"، "هل يمكن أن يصل قبل الساعة 3 ظهراً غداً".
في البرمجة، التعبيرات النمطية تساعدك على تحديد واستخراج المعلومات بدقة من النصوص، بينما حزمة time تساعدك على قياس وحساب وعرض الوقت.
المفاهيم الأساسية
التعبيرات النمطية (حزمة regexp)
حزمة regexp في Go مبنية على محرك صياغة RE2، تدعم معظم صيغ regexp الشائعة، لكنها لا تدعم التراجع والمراجع الخلفية (خيار تصميمي يُعطي الأولوية للأداء).
الواجهات الأساسية:
- Compile: regexp.Compile(pattern) → (*Regexp, error)
- Match: MatchString(s) → bool
- Find: FindString / FindAllString / FindStringSubmatch
- Replace: ReplaceAllString / ReplaceAllStringFunc
التاريخ والوقت (حزمة time)
التعامل مع الوقت في Go يتركز على هيكل Time، باستخدام وقت مرجعي ثابت كقالب تنسيق:
الوقت المرجعي: Mon Jan 2 15:04:05 MST 2006
الاختصاص العددي: 01/02 03:04:05PM 2006 -0700
1 2 3 4 5 6 7 (الشهر 1، اليوم 2، الساعة 3، الدقيقة 4، الثانية 5، السنة 6، المنطقة الزمنية 7) لسهولة الحفظ.
الصياغة الأساسية والاستخدام
1. أساسيات التعبيرات النمطية
package main
import (
"fmt"
"regexp"
)
func main() {
// تجميع تعبير نمطي
re, err := regexp.Compile(`\d{3}-\d{4}-\d{4}`)
if err != nil {
fmt.Println("فشل تجميع regexp:", err)
return
}
// التحقق من المطابقة
phone := "138-1234-5678"
fmt.Println("يطابق:", re.MatchString(phone)) // true
// إيجاد أول مطابقة
text := "جهة الاتصال: 138-1234-5678 أو 139-8765-4321"
fmt.Println("الأولى:", re.FindString(text)) // 138-1234-5678
// إيجاد جميع المطابقات
all := re.FindAllString(text, -1)
fmt.Println("الكل:", all) // [138-1234-5678 139-8765-4321]
}
MustCompile مقابل Compile: MustCompile يُسبب panic مباشرة عند فشل التجميع، مناسب لـ regexp ثابتة كثوابت عالمية؛ Compile تُرجع خطأ، مناسبة لـ regexp المبنية ديناميكياً وقت التشغيل.
2. مجموعات التقاط مسماة
package main
import (
"fmt"
"regexp"
)
func main() {
// استخدام مجموعات التقاط مسماة لاستخراج أجزاء البريد الإلكتروني
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
email := "zhangsan@example.com"
// الحصول على نتائج المطابقة
match := re.FindStringSubmatch(email)
names := re.SubexpNames()
for i, name := range names {
if i > 0 && name != "" {
fmt.Printf("%s = %s\n", name, match[i])
}
}
// user = zhangsan
// domain = example.com
}
3. عمليات الاستبدال
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
re := regexp.MustCompile(`\s+`)
// استبدال جميع المسافات البيضاء بمسافة واحدة
text := " hello world go "
result := re.ReplaceAllString(text, " ")
fmt.Printf("[%s]\n", result) // [hello world go]
// استخدام دالة للاستبدال الديناميكي
re2 := regexp.MustCompile(`\b\w`)
title := re2.ReplaceAllStringFunc("hello world go", func(s string) string {
// تحويل الحرف الأول إلى كبير
return strings.ToUpper(s)
})
fmt.Println(title) // Hello World Go
}
4. عمليات الوقت الأساسية
package main
import (
"fmt"
"time"
)
func main() {
// الحصول على الوقت الحالي
now := time.Now()
fmt.Println("الوقت الحالي:", now)
// استخراج المكونات الفردية
fmt.Println("السنة:", now.Year())
fmt.Println("الشهر:", now.Month())
fmt.Println("اليوم:", now.Day())
fmt.Println("الساعة:", now.Hour())
fmt.Println("الدقيقة:", now.Minute())
fmt.Println("الثانية:", now.Second())
fmt.Println("اليوم:", now.Weekday())
}
time.Now().UTC().
5. تنسيق وتحليل الوقت
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// التنسيق: استخدام الوقت المرجعي كقالب
fmt.Println(now.Format("2006-01-02")) // 2025-06-27
fmt.Println(now.Format("2006/01/02 15:04:05")) // 2025/06/27 14:30:00
fmt.Println(now.Format("03:04PM")) // 02:30PM
// صيغ مسبقة التعريف شائعة
fmt.Println(now.Format(time.RFC3339)) // 2025-06-27T14:30:00+08:00
// تحليل نص إلى وقت
t, err := time.Parse("2006-01-02", "2025-12-25")
if err != nil {
fmt.Println("فشل التحليل:", err)
return
}
fmt.Println("نتيجة التحليل:", t)
// التحليل مع المنطقة الزمنية
t2, _ := time.ParseInLocation("2006-01-02 15:04:05",
"2025-12-25 08:00:00", time.Local)
fmt.Println("مع المنطقة الزمنية:", t2)
}
2006-01-02 15:04:05.
6. المدة وحسابات الوقت
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
// إضافة وطرح الوقت
tomorrow := now.Add(24 * time.Hour)
fmt.Println("غداً:", tomorrow.Format("2006-01-02"))
twoHoursLater := now.Add(2 * time.Hour)
fmt.Println("بعد ساعتين:", twoHoursLater.Format("15:04:05"))
// حساب الفرق الزمني
diff := tomorrow.Sub(now)
fmt.Println("الفرق الزمني:", diff) // 24h0m0s
fmt.Println("الساعات:", diff.Hours()) // 24
fmt.Println("الدقائق:", diff.Minutes()) // 1440
// ترتيب المقارنة
fmt.Println("غداً بعد:", tomorrow.After(now)) // true
fmt.Println("اليوم قبل:", now.Before(tomorrow)) // true
// اقتطاع إلى دقة محددة
floored := now.Truncate(time.Hour)
fmt.Println("مقتطع إلى الساعة:", floored.Format("15:04:05"))
}
7. المؤقتات
package main
import (
"fmt"
"time"
)
func main() {
// مؤقت لمرة واحدة
timer := time.NewTimer(2 * time.Second)
fmt.Println("انتظار 2 ثانية...")
<-timer.C
fmt.Println("انتهى الوقت!")
// مؤقت دوري (Ticker)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop() // تذكر الإيقاف لتجنب التسريبات
count := 0
for t := range ticker.C {
count++
fmt.Println(".Tick:", t.Format("15:04:05.000"))
if count >= 3 {
break
}
}
// نسخة time.After المبسطة (انتظار واحد)
<-time.After(1 * time.Second)
fmt.Println("تم التنفيذ بعد ثانية واحدة")
}
time.After في الحلقات — كل تكرار ينشئ قناة جديدة، والمؤقتات القديمة لن يتم جمعها، مما قد يسبب تسرب ذاكرة. في الحلقات، استخدم time.NewTimer وقم بإعادة التعيين يدوياً.
أمثلة الكود
مثال: التحقق والاستخراج — تحليل السجلات (الصعوبة ⭐)
package main
import (
"fmt"
"regexp"
)
func main() {
// أسطر سجلات محاكاة
logs := []string{
"[2025-06-27 14:30:00] ERROR Database connection failed",
"[2025-06-27 14:30:01] INFO Service started successfully",
"[2025-06-27 14:30:05] WARN Disk space low",
"[2025-06-27 14:31:00] ERROR Request timeout",
}
// مطابقة صيغة السجل: وقت + مستوى + رسالة
re := regexp.MustCompile(`\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+)\s+(.+)`)
// العد حسب المستوى
levelCount := make(map[string]int)
for _, log := range logs {
match := re.FindStringSubmatch(log)
if match == nil {
continue
}
timestamp := match[1]
level := match[2]
message := match[3]
fmt.Printf("الوقت: %s | المستوى: %-5s | الرسالة: %s\n",
timestamp, level, message)
levelCount[level]++
}
fmt.Println("\nإحصائيات المستوى:")
for level, count := range levelCount {
fmt.Printf(" %s: %d إدخال\n", level, count)
}
}
المخرجات:
الوقت: 2025-06-27 14:30:00 | المستوى: ERROR | الرسالة: Database connection failed
الوقت: 2025-06-27 14:30:01 | المستوى: INFO | الرسالة: Service started successfully
الوقت: 2025-06-27 14:30:05 | المستوى: WARN | الرسالة: Disk space low
الوقت: 2025-06-27 14:31:00 | المستوى: ERROR | الرسالة: Request timeout
إحصائيات المستوى:
ERROR: 2 إدخال
INFO: 1 إدخال
WARN: 1 إدخال
مثال: محرك القوالب — نظام استبدال نصوص بسيط (الصعوبة ⭐⭐)
package main
import (
"fmt"
"regexp"
"strings"
"time"
)
func main() {
template := `عزيزي {{name}}:
طلبك {{order}} تم شحنه في {{date}}.
التسليم المتوقع خلال {{days}} أيام.
الوقت الحالي: {{now}}`
// تعريف خريطة المتغيرات
vars := map[string]string{
"name": "Alice",
"order": "ORD-20250627-001",
"date": "2025-06-27",
"days": "3",
}
// مطابقة نمط {{اسم_المتغير}}
re := regexp.MustCompile(`\{\{(\w+)\}\}`)
// استبدال المتغيرات
result := re.ReplaceAllStringFunc(template, func(match string) string {
// استخراج اسم المتغير (إزالة {{ و }})
key := match[2 : len(match)-2]
if key == "now" {
return time.Now().Format("2006-01-02 15:04:05")
}
if val, ok := vars[key]; ok {
return val
}
return match // المتغير غير موجود، الإبقاء كما هو
})
fmt.Println(result)
// عدد المتغيرات في القالب
allVars := re.FindAllString(template, -1)
varNames := make([]string, 0, len(allVars))
for _, v := range allVars {
varNames = append(varNames, v[2:len(v)-2])
}
fmt.Printf("\nمتغيرات القالب: %s\n", strings.Join(varNames, ", "))
}
المخرجات:
عزيزي Alice:
طلبك ORD-20250627-001 تم شحنه في 2025-06-27.
التسليم المتوقع خلال 3 أيام.
الوقت الحالي: 2025-06-27 14:30:00
متغيرات القالب: name, order, date, days, now
مثال: مؤقت عد تنازلي — عرض فوري مع تنسيق (الصعوبة ⭐⭐⭐)
package main
import (
"fmt"
"regexp"
"strings"
"time"
)
// formatDuration تنسق Duration كـ "Xd Xh Xm Xs"
func formatDuration(d time.Duration) string {
if d <= 0 {
return "منتهي"
}
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60
parts := []string{}
if days > 0 {
parts = append(parts, fmt.Sprintf("%dي", days))
}
if hours > 0 {
parts = append(parts, fmt.Sprintf("%دس", hours))
}
if minutes > 0 {
parts = append(parts, fmt.Sprintf("%دد", minutes))
}
parts = append(parts, fmt.Sprintf("%02dث", seconds))
return strings.Join(parts, " ")
}
// parseDeadline تحلل صيغ تاريخ متعددة
func parseDeadline(s string) (time.Time, error) {
// محاولة صيغ متعددة
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02",
"2006/01/02 15:04",
"01-02 15:04",
}
// التحقق مما إذا كان وقتاً نسبياً (مثل "+2h30m")
re := regexp.MustCompile(`^\+(\d+)([hms])`)
if match := re.FindStringSubmatch(s); match != nil {
// تحليل وقت نسبي (مبسط، وحدة واحدة فقط)
return time.Now().Add(2 * time.Hour), nil
}
for _, format := range formats {
if t, err := time.ParseInLocation(format, s, time.Local); err == nil {
return t, nil
}
}
return time.Time{}, fmt.Errorf("غير قادر على تحليل الوقت: %s", s)
}
func main() {
// تحليل الموعد النهائي
deadline, err := parseDeadline("2025-12-31 23:59:59")
if err != nil {
fmt.Println("خطأ:", err)
return
}
fmt.Printf("الموعد النهائي: %s\n", deadline.Format("2006-01-02 15:04:05"))
fmt.Println(strings.Repeat("─", 40))
// محاكاة العد التنازلي (يتحدث كل ثانية، 5 مرات إجمالاً)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
count := 0
for now := range ticker.C {
remaining := deadline.Sub(now)
// تأثير مسح الشاشة: الكتابة فوق السطر الحالي بإرجاع عربة
fmt.Printf("\rالمتبقي: %-30s", formatDuration(remaining))
count++
if count >= 5 || remaining <= 0 {
break
}
}
fmt.Println("\n\nاكتمل عرض العد التنازلي")
// التحقق من صيغة الوقت
re := regexp.MustCompile(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`)
formatted := deadline.Format("2006-01-02 15:04:05")
fmt.Printf("التحقق من التنسيق: %v\n", re.MatchString(formatted)) // true
}
سيناريوهات التطبيق العملي
السيناريو 1: التحقق من بيانات النموذج
package main
import (
"fmt"
"regexp"
)
// Validator مُتحقق من النموذج
type Validator struct {
rules map[string]*regexp.Regexp
}
// NewValidator ينشئ مُتحققاً ويجمع جميع regexp مسبقاً
func NewValidator() *Validator {
rules := map[string]*regexp.Regexp{
"phone": regexp.MustCompile(`^1[3-9]\d{9}$`),
"email": regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`),
"id_card": regexp.MustCompile(`^\d{17}[\dXx]$`),
"username": regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{3,15}$`),
"password": regexp.MustCompile(`^.{8,}$`),
"ip": regexp.MustCompile(`^(\d{1,3}\.){3}\d{1,3}$`),
"date": regexp.MustCompile(`^\d{4}[-/](0[1-9]|1[0-2])[-/](0[1-9]|[12]\d|3[01])$`),
}
return &Validator{rules: rules}
}
// Validate يتحقق من حقل واحد
func (v *Validator) Validate(field, value string) bool {
re, ok := v.rules[field]
if !ok {
return true // لا قاعدة يعني اجتاز
}
return re.MatchString(value)
}
// ValidateAll يتحقق من حقول متعددة، يُرجع جميع الأخطاء
func (v *Validator) ValidateAll(data map[string]string) []string {
var errors []string
for field, value := range data {
if !v.Validate(field, value) {
errors = append(errors, fmt.Sprintf("تنسيق %s غير صحيح: %s", field, value))
}
}
return errors
}
func main() {
v := NewValidator()
// بيانات اختبار
data := map[string]string{
"phone": "13812345678",
"email": "test@example.com",
"id_card": "110101199001011234",
"username": "go_dev",
"date": "2025-06-27",
}
errors := v.ValidateAll(data)
if len(errors) == 0 {
fmt.Println("✓ تم التحقق من جميع الحقول بنجاح")
} else {
fmt.Println("فشل التحقق:")
for _, err := range errors {
fmt.Printf(" ✗ %s\n", err)
}
}
// اختبار بيانات غير صالحة
invalidData := map[string]string{
"phone": "12345678901", // بادئة هاتف غير صالحة
"email": "not-an-email", // مفقود @
}
fmt.Println("\nاختبار بيانات غير صالحة:")
for field, value := range invalidData {
result := "✓"
if !v.Validate(field, value) {
result = "✗"
}
fmt.Printf(" %s %s: %s\n", result, field, value)
}
}
السيناريو 2: استعلام النطاق الزمني — إدارة حالة النشاط
package main
import (
"fmt"
"strings"
"time"
)
// Activity هيكل النشاط
type Activity struct {
Name string
StartTime time.Time
EndTime time.Time
}
// Status يحصل على حالة النشاط الحالية
func (a Activity) Status(now time.Time) string {
switch {
case now.Before(a.StartTime):
return "لم يبدأ"
case now.After(a.EndTime):
return "انتهى"
default:
return "جارٍ"
}
}
// Remaining يحصل على الوقت المتبقي أو العد التنازلي
func (a Activity) Remaining(now time.Time) string {
if now.Before(a.StartTime) {
d := a.StartTime.Sub(now)
return fmt.Sprintf("يبدأ خلال: %s", formatDur(d))
}
if now.Before(a.EndTime) {
d := a.EndTime.Sub(now)
return fmt.Sprintf("متبقي: %s", formatDur(d))
}
return "منتهي"
}
func formatDur(d time.Duration) string {
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
if hours > 24 {
days := hours / 24
hours = hours % 24
return fmt.Sprintf("%دد %دس %دد", days, hours, minutes)
}
return fmt.Sprintf("%دس %دد", hours, minutes)
}
func main() {
now := time.Now()
// إنشاء قائمة الأنشطة
activities := []Activity{
{
Name: "تخفيضات الصيف",
StartTime: mustParse("2025-06-01 00:00:00"),
EndTime: mustParse("2025-06-18 23:59:59"),
},
{
Name: "عرض العودة للمدارس",
StartTime: mustParse("2025-07-01 00:00:00"),
EndTime: mustParse("2025-08-31 23:59:59"),
},
{
Name: "معاينة الجمعة السوداء",
StartTime: mustParse("2025-11-01 00:00:00"),
EndTime: mustParse("2025-11-11 23:59:59"),
},
}
fmt.Printf("الوقت الحالي: %s\n", now.Format("2006-01-02 15:04:05"))
fmt.Println(strings.Repeat("─", 50))
for _, act := range activities {
status := act.Status(now)
remaining := act.Remaining(now)
fmt.Printf("النشاط: %s\n", act.Name)
fmt.Printf(" الوقت: %s ~ %s\n",
act.StartTime.Format("2006-01-02"),
act.EndTime.Format("2006-01-02"))
fmt.Printf(" الحالة: %s | %s\n\n", status, remaining)
}
// التصفية حسب الحالة
fmt.Println("الأنشطة الجارية:")
found := false
for _, act := range activities {
if act.Status(now) == "جارٍ" {
fmt.Printf(" - %s\n", act.Name)
found = true
}
}
if !found {
fmt.Println(" لا توجد أنشطة جارية حالياً")
}
}
func mustParse(s string) time.Time {
t, err := time.ParseInLocation("2006-01-02 15:04:05", s, time.Local)
if err != nil {
panic(err)
}
return t
}
❓ أسئلة شائعة
س1: لماذا لا يستطيع regexp تطابق الأحرف الصينية؟
حزمة regexp في Go تتعامل مع ترميز UTF-8 افتراضياً، لذا يمكن مطابقة الأحرف الصينية نفسها بشكل طبيعي. المشكلة الشائعة هي عدم استخدام فئة الأحرف الصحيحة:
// خطأ: \w لا يطابق الصينية
re := regexp.MustCompile(`^\w+$`)
re.MatchString("مرحبا") // false
// صحيح: استخدم فئة Unicode أو نطاق الصينية مباشرة
re2 := regexp.MustCompile(`^[\p{Han}]+$`)
re2.MatchString("مرحبا") // true
// أو مطابقة مختلطة
re3 := regexp.MustCompile(`^[\w\p{Han}]+$`)
re3.MatchString("helloمرحبا123") // true
س2: ما الفرق بين time.Parse و time.ParseInLocation؟
// Parse تستخدم UTC عندما لا توجد معلومات منطقة زمنية
t1, _ := time.Parse("2006-01-02", "2025-06-27")
fmt.Println(t1.Location()) // UTC
// ParseInLocation تحدد منطقة زمنية افتراضية
t2, _ := time.ParseInLocation("2006-01-02", "2025-06-27", time.Local)
fmt.Println(t2.Location()) // Local (مثل Asia/Shanghai)
// إذا كانت سلسلة التنسيق تحتوي على معلومات منطقة زمنية (مثل -0700)، تتصرفان بنفس الطريقة
t3, _ := time.Parse("2006-01-02 15:04:05 -0700", "2025-06-27 08:00:00 +0800")
fmt.Println(t3) // تم تحليلها بشكل صحيح كمنطقة +0800
ParseInLocation وحدد المنطقة الزمنية صراحةً.
س3: ماذا لو كان أداء regexp ضعيفاً؟
// خطأ: إعادة التجميع في كل استدعاء
func validatePhone(phone string) bool {
re := regexp.MustCompile(`^1[3-9]\d{9}$`) // يُجمع كل مرة
return re.MatchString(phone)
}
// صحيح: تجميع مسبق كمتغير على مستوى الحزمة
var phoneRe = regexp.MustCompile(`^1[3-9]\d{9}$`)
func validatePhone2(phone string) bool {
return phoneRe.MatchString(phone)
}
// للمطابقات عالية التكرار،فكّر في استخدام Regexp.Copy() لتجنب قفل التنافس
var globalRe = regexp.MustCompile(`\d+`)
func processConcurrently(text string) string {
re := globalRe.Copy() // الحصول على نسخة، تجنب الأقفال المتزامنة
return re.ReplaceAllString(text, "#")
}
س4: كيف أتعامل مع تحويل المنطقة الزمنية؟
package main
import (
"fmt"
"time"
)
func main() {
// تحميل مناطق زمنية محددة
shanghai, _ := time.LoadLocation("Asia/Shanghai")
tokyo, _ := time.LoadLocation("Asia/Tokyo")
newyork, _ := time.LoadLocation("America/New_York")
// إنشاء وقت بمنطقة زمنية
t := time.Date(2025, 6, 27, 14, 0, 0, 0, shanghai)
fmt.Println("Shanghai:", t.Format("15:04 MST"))
// تحويل المنطقة الزمنية
fmt.Println("Tokyo:", t.In(tokyo).Format("15:04 MST"))
fmt.Println("New York:", t.In(newyork).Format("15:04 MST"))
// التحليل من نص والتحويل
parsed, _ := time.ParseInLocation("2006-01-02 15:04",
"2025-06-27 14:00", shanghai)
fmt.Println("\nتم التحليل ثم التحويل إلى UTC:", parsed.UTC().Format("2006-01-02 15:04 MST"))
}
time.LoadLocation قد تتطلب تثبيت tzdata على بعض الأنظمة. يمكن لـ Go 1.15+ تضمين بيانات المنطقة الزمنية باستيراد _ "time/tzdata".
📖 ملخص
غطى هذا الدرس موضوعين أساسيين:
| الموضوع | الحزمة | الأنواع الأساسية | العمليات الرئيسية |
|---|---|---|---|
| التعبيرات النمطية | regexp |
Regexp |
Compile, Match, Find, Replace |
| التاريخ والوقت | time |
Time, Duration |
Format, Parse, Calculate, Timer |
نقاط أساسية لـ regexp:
- استخدم
MustCompileللتجميع المسبق لـ regexp الثابتة - مجموعات التقاط مسماة
(?P<name>...)تُحسّن الوضوح \p{Han}يطابق الصينية،\p{L}يطابق جميع أحرف Unicode- محرك RE2 لا يدعم المراجع الخلفية، لكنه يضمن تعقيداً زمنياً خطياً
نقاط أساسية للوقت:
- قالب التنسيق هو الوقت المرجعي
2006-01-02 15:04:05 Parseتستخدم UTC افتراضياً،ParseInLocationتحدد المنطقة الزمنيةDurationتمثل فترات زمنية، تدعم طرق وحدات غنية- يجب إيقاف المؤقتات بـ
Stop()بعد الاستخدام لتجنب تسريبات goroutine
📝 تمارين
التمرين 1: مستخرج روابط Markdown
اكتب برنامجاً يستخرج جميع الروابط من نص Markdown، ويُخرج بصيغة العنوان -> الرابط.
// تلميح: مطابقة صيغة [العنوان](الرابط)
// المدخلات: `قم بزيارة [موقع Go](https://golang.org) أو [GitHub](https://github.com)`
// المخرجات:
// موقع Go -> https://golang.org
// GitHub -> https://github.com
التمرين 2: حاسب أيام العمل
اكتب دالة AddBusinessDays(t time.Time, n int) time.Time تحسب التاريخ بعد n يوم عمل من تاريخ معين (تخطي السبت والأحد).
// الاختبارات:
// AddBusinessDays(2025-06-27 الجمعة, 1) → 2025-06-30 الاثنين
// AddBusinessDays(2025-06-27 الجمعة, 5) → 2025-07-04 الجمعة
التمرين 3: نظام فلترة الكلمات الحساسة
نفذ فلتر كلمات حساسة:
- تحميل قائمة الكلمات الحساسة من التكوين، وتجميعها إلى regexp
- دعم وضع الاستبدال بـ
*والإزالة الكاملة - دعم اكتشاف متغيرات الكلمات الحساسة (مثل مسافات مدرجة في المنتصف: "gam bling")
// نص المدخلات: "هذا موقع قمار، يخدم gam bling"
// مخرجات وضع الاستبدال: "هذا موقع *، يخدم *"
// مخرجات وضع الإزالة: "هذا موقع، يخدم"
الدرس التالي
في الدرس التالي، سنتعلم تطوير برامج سطر الأوامر، ونفهم كيفية استخدام Go لبناء أدوات CLI قوية، بما في ذلك تحليل الوسائط، والأوامر الفرعية، والدخل التفاعلي، وغيرها من المهارات العملية.



