Desain Struct yang Aman untuk Thread
Termasuk bagian Pemrograman Berorientasi Objek dalam perjalanan GO di Coddy. Pelajaran 65 dari 107.
Sekarang setelah kamu memahami mutex dan WaitGroup, mari gabungkan keduanya untuk merancang struct yang aman digunakan secara bersamaan oleh beberapa goroutine. Struct yang aman untuk thread mengenkapsulasi sinkronisasi di dalam method-nya, sehingga pemanggil tidak perlu khawatir tentang penguncian.
Polanya sederhana: sematkan mutex di struct-mu dan kunci mutex itu di setiap method yang mengakses status bersama:
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
}Perhatikan bahwa bahkan method Value() yang hanya bisa membaca tetap mengunci mutex. Tanpa ini, satu goroutine mungkin membaca saat goroutine lain menulis, sehingga menyebabkan data race. Jika pembacaan jauh lebih sering daripada penulisan, gunakan sync.RWMutex sebagai gantinya dan panggil RLock() untuk membaca.
Prinsip desain utama: jaga mutex tetap privat. Dengan menggunakan nama field huruf kecil (mu), kamu mencegah kode eksternal mengaksesnya secara langsung. Semua sinkronisasi terjadi melalui method-mu, sehingga kamu memiliki kendali penuh atas keamanan thread.
Untuk struct dengan beberapa field, lindungi semua field terkait dengan mutex yang sama untuk memastikan status tetap konsisten:
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 dan history diperbarui secara atomik: tidak ada goroutine yang dapat melihat keadaan yang tidak konsisten, yaitu saat salah satunya berubah tetapi yang lain tidak.
Tantangan
MudahMari kita buat sistem rekening bank yang aman untuk thread, yang menunjukkan enkapsulasi sinkronisasi yang tepat di dalam method struct. Rekeningmu akan menangani setoran, penarikan, dan pemeriksaan saldo secara bersamaan dengan aman tanpa mengungkap detail penguncian kepada pemanggil.
Kamu akan menyusun kodemu dalam dua file:
account.go: Definisikan rekening bank yang aman untuk thread.Buat struct
BankAccountdengansync.Mutexyang disematkan, fieldbalance(int), dan slicetransactionsyang mencatat semua operasi berhasil sebagai string.Implementasikan method berikut:
NewBankAccount(initial int) *BankAccount: Membuat rekening baru dengan saldo awal yang diberikan dan slice transactions yang kosongDeposit(amount int): Menambahkan jumlah tersebut ke saldo dan mencatat transaksi sebagai+[amount]Withdraw(amount int) bool: Jika dana mencukupi, mengurangi jumlah tersebut, mencatat-[amount], dan mengembalikantrue. Jika tidak, mengembalikanfalsetanpa mengubah apa punBalance() int: Mengembalikan saldo saat iniHistory() []string: Mengembalikan salinan slice transactions
Setiap method yang mengakses field struct harus mengunci mutex untuk memastikan keamanan thread. Gunakan
deferuntuk membuka kunci. Biarkan mutex dan semua field tidak diekspor (huruf kecil) agar kode eksternal harus menggunakan method-mu.main.go: Proses operasi perbankan dan tunjukkan penggunaan rekening yang aman untuk thread.Baca saldo awal, lalu jumlah operasi. Untuk setiap operasi, baca jenisnya (
deposit,withdraw, ataubalance), lalu untuk deposit/withdraw, baca jumlahnya.Cetak hasil untuk setiap operasi:
deposit: CetakDeposited [amount], Balance: [new balance]withdraw: CetakWithdrew [amount], Balance: [new balance]jika berhasil, atauWithdrawal failed: insufficient fundsjika tidakbalance: CetakCurrent balance: [balance]
Setelah semua operasi, cetak riwayat transaksi dengan setiap entri pada baris baru. Hanya awali entri pertama dengan
History:.
Input berikut akan diberikan:
- Baris 1: Saldo awal (bilangan bulat)
- Baris 2: Jumlah operasi (bilangan bulat)
- Baris berikutnya: Untuk setiap operasi, jenisnya (
deposit,withdraw, ataubalance), lalu untuk deposit/withdraw, jumlahnya pada baris berikutnya
Contohnya, jika diberikan:
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceOutput-mu seharusnya:
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30Prinsip utamanya adalah semua sinkronisasi disembunyikan di dalam method BankAccount-mu. Pemanggil cukup menggunakan Deposit(), Withdraw(), dan Balance() tanpa perlu memikirkan lock. Struct-mu menangani keamanan thread secara internal.
Coba sendiri
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Baca saldo awal
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// Baca jumlah operasi
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// Buat rekening bank
account := NewBankAccount(initial)
// Proses setiap operasi
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: Panggil Deposit dan cetak hasilnya
// Format: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Panggil Withdraw dan cetak hasil yang sesuai
// Jika berhasil: "Withdrew [amount], Balance: [new balance]"
// Jika gagal: "Withdrawal failed: insufficient funds"
_ = amount // Hapus baris ini saat kamu mengimplementasikannya
case "balance":
// TODO: Panggil Balance dan cetak hasilnya
// Format: "Current balance: [balance]"
}
}
// TODO: Cetak riwayat transaksi
// Awali entri pertama dengan "History: "
// Tampilkan entri berikutnya pada baris baru tanpa awalan
}
Pelajaran ini berisi kuis singkat. Mulai pelajarannya untuk menjawab kuis dan melacak progresmu.
Semua pelajaran di Pemrograman Berorientasi Objek
1Dasar-Dasar OOP di Go
File EksternalWorkspace Go dan modulePaket dan ImporNama yang Diekspor dan Tidak DieksporPengantar OOP di Gostruct sebagai classMendefinisikan method pada structPointer receiver vs value receiverInisialisasi structFungsi KonstruktorUlasan: Kalkulator Sederhana4Interface
Pengenalan InterfaceImplementasi ImplisitInterface sebagai KontrakInterface Kosong (any)Type AssertionType SwitchKomposisi InterfaceInterface Stringer & ErrorUlasan: Kalkulator Bentuk7Enkapsulasi
Field yang Diekspor vs Tidak DieksporEnkapsulasi Tingkat PackageMethod Getter dan SetterPenyembunyian Informasi di GoRingkasan: Data Siswa10Generics (Go 1.18+)
Pengantar GenericsParameter TipeBatasan TipeStruct GenerikCara Alternatif untuk Method GenerikRingkasan Koleksi Generik2Mendalami Tipe dan Struct
Tipe Dasar dan KompositDefinisi Tipe KustomTag StructStruct AnonimStruct BersarangNilai Nol dan Nilai BawaanRangkuman: Buku Kontak5Komposisi daripada Pewarisan
Mengapa Go Tidak Memiliki PewarisanDasar Penyematan StructPromosi methodMenyematkan Beberapa StructPenyematan vs AgregasiMembayangi method yang DisematkanRangkuman: Hierarki Karyawan8Penanganan Error & OOP
Interface errorTipe Error KustomPembungkusan Error (fmt.Errorf)Error Sentinelerrors.Is() dan errors.As()Panic, Defer, dan RecoverRangkuman: Parser File3Pointer dan Memori
Dasar-Dasar Pointer di GoPointer ke StructPass by Value vs ReferenceFungsi new()Garbage Collection di GoRekap: Pembuat Linked List6Polimorfisme di Go
Polimorfisme melalui InterfaceDuck Typing di GoAturan Pemenuhan InterfaceKoleksi PolimorfikInjeksi DependensiRangkuman: Pemroses Pembayaran9Konkruensi & OOP
Dasar-dasar GoroutineChannel & KomunikasiChannel Ber-buffer vs Tanpa BufferPernyataan selectsync.Mutex & sync.RWMutexsync.WaitGroupDesain Struct yang Aman untuk ThreadRingkasan: Worker PoolLatihan sendiri: Compiler Go online