packagemainimport("fmt""sync")typemyMapstruct{mmap[string]interface{}sync.Mutex}func(m*myMap)push(keystring,einterface{})interface{}{m.Lock()deferm.Unlock()ifval,exists:=m.m[key];exists{returnval}m.m[key]=ereturnnil}func(m*myMap)pop(keystring)interface{}{m.Lock()deferm.Unlock()ifval,exists:=m.m[key];exists{delete(m.m,key)returnval}returnnil}funcnewMap()*myMap{return&myMap{m:make(map[string]interface{}),}}funcmain(){m:=newMap()fmt.Println("Thêm lần 1:",m.push("xin_chào","thế_giới"))// nilfmt.Println("Thêm lần 2:",m.push("xin_chào","thế_giới"))// thế_giớifmt.Println("Lấy ra lần 1:",m.pop("xin_chào"))// thế_giớifmt.Println("Lấy ra lần 2:",m.pop("xin_chào"))// nil}
💡 Giải thích quan trọng:
Chúng ta phải sử dụng con trỏ (*myMap) trong các phương thức vì cấu trúc sync.Mutex không thể sao chép. Go không hỗ trợ các kỹ thuật như copy constructor hay overload toán tử gán như C++, do đó sử dụng con trỏ là giải pháp tối ưu nhờ vào cơ chế garbage collection tự động.
Phiên bản sử dụng Channel (Goroutine-based)
Một cách tiếp cận hiện đại hơn là tận dụng mô hình CSP (Communicating Sequential Processes) của Go thông qua channel:
packagemainimport"fmt"typemyMapinterface{push(keystring,einterface{})interface{}pop(keystring)interface{}}typemapRequeststruct{keystringvalueinterface{}}typemapServicestruct{pushReqchan*mapRequestpushReschaninterface{}popReqchanstringpopReschaninterface{}}func(s*mapService)push(keystring,einterface{})interface{}{s.pushReq<-&mapRequest{key:key,value:e}return<-s.pushRes}func(s*mapService)pop(keystring)interface{}{s.popReq<-keyreturn<-s.popRes}funcnewMap()myMap{service:=&mapService{pushReq:make(chan*mapRequest),pushRes:make(chaninterface{}),popReq:make(chanstring),popRes:make(chaninterface{}),}gofunc(){internalMap:=make(map[string]interface{})for{select{casereq:=<-service.pushReq:ifval,exists:=internalMap[req.key];exists{service.pushRes<-val}else{internalMap[req.key]=req.valueservice.pushRes<-nil}casekey:=<-service.popReq:ifval,exists:=internalMap[key];exists{delete(internalMap,key)service.popRes<-val}else{service.popRes<-nil}}}}()returnservice}funcmain(){m:=newMap()fmt.Println("Thêm lần 1:",m.push("xin_chào","thế_giới"))// nilfmt.Println("Thêm lần 2:",m.push("xin_chào","thế_giới"))// thế_giớifmt.Println("Lấy ra lần 1:",m.pop("xin_chào"))// thế_giớifmt.Println("Lấy ra lần 2:",m.pop("xin_chào"))// nil}
So sánh hai cách tiếp cận
Tiêu chí
Dùng Mutex
Dùng Channel
Độ phức tạp
Đơn giản, dễ hiểu
Phức tạp hơn, cần tư duy bất đồng bộ
Hiệu năng
Tốt cho đa số trường hợp
Có thể tối ưu hơn với I/O密集型
Khả năng mở rộng
Hạn chế khi logic phức tạp
Dễ mở rộng với nhiều loại request
Phong cách lập trình
Truyền thống, mệnh lệnh
Hiện đại, hướng sự kiện
💡 Mẹo chuyên nghiệp:
Mô hình channel rất mạnh mẽ nhưng dễ dẫn đến deadlock nếu không xử lý cẩn thận. Khi xây dựng service dựa trên channel, hãy luôn đảm bảo có cơ chế timeout hoặc context cancellation để tránh treo chương trình.
Kết luận
Go cung cấp cho chúng ta hai lựa chọn thú vị:
Cách tiếp cận truyền thống với sync.Mutex – phù hợp cho các ứng dụng đơn giản, dễ bảo trì
Cách tiếp cận hiện đại với goroutine + channel – mở ra tiềm năng xây dựng các hệ thống phản ứng (reactive systems)
Tùy theo ngữ cảnh sử dụng, bạn có thể chọn giải pháp phù hợp. Trong thực tế, nhiều dự án kết hợp cả hai cách để tối ưu hiệu năng và khả năng bảo trì.