Go: Go Map والهياكل
آخر تحديث: 2026-08-26
تُعد «الخرائط» و«الهياكل» الركيزتين الأساسيتين لنمذجة البيانات في لغة Go — حيث تتولى «الخرائط» معالجة الاستعلامات الديناميكية، بينما تصف «الهياكل» البنى الثابتة — ويمكنهما معًا وصف 90% من كائنات الأعمال.
تتيح الخرائط (Maps) إجراء عمليات البحث عن القيم باستخدام المفاتيح بزمن O(1)، بينما تقوم البنيات (structs) بتجميع البيانات باستخدام الحقول. في هذا الدرس، ستتقن جميع المفاهيم الأساسية لنمذجة البيانات في لغة Go وستتمكن من استخدامها لبناء نظام مخزون متكامل للتجارة الإلكترونية.
1. ستتعلم
- CRUD (إنشاء/قراءة/تحديث/حذف) للخرائط
- comma-ok: يتحقق من وجود المفتاح
- التكرار على عناصر نطاق الخريطة وحذفها
- تعريف الحقول في البنية وتهيئتها والوصول إليها
- علامة البنية (مفتاح للتسلسل إلى JSON)
- الهياكل المتداخلة والتركيب
- الاختيار بين الخريطة والشريحة
- إنشاء نظام جرد وحدات التخزين (SKU) للتجارة الإلكترونية باستخدام «Maps» و«Structs»
2. القصة الحقيقية لمهندس في مجال التجارة الإلكترونية
(1) المشكلة: عمليات البحث في الشرائح بطيئة للغاية
يعمل بوب كمهندس خلفي في إحدى منصات التجارة الإلكترونية. ومع انطلاق حملة التخفيضات المسبقة لـ«Double 11»، أصبحت واجهة برمجة التطبيقات (API) الخاصة بالاستعلام عن المخزون بطيئة بشكل لا يُصدق:
"لدينا 100,000 وحدة تخزين (SKU) في المخزون، مخزنة باستخدام شريحة
[]Product، وكل استعلام يتطلب مسحًا خطيًّا. مع زيادة معدل الاستعلامات في الثانية (QPS)، يبلغ متوسط وقت الاستجابة 800 مللي ثانية مع استخدام بنسبة 100% لوحدة المعالجة المركزية (CPU). وقد منحني مديري ثلاثة أيام لتحسين هذا الأداء بحيث يصبح أقل من 100 مللي ثانية."
فتح الكود الذي كان قد كتبه:
// Version 1: Store all products using a slice
var products []Product
func findProduct(sku string) *Product {
for _, p := range products { // O(n) linear scan
if p.SKU == sku {
return &p
}
}
return nil
}
100,000 منتج × 1,000 عملية في الثانية = 100 مليون عملية مسح في الثانية؛ ولا مفر من وصول معدل استخدام وحدة المعالجة المركزية (CPU) إلى 100%.
(2) الحل بلغة Go: استخدام خريطة (map) لإجراء عمليات البحث بزمن O(1)
// inventory.go
package main
import "fmt"
type Product struct {
SKU string // Product unique identifier
Name string // Product name
Price float64 // Price
Stock int // Stock count
Category string // Classification
}
// Using map to create a hash table
type Inventory struct {
products map[string]Product // SKU -> Product
}
func NewInventory() *Inventory {
return &Inventory{products: make(map[string]Product)}
}
// O(1) query
func (inv *Inventory) Find(sku string) (Product, bool) {
p, ok := inv.products[sku]
return p, ok
}
// O(1) update
func (inv *Inventory) UpdateStock(sku string, delta int) error {
p, ok := inv.products[sku]
if !ok {
return fmt.Errorf("SKU %s not found", sku)
}
p.Stock += delta
inv.products[sku] = p
return nil
}
func main() {
inv := NewInventory()
// Bulk import of 100,000 SKUs
for i := 0; i < 100000; i++ {
sku := fmt.Sprintf("SKU-%05d", i)
inv.products[sku] = Product{
SKU: sku,
Name: fmt.Sprintf("Product-%d", i),
Price: 99.99,
Stock: 100,
Category: "electronics",
}
}
// Query performance comparison
p, ok := inv.Find("SKU-50000")
if ok {
fmt.Printf("Found: %s, price=%.2f\n", p.Name, p.Price)
}
// Update inventory
inv.UpdateStock("SKU-50000", -5)
}
الناتج:
Found: Product-50000, price=99.99
(3) الأداء: مقارنة أداء الاستعلامات باستخدام «slice» و«map»
| حجم البيانات | المسح الخطي في الشريحة | البحث عن التجزئة في الخريطة | الفارق في الأداء |
|---|---|---|---|
| 100 SKU | 50 نانو ثانية | 50 نانو ثانية | مكافئ |
| 1,000 SKU | 500 نانو ثانية | 50 نانو ثانية | 10x |
| 10,000 SKU | 5 ميكروثانية | 50 نانوثانية | 100x |
| 100,000 SKU | 50 ميكروثانية | 50 نانوثانية | 1,000x |
3. أساسيات الخرائط
(1) ثلاث طرق لإنشاء خريطة
package main
import "fmt"
func main() {
// Method 1: make (Recommended)
m1 := make(map[string]int) // empty map, writable
// Method 2: make + pre-allocated capacity
m2 := make(map[string]int, 100) // pre-allocate 100 capacity, reduce resizing
// Method 3: Literal Initialization
m3 := map[string]int{
"Alice": 28,
"Bob": 32,
}
// Method 4: nil map (read-only, cannot be written to)
var m4 map[string]int // == nil, cannot do m4["a"] = 1
_ = m4
}
(2) خريطة CRUD
package main
import "fmt"
func main() {
ages := make(map[string]int)
// Create
ages["Alice"] = 28
ages["Bob"] = 32
// Read
fmt.Println(ages["Alice"]) // 28
// Update
ages["Alice"] = 29
// Delete
delete(ages, "Bob")
// Length
fmt.Printf("len=%d\n", len(ages))
fmt.Println(ages) // map[Alice:29]
}
الناتج:
28
len=1
map[Alice:29]
▶ مثال: تكرار الخريطة (الترتيب عشوائي)
package main
import "fmt"
func main() {
ages := map[string]int{"Alice": 28, "Bob": 32, "Charlie": 45}
// for range: key + value
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Only the key
for name := range ages {
fmt.Printf("name: %s\n", name)
}
// Only the value (use _ to ignore the key)
for _, age := range ages {
fmt.Printf("age: %d\n", age)
}
}
الناتج (بترتيب عشوائي):
Alice is 28 years old
Charlie is 45 years old
Bob is 32 years old
name: Bob
...
4. قواعد استخدام الفاصلة (نقطة أساسية)
(1) التمييز بين «القيمة الصفرية» و«غير موجود»
package main
import "fmt"
func main() {
ages := map[string]int{"Alice": 28}
// Incorrect approach: Cannot distinguish between "key does not exist" and "value = 0"
age := ages["Bob"]
fmt.Printf("Bob's age: %d\n", age) // 0 (but is Bob really 0 years old?)
// Correct approach: comma-ok
age, ok := ages["Bob"]
if ok {
fmt.Printf("Bob's age: %d\n", age)
} else {
fmt.Println("Bob not found")
}
// Just check if it exists: discard the value
_, exists := ages["Bob"]
fmt.Printf("Bob exists: %v\n", exists)
}
الناتج:
Bob's age: 0
Bob not found
Bob exists: false
(2) استخدام الفاصلة في الممارسة العملية: التخزين المؤقت للاستعلامات
package main
import "fmt"
// Cache: key → value
var cache = make(map[string]string)
func getCached(key string) (string, bool) {
val, ok := cache[key]
return val, ok
}
func main() {
// Set cache
cache["user:1"] = "Alice"
cache["user:2"] = "Bob"
// Query cache
if val, ok := getCached("user:1"); ok {
fmt.Printf("Hit: %s\n", val)
} else {
fmt.Println("Miss")
}
if _, ok := getCached("user:999"); !ok {
fmt.Println("user:999 not in cache, fetching from DB...")
}
}
الناتج:
Hit: Alice
user:999 not in cache, fetching from DB...
(3) النمط العام لـ «comma-ok»
| السياق | الصيغة | المعنى |
|---|---|---|
| البحث في الخريطة | v, ok := m[key] |
هل المفتاح موجود؟ |
| تأكيد النوع | v, ok := x.(T) |
مطابقة النوع؟ |
| الاستقبال من القناة | v, ok := <-ch |
هل القناة مغلقة؟ |
5. أساسيات الهياكل (Structs)
(1) تعريف الهياكل وتهيئتها
package main
import "fmt"
// define struct
type User struct {
Name string
Age int
City string
}
func main() {
// Method 1: By field order (not recommended; poor readability)
u1 := User{"Alice", 28, "Shanghai"}
// Method 2: Initializing Field Names (Recommended)
u2 := User{
Name: "Bob",
Age: 32,
City: "Beijing",
}
// Method 3: Partial Initialization (with the remainder set to zero)
u3 := User{Name: "Charlie"} // Age=0, City=""
// Method 4: new() returns a pointer
u4 := new(User)
u4.Name = "Dave"
fmt.Println(u1, u2, u3, u4)
}
الناتج:
{Alice 28 Shanghai} {Bob 32 Beijing} {Charlie } &{Dave 0 }
(2) الوصول إلى الحقول وتعديلها
package main
import "fmt"
type User struct {
Name string
Age int
}
func main() {
u := User{Name: "Alice", Age: 28}
// Read field
fmt.Println(u.Name) // Alice
// Write to a field
u.Age = 29
// Pointer access (automatic dereferencing)
p := &u
fmt.Println(p.Name) // Alice (equivalent to (*p).Name)
p.Age = 30 // automatic dereferencing
}
▶ مثال: نسخ البنية مقابل المؤشرات
package main
import "fmt"
type Counter struct {
Value int
}
// Pass-by-value: Copy the entire struct
func incrementByValue(c Counter) {
c.Value++ // modifies the copy
fmt.Printf("Inside function: %d\n", c.Value)
}
// Pointer passing: pass by reference
func incrementByPointer(c *Counter) {
c.Value++ // modifies the original
fmt.Printf("Inside function: %d\n", c.Value)
}
func main() {
c := Counter{Value: 10}
incrementByValue(c)
fmt.Printf("After value passing: %d\n", c.Value) // 10 (unchanged)
incrementByPointer(&c)
fmt.Printf("After pointer passing: %d\n", c.Value) // 11 (modified)
}
الناتج:
Inside function: 11
After value passing: 10
Inside function: 11
After pointer passing: 11
6. علامة البنية (مفتاح التسلسل إلى JSON)
(1) صيغة علامة البنية
type User struct {
Name string `json:"name" db:"user_name"`
Age int `json:"age" validate:"min=0,max=150"`
Email string `json:"email,omitempty"`
Password string `json:"-"` // - means JSON ignores this field
}
(2) التسلسل العملي لـ JSON
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
Password string `json:"-"` // password not serialized
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Email: "alice@example.com",
Password: "secret123",
}
// Serialization: struct → JSON
data, _ := json.Marshal(u)
fmt.Printf("JSON: %s\n", data)
// Deserialization: JSON → struct
jsonStr := `{"name":"Bob","age":32,"email":"bob@example.com"}`
var u2 User
json.Unmarshal([]byte(jsonStr), &u2)
fmt.Printf("Deserialization: %+v\n", u2)
}
الناتج:
JSON: {"name":"Alice","age":28,"email":"alice@example.com"}
Deserialization: {Name:Bob Age:32 Email:bob@example.com Password:}
(3) مكتبات علامات البنية الشائعة الاستخدام
| المكتبة | اسم العلامة | الغرض |
|---|---|---|
| encoding/json | json:"name" |
اسم حقل JSON |
| gorm | gorm:"primaryKey" |
قيود حقول ORM |
| أداة التحقق من الصحة | validate:"required" |
التحقق من صحة الحقول |
| yaml | yaml:"name" |
تسلسل YAML |
7. الهياكل المتداخلة
(1) الهياكل المتداخلة (التركيب بدلاً من الوراثة)
لا توجد ميزة «الوراثة» في لغة Go؛ فهي تستخدم الهياكل المتداخلة لتحقيق التركيب:
package main
import "fmt"
type Address struct {
City string
Country string
}
type User struct {
Name string
Age int
Address Address // nested Address
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Address: Address{
City: "Shanghai",
Country: "China",
},
}
// Accessing nested fields
fmt.Println(u.Address.City) // Shanghai
fmt.Println(u.Address.Country) // China
}
(2) التداخل المجهول (ترقية الحقول)
package main
import "fmt"
type Address struct {
City string
Country string
}
// Anonymous nesting: fields are "promoted"
type User struct {
Name string
Age int
Address // equivalent to Address Address, but without the field name
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Address: Address{
City: "Shanghai",
Country: "China",
},
}
// Field promotion: direct access, without u.Address.City
fmt.Println(u.City) // Shanghai
fmt.Println(u.Country) // China
// You can also use the full path
fmt.Println(u.Address.City)
}
▶ مثال: العلامات المتداخلة + JSON في التطبيق العملي
package main
import (
"encoding/json"
"fmt"
)
type Address struct {
City string `json:"city"`
Country string `json:"country"`
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
Address Address `json:"address"`
}
func main() {
u := User{
Name: "Alice",
Age: 28,
Email: "alice@example.com",
Address: Address{
City: "Shanghai",
Country: "China",
},
}
data, _ := json.MarshalIndent(u, "", " ")
fmt.Println(string(data))
}
الناتج:
{
"name": "Alice",
"age": 28,
"email": "alice@example.com",
"address": {
"city": "Shanghai",
"country": "China"
}
}
8. الاختيار بين map و slice
(1) جدول مرجعي لاختيار النموذج
| البعد | شريحة []T |
خريطة map[K]V |
|---|---|---|
| طريقة البحث | المسح الخطي O(n) | التجزئة O(1) |
| هل تم طلبه؟ | ✅ تم طلبه | ❌ لم يتم طلبه |
| استخدام الذاكرة | مضغوط (24 + طول*الحجم) | أقل ضغطًا (جدول التجزئة + الحجرة) |
| حذف عنصر | يتطلب نقلًا يدويًّا | delete(m, k) O(1) |
| السيناريوهات النموذجية | القوائم/قوائم الانتظار/المكدسات/الفرز | القواميس/ذاكرات التخزين المؤقت/الفهارس |
(2) شجرة قرار اختيار النموذج
graph TB
A[Need to store multiple elements] --> B{Need to look up by key?}
B -->|Yes| C[Use map<br/>O(1) lookup]
B -->|No| D{Need to maintain order?}
D -->|Yes| E[Use slice]
D -->|No| F{Element count < 100?}
F -->|Yes| G[Either slice or map works]
F -->|No| C
(3) مثال من الواقع: إدارة درجات الطلاب
package main
import "fmt"
// Store student grades in a map (look up by student ID)
type GradeBook struct {
scores map[string]int // Student ID → Score
}
func (gb *GradeBook) Set(id string, score int) {
gb.scores[id] = score
}
func (gb *GradeBook) Get(id string) (int, bool) {
score, ok := gb.scores[id]
return score, ok
}
// Use a slice to store a list of scores (for sorting or calculating the average)
func average(scores []int) float64 {
if len(scores) == 0 {
return 0
}
sum := 0
for _, s := range scores {
sum += s
}
return float64(sum) / float64(len(scores))
}
func main() {
gb := &GradeBook{scores: make(map[string]int)}
gb.Set("S001", 95)
gb.Set("S002", 82)
gb.Set("S003", 67)
if s, ok := gb.Get("S001"); ok {
fmt.Printf("S001: %d\n", s)
}
// Collect all scores and calculate the average
allScores := []int{}
for _, s := range gb.scores {
allScores = append(allScores, s)
}
fmt.Printf("Average Score: %.2f\n", average(allScores))
}
الناتج:
S001: 95
Average Score: 81.33
9. مثال كامل: نظام جرد وحدات التخزين (SKU) للتجارة الإلكترونية
اجمع بين جميع ميزات الخرائط والهياكل (structs) لإنشاء نظام كامل للاستعلام عن مخزون التجارة الإلكترونية:
// inventory_system.go
package main
import (
"encoding/json"
"fmt"
"sort"
)
// Product
type Product struct {
SKU string `json:"sku"`
Name string `json:"name"`
Price float64 `json:"price"`
Stock int `json:"stock"`
Category string `json:"category"`
Tags []string `json:"tags,omitempty"` // Tags: new/hot/discount
}
// Inventory System
type Inventory struct {
products map[string]Product // SKU → Product
}
func NewInventory() *Inventory {
return &Inventory{products: make(map[string]Product)}
}
// Add product
func (inv *Inventory) Add(p Product) {
inv.products[p.SKU] = p
}
// Query (comma-ok)
func (inv *Inventory) Find(sku string) (Product, bool) {
p, ok := inv.products[sku]
return p, ok
}
// Update stock
func (inv *Inventory) UpdateStock(sku string, delta int) error {
p, ok := inv.products[sku]
if !ok {
return fmt.Errorf("SKU %s not found", sku)
}
newStock := p.Stock + delta
if newStock < 0 {
return fmt.Errorf("insufficient stock for %s: have %d, need %d",
sku, p.Stock, -delta)
}
p.Stock = newStock
inv.products[sku] = p
return nil
}
// Sort by price (unordered map → convert to slice)
func (inv *Inventory) ListByPrice() []Product {
list := make([]Product, 0, len(inv.products))
for _, p := range inv.products {
list = append(list, p)
}
sort.Slice(list, func(i, j int) bool {
return list[i].Price < list[j].Price
})
return list
}
// Filter by category
func (inv *Inventory) FindByCategory(category string) []Product {
var result []Product
for _, p := range inv.products {
if p.Category == category {
result = append(result, p)
}
}
return result
}
func main() {
inv := NewInventory()
// Initialize 100 SKUs
categories := []string{"electronics", "clothing", "food"}
for i := 0; i < 100; i++ {
inv.Add(Product{
SKU: fmt.Sprintf("SKU-%05d", i),
Name: fmt.Sprintf("Product-%d", i),
Price: float64(i%50 + 10),
Stock: 100 - i%30,
Category: categories[i%3],
})
}
// 1. Look up a single SKU
if p, ok := inv.Find("SKU-0050"); ok {
fmt.Printf("Found: %s, price=%.2f, stock=%d\n", p.Name, p.Price, p.Stock)
}
// 2. Update inventory
if err := inv.UpdateStock("SKU-0050", -10); err == nil {
fmt.Println("Stock updated successfully")
}
// 3. Filter by category
electronics := inv.FindByCategory("electronics")
fmt.Printf("\nElectronics products: %d\n", len(electronics))
// 4. Sort by price (top 3)
sorted := inv.ListByPrice()
fmt.Println("\nTop 3 cheapest:")
for i, p := range sorted[:3] {
fmt.Printf(" %d. %s: %.2f\n", i+1, p.Name, p.Price)
}
// 5. JSON Serialization (API Response)
if p, ok := inv.Find("SKU-0050"); ok {
data, _ := json.MarshalIndent(p, "", " ")
fmt.Printf("\nJSON output:\n%s\n", data)
}
}
النتيجة المتوقعة:
Found: Product-50, price=10.00, stock=70
Stock updated successfully
Electronics products: 34
Top 3 cheapest:
1. Product-0: 10.00
2. Product-30: 10.00
3. Product-60: 10.00
JSON output:
{
"sku": "SKU-0050",
"name": "Product-50",
"price": 10,
"stock": 60,
"category": "food"
}
ListByPrice، تستخدم طريقة sort.Slice دالة مغلقة كدالة مقارنة. وتقوم الدالة المغلقة بالتقاط المتغير list واستدعائه أثناء كل عملية مقارنة.
10. مجموعات أمثلة إضافية
▶ مثال: مقارنة الأداء بين التهيئة باستخدام قيمة خريطة حرفية واستخدام الدالة make
package main
import (
"fmt"
"time"
)
func main() {
// Literal initialization
start1 := time.Now()
m1 := map[string]int{
"a": 1, "b": 2, "c": 3, "d": 4, "e": 5,
"f": 6, "g": 7, "h": 8, "i": 9, "j": 10,
}
fmt.Printf("Literals: %v, %v\n", len(m1), time.Since(start1))
// make pre-allocation
start2 := time.Now()
m2 := make(map[string]int, 10)
m2["a"] = 1
m2["b"] = 2
m2["c"] = 3
m2["d"] = 4
m2["e"] = 5
m2["f"] = 6
m2["g"] = 7
m2["h"] = 8
m2["i"] = 9
m2["j"] = 10
fmt.Printf("make: %v, %v\n", len(m2), time.Since(start2))
}
الناتج:
Literals: 10, 1.2µs
make: 10, 850ns
▶ مثال: الطرق التي تقبل قيم «struct» مقابل الطرق التي تقبل المؤشرات
package main
import "fmt"
type Counter struct {
Value int
}
// Value receiver: operates on a copy; does not affect the original
func (c Counter) IncrementValue() Counter {
c.Value++
return c
}
// Pointer receiver: directly modifies the original
func (c *Counter) IncrementPointer() {
c.Value++
}
func main() {
c := Counter{Value: 10}
// Value receiver: must use the return value
c = c.IncrementValue()
fmt.Printf("After value receiver: %d\n", c.Value) // 11
// Pointer receiver: direct modification
c.IncrementPointer()
fmt.Printf("After pointer receiver: %d\n", c.Value) // 12
}
الناتج:
After value receiver: 11
After pointer receiver: 12
▶ مثال: توضيح مخاطر التزامن في دالة map
package main
import (
"fmt"
"time"
)
func main() {
m := make(map[string]int)
// Writing goroutine
go func() {
for i := 0; i < 1000; i++ {
m["key"] = i // write operation
}
}()
// Reading goroutine
go func() {
for i := 0; i < 1000; i++ {
_ = m["key"] // read operation
}
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("Concurrent reads and writes may cause a panic (fatal error: concurrent map read and map write)")
}
الناتج (قد يتسبب في حدوث حالة ذعر أثناء وقت التشغيل):
Concurrent reads and writes may cause a panic (fatal error: concurrent map read and map write)
sync.Map أو sync.Mutex؛ وقد تم تناول هذا الموضوع بالتفصيل في الدرس 16.
❓ أسئلة شائعة
reflect.DeepEqual() لإجراء مقارنة عميقة.json:"name") أو استخدام نوع خاطئ من علامات الاقتباس (يجب استخدام علامات الاقتباس المزدوجة).sync.Map (الدرس 16) أو أضف موتكس (الدرس 16).📖 ملخص
- توفر الخريطة عمليات بحث عن القيم باستخدام المفاتيح بزمن O(1)، وهي جوهر أنواع المجموعات في لغة Go
- تميز صيغة "comma-ok" بين "القيم الصفرية" و"عدم الوجود"، مما يجعلها طريقة آمنة لاستعلامات الخرائط
- تقوم الهياكل (Structs) بتجميع البيانات باستخدام الحقول وتدعم التركيب المتداخل (كبديل عن الوراثة)
- تعد علامة «struct» عنصراً أساسياً في عملية التسلسل في JSON/ORM؛ ويُعد
json:"name"التنسيق الأكثر شيوعاً - الاختيار بين «map» و«slice»: البحث حسب المفتاح → «map»؛ الحفاظ على الترتيب / الفرز → «slice»؛ وغالبًا ما يتم الجمع بين الاثنين
- كل من «map» و«struct» يخضعان لدلالات القيمة، لكن «map» هو نوع مرجعي (يشارك البيانات الأساسية)، في حين أن «struct» هو نوع قيمة (يتم نسخه عند تمريره كمعلمة)
- يجب أن تستخدم البنيات الكبيرة أو البنيات التي تحتاج إلى تعديل المؤشرات (
func (s *Struct) Method())
📝 تمارين
-
المسألة الأساسية (الصعوبة ⭐): استخدم
map[string]intلحساب عدد مرات ظهور كل كلمة في نص ما. إذا كان المدخلات هي"the quick brown fox jumps over the lazy dog the"، فيجب أن تكون المخرجاتmap[the:3 quick:1 brown:1 ...]. -
مشكلة متقدمة (درجة الصعوبة ⭐⭐): عرّف بنية
Student(Name string, Scores []int) وقم بتنفيذ الطرق التالية: (1)Average() float64لحساب متوسط الدرجات؛ (2)Grade() stringلإرجاع درجة (A/B/C/D/F)؛ (3) اختبر البرنامج على 3 طلاب على الأقل. -
مشكلة التحدي (الصعوبة ⭐⭐⭐): قم بتنفيذ تطبيق قائمة جهات الاتصال: قم بتخزين جهات الاتصال باستخدام
map[string]Contact(حيث يتضمنContactالاسم، ورقم الهاتف، والبريد الإلكتروني، والمجموعة)؛ وقم بتنفيذ (1) الإضافة والحذف والبحث؛ (2) التصفية حسب المجموعة (العائلة/الأصدقاء/العمل)؛ (3) التصدير إلى ملف JSON (باستخدامos.WriteFile). المتطلبات: معالجة كاملة للأخطاء + علامات JSON + ما لا يقل عن 10 جهات اتصال للاختبار.