Golang中NetPoll機(jī)制的實(shí)現(xiàn)
Linux 網(wǎng)絡(luò)IO
Linux 的阻塞網(wǎng)絡(luò) I/O (輸入/輸出) 是指在進(jìn)行網(wǎng)絡(luò)操作(如 read() 或 write())時(shí),如果操作無(wú)法立即完成,調(diào)用線程將被操作系統(tǒng)“阻塞”,直到操作成功或失敗才返回。它屬于同步 I/O 模型的一種,與之相對(duì)的是非阻塞 I/O。
int listenfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 綁定到所有可用接口 server_addr.sin_port = htons(8080); // 綁定到 8080 端口 bind(listenfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); listen(listenfd, 5); // 允許 5 個(gè)待處理的連接 // 阻塞等待 struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int connectfd = accept(listenfd, (struct sockaddr *)&client_addr, &client_addr_len);
Linux 非阻塞IO Select、Poll、Epoll
核心機(jī)制:這些系統(tǒng)調(diào)用讓用戶進(jìn)程在調(diào)用前,能預(yù)先指定一系列要監(jiān)控的文件描述符。當(dāng)一個(gè)或多個(gè)文件描述符準(zhǔn)備好進(jìn)行讀寫(xiě)操作時(shí),系統(tǒng)會(huì)通知用戶進(jìn)程。
輪詢與事件驅(qū)動(dòng):
select 和 poll: 它們采用輪詢方式,每次都檢查所有傳入的文件描述符集合來(lái)判斷哪些是活躍的。
(1)select==>時(shí)間復(fù)雜度O(n)
它僅僅知道了,有I/O事件發(fā)生了,卻并不知道是哪那幾個(gè)流(可能有一個(gè),多個(gè),甚至全部),我們只能無(wú)差別輪詢所有流,找出能讀出數(shù)據(jù),或者寫(xiě)入數(shù)據(jù)的流,對(duì)他們進(jìn)行操作。所以select具有O(n)的無(wú)差別輪詢復(fù)雜度,同時(shí)處理的流越多,無(wú)差別輪詢時(shí)間就越長(zhǎng)。
(2)poll==>時(shí)間復(fù)雜度O(n)
poll本質(zhì)上和select沒(méi)有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個(gè)fd對(duì)應(yīng)的設(shè)備狀態(tài), 但是它沒(méi)有最大連接數(shù)的限制,原因是它是基于鏈表來(lái)存儲(chǔ)的.
(3)epoll==>時(shí)間復(fù)雜度O(1)
epoll: 它采用事件驅(qū)動(dòng)模型,通過(guò)內(nèi)核的回調(diào)機(jī)制,只在需要時(shí)通知用戶進(jìn)程,效率更高,尤其是在處理大量并發(fā)連接時(shí)。epoll 機(jī)制通過(guò)在內(nèi)核中維護(hù)一個(gè)就緒列表,當(dāng)數(shù)據(jù)到達(dá)時(shí),將對(duì)應(yīng)的節(jié)點(diǎn)加入就緒列表,然后喚醒等待的用戶進(jìn)程,從而避免了不必要的輪詢。
Golang 中Epoll 應(yīng)用
一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)IO
// 啟動(dòng) tcp server 代碼示例
func main() {
/*
- 創(chuàng)建 tcp 端口監(jiān)聽(tīng)器
- 創(chuàng)建 socket fd,bind、accept
- 創(chuàng)建 epoll 事件表(epoll_create)
- socket fd 注冊(cè)到 epoll 事件表(epoll_ctl:add)
*/
l, _ := net.Listen("tcp", ":8080")
// eventloop reactor 模型
for {
/*
- 等待 tcp 連接到達(dá)
- loop + 非阻塞模式調(diào)用 accept
- 若未就緒,則通過(guò) gopark 進(jìn)行阻塞
- 等待 netpoller 輪詢喚醒
- 檢查是否有 io 事件就緒(epoll_wait——nonblock)
- 若發(fā)現(xiàn)事件就緒 通過(guò) goready 喚醒 g
- accept 獲取 conn fd 后注冊(cè)到 epoll 事件表(epoll_ctl:add)
- 返回 conn
*/
conn, _ := l.Accept()
// goroutine per conn
go serve(conn)
}
}
// 處理一筆到來(lái)的 tcp 連接
func serve(conn net.Conn) {
/*
- 關(guān)閉 conn
- 從 epoll 事件表中移除該 fd(epoll_ctl:remove)
- 銷毀該 fd
*/
defer conn.Close()
var buf []byte
/*
- 讀取連接中的數(shù)據(jù)
- loop + 非阻塞模式調(diào)用 recv (read)
- 若未就緒,則通過(guò) gopark 進(jìn)行阻塞
- 等待 netpoller 輪詢喚醒
- 檢查是否有 io 事件就緒(epoll_wait——nonblock)
- 若發(fā)現(xiàn)事件就緒 通過(guò) goready 喚醒 g
*/
_, _ = conn.Read(buf)
/*
- 向連接中寫(xiě)入數(shù)據(jù)
- loop + 非阻塞模式調(diào)用 writev (write)
- 若未就緒,則通過(guò) gopark 進(jìn)行阻塞
- 等待 netpoller 輪詢喚醒
- 檢查是否有 io 事件就緒(epoll_wait:nonblock)
- 若發(fā)現(xiàn)事件就緒 通過(guò) goready 喚醒 g
*/
_, _ = conn.Write(buf)
}
1、調(diào)用Listen方法,通過(guò)epoll_create 初始化事件表, 創(chuàng)建 socket fd, 通過(guò)epoll_ctl 將socket fd注冊(cè)到Epoll事件表, 監(jiān)聽(tīng)就緒事件,等待遠(yuǎn)端連接。如果有遠(yuǎn)端連接,則會(huì)在內(nèi)核空間執(zhí)行回調(diào)函數(shù),將socket fd放入就緒列表中
// 在 epoll 事件表中注冊(cè)監(jiān)聽(tīng) r 的讀就緒事件
ev := epollevent{
events: _EPOLLIN,
}
2、調(diào)用Accept方法,非阻塞調(diào)用Epoll Wait,獲取當(dāng)前監(jiān)聽(tīng)的fd是否有就緒的socket fd
2.1、如果沒(méi)有則將當(dāng)前的goroutine阻塞,并掛載在socket fd 對(duì)應(yīng)的polldesc的rg等待鏈表中,等待環(huán)境
2.2、如果有,則獲取conn fd返回給用戶程序
// Accept wraps the accept network call.
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
// ...
for {
// 以nonblock 模式發(fā)起一次 syscall accept 嘗試接收到來(lái)的 conn
s, rsa, errcall, err := accept(fd.Sysfd)
// 接收conn成功,直接返回結(jié)果
if err == nil {
return s, rsa, "", err
}
switch err {
// 中斷類錯(cuò)誤直接忽略
case syscall.EINTR:
continue
// 當(dāng)前未有到達(dá)的conn
case syscall.EAGAIN:
// 走入 poll_wait 流程,并標(biāo)識(shí)關(guān)心的是 socket fd 的讀就緒事件
// (當(dāng)conn 到達(dá)時(shí),表現(xiàn)為 socket fd 可讀)
if fd.pd.pollable() {
// 倘若讀操作未就緒,當(dāng)前g 會(huì) park 阻塞在該方法內(nèi)部,直到因超時(shí)或者事件就緒而被 netpoll ready 喚醒
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
// ...
}
// ...
}
}
// 指定 mode 為 r 標(biāo)識(shí)等待的是讀就緒事件,然后走入更底層的 poll_wait 流程
func (pd *pollDesc) waitRead(isFile bool) error {
return pd.wait('r', isFile)
}
3、調(diào)用Read、Write方法。非阻塞調(diào)用Epoll Wait,關(guān)心conn fd上是否有讀就緒事件或者寫(xiě)就緒事件
3.1、如果 conn fd 下讀操作尚未就緒(尚無(wú)數(shù)據(jù)到達(dá)),則會(huì)執(zhí)行 poll wait 將當(dāng)前 g 阻塞并掛載到 conn fd 對(duì)應(yīng) pollDesc 的 rg 中
3.2、如果 conn fd 下寫(xiě)操作尚未就緒(緩沖區(qū)空間不足),則會(huì)執(zhí)行 poll wait 將當(dāng)前 g 阻塞并掛載到 conn fd 對(duì)應(yīng) pollDesc 的wg中
// Read implements io.Reader.
func (fd *FD) Read(p []byte) (int, error) {
// ...
for {
// 以非阻塞模式執(zhí)行一次syscall read 操作
n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
if err != nil {
n = 0
// 走入 poll_wait 流程,并標(biāo)識(shí)關(guān)心的是該 fd 的讀就緒事件
if err == syscall.EAGAIN && fd.pd.pollable() {
// 倘若讀操作未就緒,當(dāng)前g 會(huì) park 阻塞在該方法內(nèi)部,直到因超時(shí)或者事件就緒而被 netpoll ready 喚醒
if err = fd.pd.waitRead(fd.isFile); err == nil {
continue
}
}
}
err = fd.eofError(n, err)
return n, err
}
}
4、小結(jié)
可以看到,一個(gè)連接的數(shù)據(jù)讀寫(xiě),如果條件不滿足,goroutine都會(huì)掛起并掛載到pollDesc的rg、wg中,但此時(shí)用戶程序主線程不會(huì)阻塞,而是等待底層數(shù)據(jù)到達(dá)后,再進(jìn)行處理。
NetPoll的調(diào)度
由上可知,當(dāng) g 發(fā)現(xiàn)關(guān)心的 io 事件未就緒時(shí),會(huì)通過(guò) gopark 操作將自身陷入阻塞,并且將 g 掛載在 pollDesc 的 rg/wg 中, 而本小節(jié)介紹的 net_poll 流程就負(fù)責(zé)輪詢獲取已就緒 pollDesc 對(duì)應(yīng)的 g,將其返回給上游的 gmp 調(diào)度系統(tǒng),對(duì)其進(jìn)行喚醒和調(diào)度.
Golang GMP模型觸發(fā)netpoll時(shí)機(jī)
1、M在尋找可用的Goroutine時(shí),在本地和全局隊(duì)列上沒(méi)有找到,會(huì)調(diào)用netpoll處理網(wǎng)絡(luò)IO
// gmp 核心調(diào)度流程:g0 為當(dāng)前 p 找到下一個(gè)調(diào)度的 g
/*
pick g 的核心邏輯:
1)每調(diào)度 61 次,需要專門嘗試處理一次全局隊(duì)列(防止饑餓)
2)嘗試從本地隊(duì)列中獲取 g
3)嘗試從全局隊(duì)列中獲取 g
4)以【非阻塞模式】調(diào)度 netpoll 流程,獲取所有需要喚醒的 g 進(jìn)行喚醒,并獲取其中的首個(gè)g
5)從其他 p 中竊取一半的 g 填充到本地隊(duì)列
6)仍找不到合適的 g,則協(xié)助 gc
7)以【阻塞或者超時(shí)】模式,調(diào)度netpoll 流程(全局僅有一個(gè) p 能走入此分支)
8)當(dāng)前m 添加到全局隊(duì)列的空閑隊(duì)列中,停止當(dāng)前 m
*/
func findRunnable() (gp *g, inheritTime, tryWakeP bool) {
// ..
/*
同時(shí)滿足下述三個(gè)條件,發(fā)起一次【非阻塞模式】的 netpoll 流程:
- epoll事件表初始化過(guò)
- 有 g 在等待io 就緒事件
- 沒(méi)有空閑 p 在以【阻塞或超時(shí)】模式發(fā)起 netpoll 流程
*/
if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 {
// 以非阻塞模式發(fā)起一輪 netpoll,如果有 g 需要喚醒,一一喚醒之,并返回首個(gè) g 給上層進(jìn)行調(diào)度
if list := netpoll(0); !list.empty() { // non-blocking
// 獲取就緒 g 隊(duì)列中的首個(gè) g
gp := list.pop()
// 將就緒 g 隊(duì)列中其余 g 一一置為就緒態(tài),并添加到全局隊(duì)列
injectglist(&list)
// 把首個(gè)g 也置為就緒態(tài)
casgstatus(gp, _Gwaiting, _Grunnable)
// ...
//返回 g 給當(dāng)前 p進(jìn)行調(diào)度
return gp, false, false
}
}
// ...
/*
同時(shí)滿足下述三個(gè)條件,發(fā)起一次【阻塞或超時(shí)模式】的 netpoll 流程:
- epoll事件表初始化過(guò)
- 有 g 在等待io 就緒事件
- 沒(méi)有空閑 p 在以【阻塞或超時(shí)】模式發(fā)起 netpoll 流程
*/
if netpollinited() && (atomic.Load(&netpollWaiters) > 0 || pollUntil != 0) && atomic.Xchg64(&sched.lastpoll, 0) != 0 {
// 默認(rèn)為阻塞模式
delay := int64(-1)
// 存在定時(shí)時(shí)間,則設(shè)為超時(shí)模式
if pollUntil != 0 {
delay = pollUntil - now
// ...
}
// 以【阻塞或超時(shí)模式】發(fā)起一輪 netpoll
list := netpoll(delay) // block until new work is available
}
// ...
}
2、Sysmon 定時(shí)會(huì)調(diào)用netpoll處理網(wǎng)絡(luò)IO
// The main goroutine.
func main() {
// ...
// 新建一個(gè) m,直接運(yùn)行 sysmon 函數(shù)
systemstack(func() {
newm(sysmon, nil, -1)
})
// ...
}
// 全局唯一監(jiān)控線程的執(zhí)行函數(shù)
func sysmon() {
// ...
for {
// ...
/*
同時(shí)滿足下述三個(gè)條件,發(fā)起一次【非阻塞模式】的 netpoll 流程:
- epoll事件表初始化過(guò)
- 沒(méi)有空閑 p 在以【阻塞或超時(shí)】模式發(fā)起 netpoll 流程
- 距離上一次發(fā)起 netpoll 流程的時(shí)間間隔已超過(guò) 10 ms
*/
lastpoll := int64(atomic.Load64(&sched.lastpoll))
if netpollinited() && lastpoll != 0 && lastpoll+10*1000*1000 < now {
// 以非阻塞模式發(fā)起 netpoll
list := netpoll(0) // non-blocking - returns list of goroutines
// 獲取到的 g 置為就緒態(tài)并添加到全局隊(duì)列中
if !list.empty() {
// ...
injectglist(&list)
// ...
}
}
// ...
}
}
3、gc并發(fā)標(biāo)記的流程
func startTheWorldWithSema(emitTraceEvent bool) int64 {
// 斷言世界已停止
assertWorldStopped()
// ...
// 如果 epoll 事件表初始化過(guò),則以非阻塞模式執(zhí)行一次 netpoll
if netpollinited() {
// 所有取得的 g 置為就緒態(tài)并添加到全局隊(duì)列
list := netpoll(0) // non-blocking
injectglist(&list)
}
// ...
}
當(dāng)上述條件成立時(shí),Netpoll執(zhí)行 epoll_wait 操作,獲取就緒的 io 事件 list. 一輪最多獲取 128 個(gè),根據(jù)就緒事件類型,將 mode 分為 w(寫(xiě)就緒事件)和 r(讀就緒事件)。獲取 pollDesc 實(shí)例中 rg或者wg中的 g 實(shí)例,一并返回GMP進(jìn)行調(diào)度

// netpoll checks for ready network connections.
// Returns list of goroutines that become runnable.
/*
- netpoll 流程用于輪詢檢查是否有就緒的 io 事件
- 如果有就緒 io 事件,還需要檢查是否有 pollDesc 中的 g 關(guān)心該事件
- 找到所有關(guān)心該就緒 io 事件的 g,添加到 list 中返回給上游進(jìn)行 goready 喚醒
*/
func netpoll(delay int64) gList {
/*
根據(jù)傳入的 delay 參數(shù),決定調(diào)用 epoll_wait 的模式
- delay < 0:設(shè)為 -1 阻塞模式(在 gmp 調(diào)度流程中,如果某個(gè) p 遲遲獲取不到可執(zhí)行的 g 時(shí),會(huì)通過(guò)該模式,使得 thread 陷入阻塞態(tài),但該情況全局最多僅有一例)
- delay = 0:設(shè)為 0 非阻塞模式(通常情況下為此模式,包括 gmp 常規(guī)調(diào)度流程、gc 以及全局監(jiān)控線程 sysmon 都是以此模式觸發(fā)的 netpoll 流程)
- delay > 0:設(shè)為超時(shí)模式(在 gmp 調(diào)度流程中,如果某個(gè) p 遲遲獲取不到可執(zhí)行的 g 時(shí),并且通過(guò) timer 啟動(dòng)了定時(shí)任務(wù)時(shí),會(huì)令 thread 以超時(shí)模式執(zhí)行 epoll_wait 操作)
*/
var waitms int32
if delay < 0 {
waitms = -1
} else if delay == 0 {
waitms = 0
// 針對(duì) delay 時(shí)長(zhǎng)取整
} else if delay < 1e6 {
waitms = 1
} else if delay < 1e15 {
waitms = int32(delay / 1e6)
} else {
// 1e9 ms == ~11.5 days.
waitms = 1e9
}
// 一次最多接收 128 個(gè) io 就緒事件
var events [128]epollevent
retry:
// 以指定模式,調(diào)用 epoll_wait 指令
n := epollwait(epfd, &events[0], int32(len(events)), waitms)
// ...
// 遍歷就緒的每個(gè) io 事件
var toRun gList
for i := int32(0); i < n; i++ {
ev := &events[i]
if ev.events == 0 {
continue
}
// pipe 接收端的信號(hào)量處理
if *(**uintptr)(unsafe.Pointer(&ev.data)) == &netpollBreakRd {
// ...
}
/*
根據(jù) io 事件類型,標(biāo)識(shí)出 mode:
- EPOLL_IN -> r;
- EPOLL_OUT -> w;
- 錯(cuò)誤或者中斷事件 -> r & w;
*/
var mode int32
if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'r'
}
if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
mode += 'w'
}
// 根據(jù) epollevent.data 獲取到監(jiān)聽(tīng)了該事件的 pollDesc 實(shí)例
if mode != 0 {
pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
// ...
// 嘗試針對(duì)對(duì)應(yīng) pollDesc 進(jìn)行喚醒操作
netpollready(&toRun, pd, mode)
}
}
return toRun
}
/*
epollwait 操作:
- epfd:epoll 事件表 fd 句柄
- ev:用于承載就緒 epoll event 的容器
- nev:ev 的容量
- timeout:
- -1:阻塞模式
- 0:非阻塞模式:
- >0:超時(shí)模式. 單位 ms
- 返回值 int32:就緒的 event 數(shù)量
*/
func epollwait(epfd int32, ev *epollevent, nev, timeout int32) int32
// It declares that the fd associated with pd is ready for I/O.
// The toRun argument is used to build a list of goroutines to return
// from netpoll. The mode argument is 'r', 'w', or 'r'+'w' to indicate
/*
根據(jù) pd 以及 mode 標(biāo)識(shí)的 io 就緒事件,獲取需要進(jìn)行 ready 喚醒的 g list
對(duì)應(yīng) g 會(huì)存儲(chǔ)到 toRun 這個(gè) list 容器當(dāng)中
*/
func netpollready(toRun *gList, pd *pollDesc, mode int32) {
var rg, wg *g
if mode == 'r' || mode == 'r'+'w' {
// 倘若到達(dá)事件包含讀就緒,嘗試獲取需要 ready 喚醒的 g
rg = netpollunblock(pd, 'r', true)
}
if mode == 'w' || mode == 'r'+'w' {
// 倘若到達(dá)事件包含寫(xiě)就緒,嘗試獲取需要 ready 喚醒的 g
wg = netpollunblock(pd, 'w', true)
}
// 找到需要喚醒的 g,添加到 glist 中返回給上層
if rg != nil {
toRun.push(rg)
}
if wg != nil {
toRun.push(wg)
}
}
/*
根據(jù)指定的就緒io 事件類型以及 pollDesc,判斷是否有 g 需要被喚醒. 若返回結(jié)果非空,則為需要喚醒的 g
*/
func netpollunblock(pd *pollDesc, mode int32, ioready bool) *g {
// 根據(jù) io 事件類型,獲取 pollDesc 中對(duì)應(yīng)的狀態(tài)標(biāo)識(shí)器
gpp := &pd.rg
if mode == 'w' {
gpp = &pd.wg
}
for {
// 從 gpp 中取出值,此時(shí)該值應(yīng)該為調(diào)用過(guò) park 操作的 g
old := gpp.Load()
// ...
if ioready {
new = pdReady
}
// 通過(guò) cas 操作,將 gpp 值由 g 置換成 pdReady
if gpp.CompareAndSwap(old, new) {
// 返回需要喚醒的 g
return (*g)(unsafe.Pointer(old))
}
}
}
到此這篇關(guān)于Golang中NetPoll機(jī)制的實(shí)現(xiàn) 的文章就介紹到這了,更多相關(guān)Golang NetPoll 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一文掌握Go語(yǔ)言并發(fā)編程必備的Mutex互斥鎖
Go 語(yǔ)言提供了 sync 包,其中包括 Mutex 互斥鎖、RWMutex 讀寫(xiě)鎖等同步機(jī)制,本篇博客將著重介紹 Mutex 互斥鎖的基本原理,需要的可以參考一下2023-04-04
Go語(yǔ)言使用組合的思想實(shí)現(xiàn)繼承
這篇文章主要為大家詳細(xì)介紹了在 Go 里面如何使用組合的思想實(shí)現(xiàn)“繼承”,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Go語(yǔ)言有一定的幫助,需要的可以了解一下2022-12-12
Goland項(xiàng)目使用gomod配置的詳細(xì)步驟
Goland是一個(gè)用于Go語(yǔ)言開(kāi)發(fā)的IDE,Goland的項(xiàng)目結(jié)構(gòu)與Go語(yǔ)言的項(xiàng)目結(jié)構(gòu)相似,下面這篇文章主要給大家介紹了關(guān)于Goland項(xiàng)目使用gomod配置的詳細(xì)步驟,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04
Go語(yǔ)言利用ffmpeg轉(zhuǎn)hls實(shí)現(xiàn)簡(jiǎn)單視頻直播
這篇文章主要為大家介紹了Go語(yǔ)言利用ffmpeg轉(zhuǎn)hls實(shí)現(xiàn)簡(jiǎn)單視頻直播,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04
Go-RESTful實(shí)現(xiàn)下載功能思路詳解
這篇文章主要介紹了Go-RESTful實(shí)現(xiàn)下載功能,文件下載包括文件系統(tǒng)IO和網(wǎng)絡(luò)IO,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-10-10
輕松入門:使用Golang開(kāi)發(fā)跨平臺(tái)GUI應(yīng)用
Golang是一種強(qiáng)大的編程語(yǔ)言,它的并發(fā)性和高性能使其成為開(kāi)發(fā)GUI桌面應(yīng)用的理想選擇,Golang提供了豐富的標(biāo)準(zhǔn)庫(kù)和第三方庫(kù),可以輕松地創(chuàng)建跨平臺(tái)的GUI應(yīng)用程序,通過(guò)使用Golang的GUI庫(kù),開(kāi)發(fā)人員可以快速構(gòu)建具有豐富用戶界面和交互功能的應(yīng)用程序,需要的朋友可以參考下2023-10-10
Go設(shè)計(jì)模式之訪問(wèn)者模式講解和代碼示例
訪問(wèn)者是一種行為設(shè)計(jì)模式, 允許你在不修改已有代碼的情況下向已有類層次結(jié)構(gòu)中增加新的行為,本文將通過(guò)代碼示例給大家詳細(xì)的介紹一下Go設(shè)計(jì)模式之訪問(wèn)者模式,需要的朋友可以參考下2023-08-08

