תכנון Struct בטוח לשימוש במקביל
חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 65 מתוך 107.
עכשיו, כשאתה מבין mutexes ו־WaitGroups, בוא נשלב אותם כדי לתכנן מבנים שבטוח להשתמש בהם מכמה goroutines בו־זמנית. מבנה בטוח לשימוש במקביל עוטף את מנגנוני הסנכרון בתוך המתודות שלו, כך שהקוראים לו לא צריכים לדאוג לנעילה.
התבנית פשוטה: הטמע mutex במבנה שלך ונעל אותו בכל מתודה שניגשת למצב משותף:
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}שימו לב שאפילו המתודה לקריאה בלבד Value() נועלת את המיוטקס. בלי זה, גורוטינה אחת עלולה לקרוא בזמן שגורוטינה אחרת כותבת, ולגרום למרוץ נתונים. אם קריאות נפוצות הרבה יותר מכתיבות, השתמשו ב־sync.RWMutex וקראו ל־RLock() עבור קריאות.
עיקרון עיצובי מרכזי: שומרים את המיוטקס פרטי. שימוש בשם שדה באותיות קטנות (mu) מונע מקוד חיצוני לגשת אליו ישירות. כל הסנכרון מתבצע באמצעות המתודות שלך, וכך יש לך שליטה מלאה בבטיחות של עבודה מקבילית.
במבנים עם מספר שדות, הגנו על כל השדות הקשורים באמצעות אותו mutex כדי להבטיח מצב עקבי:
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}גם balance וגם history מתעדכנים באופן אטומי: אף גורוטינה לא יכולה לראות מצב לא עקבי שבו אחד מהם השתנה והשני לא.
אתגר
קלבואו נבנה מערכת של חשבון בנק בטוחה לשימוש במקביל, שמדגימה הסתרה נכונה של סנכרון בתוך המתודות של מבנה. החשבון יטפל בבטחה בהפקדות, במשיכות ובבדיקות יתרה במקביל, בלי לחשוף למי שקוראים לו פרטים כלשהם על נעילות.
תארגנו את הקוד שלכם בשני קבצים:
account.go: הגדירו את חשבון הבנק הבטוח לשימוש במקביל שלכם.צרו מבנה
BankAccountשכוללsync.Mutexמוטמע, שדהbalance(מסוג int) ופרוסתtransactionsשמתעדת את כל הפעולות שהצליחו כמחרוזות.ממשו את המתודות האלה:
NewBankAccount(initial int) *BankAccount- יוצרת חשבון חדש עם היתרה ההתחלתית הנתונה ופרוסת עסקאות ריקהDeposit(amount int)- מוסיפה את הסכום ליתרה ומתעדת את העסקה בתור+[amount]Withdraw(amount int) bool- אם יש מספיק כסף, מחסרת את הסכום, מתעדת-[amount]ומחזירהtrue. אחרת, מחזירהfalseבלי לשנות דברBalance() int- מחזירה את היתרה הנוכחיתHistory() []string- מחזירה עותק של פרוסת העסקאות
כל מתודה שניגשת לשדות של המבנה חייבת לנעול את המנעול כדי להבטיח בטיחות בשימוש במקביל. השתמשו ב-
deferלשחרור הנעילה. השאירו את המנעול ואת כל השדות לא מיוצאים (באותיות קטנות), כדי שקוד חיצוני יהיה חייב להשתמש במתודות שלכם.main.go: עבדו את פעולות הבנק והדגימו את החשבון הבטוח לשימוש במקביל שלכם.קראו את היתרה ההתחלתית, ולאחר מכן את מספר הפעולות. עבור כל פעולה, קראו את הסוג (
deposit,withdrawאוbalance), ובהפקדה או במשיכה קראו גם את הסכום.הדפיסו את התוצאות של כל פעולה:
deposit: הדפיסוDeposited [amount], Balance: [new balance]withdraw: אם הפעולה הצליחה, הדפיסוWithdrew [amount], Balance: [new balance], ואם לא, הדפיסוWithdrawal failed: insufficient fundsbalance: הדפיסוCurrent balance: [balance]
לאחר כל הפעולות, הדפיסו את היסטוריית העסקאות, כשכל רשומה מופיעה בשורה חדשה, ורק לפני הרשומה הראשונה מופיעה הקידומת
History:.
הקלטים הבאים יסופקו:
- שורה 1: יתרה התחלתית (מספר שלם)
- שורה 2: מספר הפעולות (מספר שלם)
- השורות הבאות: עבור כל פעולה, הסוג (
deposit,withdrawאוbalance), ובהפקדה או במשיכה, הסכום בשורה הבאה
לדוגמה, בהינתן:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceהפלט שלכם אמור להיות:
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30העיקרון המרכזי כאן הוא שכל הסנכרון מוסתר בתוך המתודות של BankAccount שלכם. מי שקוראים למתודות פשוט משתמשים ב-Deposit(), ב-Withdraw() וב-Balance(), בלי לחשוב בכלל על נעילות. המבנה שלכם מטפל בבטיחות בשימוש במקביל באופן פנימי.
נסו בעצמכם
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// קרא את היתרה ההתחלתית
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// קרא את מספר הפעולות
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// צור את חשבון הבנק
account := NewBankAccount(initial)
// עבד כל פעולה
for i := 0; i < numOps; i++ {
opType, _ := reader.ReadString('\n')
opType = strings.TrimSpace(opType)
switch opType {
case "deposit":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: קרא ל-Deposit והדפס את התוצאה
// תבנית: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: קרא ל-Withdraw והדפס את התוצאה המתאימה
// אם הצליח: "Withdrew [amount], Balance: [new balance]"
// אם נכשל: "Withdrawal failed: insufficient funds"
_ = amount // הסר שורה זו בעת המימוש
case "balance":
// TODO: קרא ל-Balance והדפס את התוצאה
// תבנית: "Current balance: [balance]"
}
}
// TODO: הדפס את היסטוריית העסקאות
// לרשומה הראשונה יש להוסיף את הקידומת "History: "
// יש להציג את הרשומות הבאות בשורות חדשות, ללא קידומת
}
השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.
כל השיעורים ביחידה תכנות מונחה עצמים
1יסודות OOP ב-Go
קבצים חיצונייםסביבת העבודה והמודולים של Goחבילות וייבואשמות מיוצאים לעומת שמות לא מיוצאיםמבוא ל-OOP ב-GoStructs כמחלקותהגדרת מתודות עבור Structsמקבלי Pointer לעומת Valueאתחול Structפונקציות Constructorחזרה – מחשבון פשוט4ממשקים
מבוא לממשקיםמימוש משתמעממשק כחוזהממשק ריק (any)אימות טיפוסמתג טיפוסיםהרכבת ממשקיםממשקי Stringer ו-Errorסיכום – מחשבון צורות7כימוס
שדות מיוצאים לעומת שדות לא מיוצאיםכימוס ברמת החבילהמתודות Getter ו-Setterהסתרת מידע ב-Goסיכום – רשומות תלמידים10Generics (Go 1.18 ומעלה)
מבוא ל־Genericsפרמטרים של טיפוסיםאילוצי טיפוסיםמבנים גנרייםפתרון עוקף למתודות גנריותסיכום – אוסף גנרי2העמקה בסוגים וב-Structs
סוגים בסיסיים ומורכביםהגדרות של סוגים מותאמים אישיתתגיות StructStructs אנונימייםStructs מקונניםערכי אפס וברירות מחדלסיכום – ספר אנשי קשר5קומפוזיציה במקום ירושה
למה ב-Go אין ירושהיסודות הטמעת Structקידום מתודותהטמעת כמה Structsהטמעה לעומת אגרגציההסתרת מתודות מוטמעותסיכום – היררכיית עובדים8טיפול בשגיאות ו-OOP
ממשק errorסוגי שגיאות מותאמים אישיתעטיפת שגיאות (fmt.Errorf)שגיאות Sentinelerrors.Is() ו-errors.As()Panic, Defer ו-Recoverסיכום – מנתח קבצים11ספרייה סטנדרטית ותכנות מונחה עצמים
io.Reader ו-io.Writersort.Interfaceהממשק fmt.Stringerencoding/json עם מבניםהממשק http.Handlerסיכום – מודלים של REST API3מצביעים וזיכרון
יסודות המצביעים ב-Goמצביעים למבניםהעברה לפי ערך לעומת לפי הפניההפונקציה new()איסוף אשפה ב-Goסיכום - בונה רשימה מקושרת6פולימורפיזם ב-Go
פולימורפיזם באמצעות ממשקיםטיפוס ברווז ב-Goכללי התאמה לממשקאוספים פולימורפייםהזרקת תלויותסיכום - מעבד תשלומים9מקביליות ו-OOP
יסודות GoroutinesChannels ותקשורתChannel עם חוצץ לעומת ללא חוצץפקודת Selectsync.Mutex ו-sync.RWMutexsync.WaitGroupתכנון Struct בטוח לשימוש במקבילסיכום - מאגר עובדים12מושגים מתקדמים בתכנות מונחה עצמים
דפוס האפשרויות הפונקציונליותדפוס Builder ב-Goשרשור מתודותכינויי טיפוסים לעומת הגדרותיסודות Reflectionסקירה כללית של יצירת קודתרגלו בעצמכם: קומפיילר Go אונליין