go語(yǔ)言?http模型reactor示例詳解
前面說(shuō)了go自帶的原生netpoll模型,大致的流程就是每一個(gè)新的連接都會(huì)開(kāi)啟一個(gè)goroutine去處理,這樣的處理的過(guò)程簡(jiǎn)單,高效,充分利用了go的底層的能力。
但是這里有幾個(gè)問(wèn)題,對(duì)于accept的時(shí)候,是否可以多個(gè)線(xiàn)程去accept,這樣的話(huà)就不用每次有一個(gè)連接就開(kāi)啟一個(gè)線(xiàn)程。
同時(shí)看過(guò)accept的源碼都知道,只會(huì)一個(gè)線(xiàn)程去accpet連接,因?yàn)檫@個(gè)套接字在創(chuàng)建的時(shí)候就被設(shè)置成了非阻塞,所以會(huì)變goruntime調(diào)用gopark掛起。
開(kāi)啟端口復(fù)用也就是SO_REUSEPORT功能。這樣一方面可以避免驚群效應(yīng)
接下來(lái)看一下一個(gè)demo,這里使用的gnet框架,github地址。
示例
接下來(lái)看一段基于reactor的示例。這里運(yùn)行通過(guò) go run main.go.
然后curl -i 127.0.0.1:8080.效果如下,也是返回了我們期望的結(jié)果
package main import ( "flag" "fmt" "log" "strconv" "strings" "time" "unsafe" "learn/http/gnet" ) var res string type request struct { proto, method string path, query string head, body string remoteAddr string } type httpServer struct { *gnet.EventServer } var ( errMsg = "Internal Server Error" errMsgBytes = []byte(errMsg) ) type httpCodec struct { req request } func (hc *httpCodec) Encode(c gnet.Conn, buf []byte) (out []byte, err error) { if c.Context() == nil { return buf, nil } return appendResp(out, "500 Error", "", errMsg+"\n"), nil } func (hc *httpCodec) Decode(c gnet.Conn) (out []byte, err error) { buf := c.Read() c.ResetBuffer() // process the pipeline var leftover []byte pipeline: leftover, err = parseReq(buf, &hc.req) // bad thing happened if err != nil { c.SetContext(err) return nil, err } else if len(leftover) == len(buf) { // request not ready, yet return } out = appendHandle(out, res) buf = leftover goto pipeline } func (hs *httpServer) OnInitComplete(srv gnet.Server) (action gnet.Action) { //log.Printf("HTTP server is listening on %s (multi-cores: %t, loops: %d)\n", // srv.Addr.String(), srv.Multicore, srv.NumEventLoop) return } func (hs *httpServer) React(frame []byte, c gnet.Conn) (out []byte, action gnet.Action) { if c.Context() != nil { // bad thing happened out = errMsgBytes action = gnet.Close return } // handle the request out = frame return } func main() { var port int var multicore bool // Example command: go run http.go --port 8080 --multicore=true flag.IntVar(&port, "port", 8888, "server port") flag.BoolVar(&multicore, "multicore", true, "multicore") flag.Parse() res = "Hello World!\r\n" http := new(httpServer) hc := new(httpCodec) // Start serving! log.Fatal(gnet.Serve(http, fmt.Sprintf("tcp://:%d", port), gnet.WithMulticore(multicore), gnet.WithCodec(hc), gnet.WithNumEventLoop(3), gnet.WithReusePort(true))) } // appendHandle handles the incoming request and appends the response to // the provided bytes, which is then returned to the caller. func appendHandle(b []byte, res string) []byte { return appendResp(b, "200 OK", "", res) } // appendResp will append a valid http response to the provide bytes. // The status param should be the code plus text such as "200 OK". // The head parameter should be a series of lines ending with "\r\n" or empty. func appendResp(b []byte, status, head, body string) []byte { b = append(b, "HTTP/1.1"...) b = append(b, ' ') b = append(b, status...) b = append(b, '\r', '\n') b = append(b, "Server: gnet\r\n"...) b = append(b, "Date: "...) b = time.Now().AppendFormat(b, "Mon, 02 Jan 2006 15:04:05 GMT") b = append(b, '\r', '\n') if len(body) > 0 { b = append(b, "Content-Length: "...) b = strconv.AppendInt(b, int64(len(body)), 10) b = append(b, '\r', '\n') } b = append(b, head...) b = append(b, '\r', '\n') if len(body) > 0 { b = append(b, body...) } return b } func b2s(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } func parseReq(data []byte, req *request) (leftover []byte, err error) { sdata := b2s(data) var i, s int var head string var clen int q := -1 // method, path, proto line for ; i < len(sdata); i++ { if sdata[i] == ' ' { req.method = sdata[s:i] for i, s = i+1, i+1; i < len(sdata); i++ { if sdata[i] == '?' && q == -1 { q = i - s } else if sdata[i] == ' ' { if q != -1 { req.path = sdata[s:q] req.query = req.path[q+1 : i] } else { req.path = sdata[s:i] } for i, s = i+1, i+1; i < len(sdata); i++ { if sdata[i] == '\n' && sdata[i-1] == '\r' { req.proto = sdata[s:i] i, s = i+1, i+1 break } } break } } break } } if req.proto == "" { return data, fmt.Errorf("malformed request") } head = sdata[:s] for ; i < len(sdata); i++ { if i > 1 && sdata[i] == '\n' && sdata[i-1] == '\r' { line := sdata[s : i-1] s = i + 1 if line == "" { req.head = sdata[len(head)+2 : i+1] i++ if clen > 0 { if len(sdata[i:]) < clen { break } req.body = sdata[i : i+clen] i += clen } return data[i:], nil } if strings.HasPrefix(line, "Content-Length:") { n, err := strconv.ParseInt(strings.TrimSpace(line[len("Content-Length:"):]), 10, 64) if err == nil { clen = int(n) } } } } // not enough data return data, nil }
看一下這個(gè)源碼解析,還是先從gnet.Serve看起來(lái)
gnet.Serve
// Serve starts handling events for the specified address. // // Address should use a scheme prefix and be formatted // like `tcp://192.168.0.10:9851` or `unix://socket`. // Valid network schemes: // tcp - bind to both IPv4 and IPv6 // tcp4 - IPv4 // tcp6 - IPv6 // udp - bind to both IPv4 and IPv6 // udp4 - IPv4 // udp6 - IPv6 // unix - Unix Domain Socket // // The "tcp" network scheme is assumed when one is not specified. func Serve(eventHandler EventHandler, protoAddr string, opts ...Option) (err error) { // 加載用戶(hù)指定的配置 options := loadOptions(opts...) logging.Debugf("default logging level is %s", logging.LogLevel()) var ( logger logging.Logger flush func() error ) if options.LogPath != "" { if logger, flush, err = logging.CreateLoggerAsLocalFile(options.LogPath, options.LogLevel); err != nil { return } } else { logger = logging.GetDefaultLogger() } if options.Logger == nil { options.Logger = logger } defer func() { if flush != nil { _ = flush() } logging.Cleanup() }() // The maximum number of operating system threads that the Go program can use is initially set to 10000, // which should also be the maximum amount of I/O event-loops locked to OS threads that users can start up. // 為了防止線(xiàn)程過(guò)多 if options.LockOSThread && options.NumEventLoop > 10000 { logging.Errorf("too many event-loops under LockOSThread mode, should be less than 10,000 "+ "while you are trying to set up %d\n", options.NumEventLoop) return errors.ErrTooManyEventLoopThreads } if rbc := options.ReadBufferCap; rbc <= 0 { options.ReadBufferCap = 0x10000 } else { options.ReadBufferCap = internal.CeilToPowerOfTwo(rbc) } // 解析addr network, addr := parseProtoAddr(protoAddr) // 初始化listener var ln *listener if ln, err = initListener(network, addr, options); err != nil { return } defer ln.close() return serve(eventHandler, ln, options, protoAddr) }
可以看出來(lái)參數(shù)是EventHandler 這樣的interface
type ( // EventHandler represents the server events' callbacks for the Serve call. // Each event has an Action return value that is used manage the state // of the connection and server. EventHandler interface { // OnInitComplete fires when the server is ready for accepting connections. // The parameter:server has information and various utilities. OnInitComplete(server Server) (action Action) // OnShutdown fires when the server is being shut down, it is called right after // all event-loops and connections are closed. OnShutdown(server Server) // OnOpened fires when a new connection has been opened. // The parameter:c has information about the connection such as it's local and remote address. // Parameter:out is the return value which is going to be sent back to the client. // It is generally not recommended to send large amounts of data back to the client in OnOpened. // // Note that the bytes returned by OnOpened will be sent back to client without being encoded. OnOpened(c Conn) (out []byte, action Action) // OnClosed fires when a connection has been closed. // The parameter:err is the last known connection error. OnClosed(c Conn, err error) (action Action) // PreWrite fires just before any data is written to any client socket, this event function is usually used to // put some code of logging/counting/reporting or any prepositive operations before writing data to client. PreWrite() // React fires when a connection sends the server data. // Call c.Read() or c.ReadN(n) within the parameter:c to read incoming data from client. // Parameter:out is the return value which is going to be sent back to the client. React(frame []byte, c Conn) (out []byte, action Action) // Tick fires immediately after the server starts and will fire again // following the duration specified by the delay return value. Tick() (delay time.Duration, action Action) } // EventServer is a built-in implementation of EventHandler which sets up each method with a default implementation, // you can compose it with your own implementation of EventHandler when you don't want to implement all methods // in EventHandler. EventServer struct{} )
initListener
然后看一下初始化監(jiān)聽(tīng)
func initListener(network, addr string, options *Options) (l *listener, err error) { var sockopts []socket.Option // 判斷是否開(kāi)啟重復(fù)使用端口 if options.ReusePort || strings.HasPrefix(network, "udp") { sockopt := socket.Option{SetSockopt: socket.SetReuseport, Opt: 1} sockopts = append(sockopts, sockopt) } // 是否開(kāi)啟nagle算法 默認(rèn)是關(guān)閉 if options.TCPNoDelay == TCPNoDelay && strings.HasPrefix(network, "tcp") { sockopt := socket.Option{SetSockopt: socket.SetNoDelay, Opt: 1} sockopts = append(sockopts, sockopt) } // 設(shè)置socket的recv buffer if options.SocketRecvBuffer > 0 { sockopt := socket.Option{SetSockopt: socket.SetRecvBuffer, Opt: options.SocketRecvBuffer} sockopts = append(sockopts, sockopt) } // 設(shè)置socket的send buffer if options.SocketSendBuffer > 0 { sockopt := socket.Option{SetSockopt: socket.SetSendBuffer, Opt: options.SocketSendBuffer} sockopts = append(sockopts, sockopt) } l = &listener{network: network, addr: addr, sockopts: sockopts} err = l.normalize() return }
normalize最后調(diào)用的是tcpSocket方法。
// tcpSocket creates an endpoint for communication and returns a file descriptor that refers to that endpoint. // Argument `reusePort` indicates whether the SO_REUSEPORT flag will be assigned. func tcpSocket(proto, addr string, sockopts ...Option) (fd int, netAddr net.Addr, err error) { var ( family int ipv6only bool sockaddr unix.Sockaddr ) // 獲取地址 if sockaddr, family, netAddr, ipv6only, err = getTCPSockaddr(proto, addr); err != nil { return } // 調(diào)用 底層的socket方法 // 調(diào)用 unix.Socket(family, sotype|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, proto) if fd, err = sysSocket(family, unix.SOCK_STREAM, unix.IPPROTO_TCP); err != nil { err = os.NewSyscallError("socket", err) return } defer func() { if err != nil { _ = unix.Close(fd) } }() if family == unix.AF_INET6 && ipv6only { if err = SetIPv6Only(fd, 1); err != nil { return } } // 添加率socket的一些自定義參數(shù) for _, sockopt := range sockopts { if err = sockopt.SetSockopt(fd, sockopt.Opt); err != nil { return } } // bind if err = os.NewSyscallError("bind", unix.Bind(fd, sockaddr)); err != nil { return } // 設(shè)置半連接數(shù)量的最大值 // Set backlog size to the maximum. err = os.NewSyscallError("listen", unix.Listen(fd, listenerBacklogMaxSize)) return }
serve
func serve(eventHandler EventHandler, listener *listener, options *Options, protoAddr string) error { // Figure out the proper number of event-loops/goroutines to run. numEventLoop := 1 if options.Multicore { numEventLoop = runtime.NumCPU() } if options.NumEventLoop > 0 { numEventLoop = options.NumEventLoop } // 實(shí)例化server svr := new(server) svr.opts = options svr.eventHandler = eventHandler svr.ln = listener // 判斷選擇的輪訓(xùn)方式 默認(rèn)是RoundRobin switch options.LB { case RoundRobin: svr.lb = new(roundRobinLoadBalancer) case LeastConnections: svr.lb = new(leastConnectionsLoadBalancer) case SourceAddrHash: svr.lb = new(sourceAddrHashLoadBalancer) } svr.cond = sync.NewCond(&sync.Mutex{}) if svr.opts.Ticker { svr.tickerCtx, svr.cancelTicker = context.WithCancel(context.Background()) } svr.codec = func() ICodec { if options.Codec == nil { return new(BuiltInFrameCodec) } return options.Codec }() server := Server{ svr: svr, Multicore: options.Multicore, Addr: listener.lnaddr, NumEventLoop: numEventLoop, ReusePort: options.ReusePort, TCPKeepAlive: options.TCPKeepAlive, } switch svr.eventHandler.OnInitComplete(server) { case None: case Shutdown: return nil } // 開(kāi)啟svr的start if err := svr.start(numEventLoop); err != nil { svr.closeEventLoops() svr.opts.Logger.Errorf("gnet server is stopping with error: %v", err) return err } defer svr.stop(server) allServers.Store(protoAddr, svr) return nil } func (svr *server) start(numEventLoop int) error { if svr.opts.ReusePort || svr.ln.network == "udp" { // 啟動(dòng)eventLoops的事件循環(huán) return svr.activateEventLoops(numEventLoop) } return svr.activateReactors(numEventLoop) }
然后看一下activateEventLoops方法。
activateEventLoops
func (svr *server) activateEventLoops(numEventLoop int) (err error) { var striker *eventloop // Create loops locally and bind the listeners. for i := 0; i < numEventLoop; i++ { ln := svr.ln if i > 0 && (svr.opts.ReusePort || ln.network == "udp") { // 再次調(diào)用initListener這個(gè)方法 生成新的socket if ln, err = initListener(svr.ln.network, svr.ln.addr, svr.opts); err != nil { return } } var p *netpoll.Poller if p, err = netpoll.OpenPoller(); err == nil { // 實(shí)例化eventloop el := new(eventloop) el.ln = ln el.svr = svr el.poller = p el.buffer = make([]byte, svr.opts.ReadBufferCap) el.connections = make(map[int]*conn) el.eventHandler = svr.eventHandler // 添加監(jiān)聽(tīng)的套接字 // 注意這里的loopAccept是一個(gè)回調(diào)函數(shù) _ = el.poller.AddRead(el.ln.packPollAttachment(el.loopAccept)) // 注冊(cè) svr.lb.register(el) // Start the ticker. if el.idx == 0 && svr.opts.Ticker { striker = el } } else { return } } // Start event-loops in background. svr.startEventLoops() go striker.loopTicker(svr.tickerCtx) return }
然后 看一下 OpenPoller方法
// OpenPoller instantiates a poller. func OpenPoller() (poller *Poller, err error) { // 創(chuàng)建poller實(shí)例 poller = new(Poller) // 調(diào)用 epoll_create if poller.fd, err = unix.EpollCreate1(unix.EPOLL_CLOEXEC); err != nil { poller = nil err = os.NewSyscallError("epoll_create1", err) return } // 創(chuàng)建eventfd用來(lái)喚醒epoll if poller.wfd, err = unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC); err != nil { _ = poller.Close() poller = nil err = os.NewSyscallError("eventfd", err) return } poller.wfdBuf = make([]byte, 8) // eventfd加入到監(jiān)聽(tīng)中 if err = poller.AddRead(&PollAttachment{FD: poller.wfd}); err != nil { _ = poller.Close() poller = nil return } // 實(shí)例化asyncTaskQueue和priorAsyncTaskQueue poller.asyncTaskQueue = queue.NewLockFreeQueue() poller.priorAsyncTaskQueue = queue.NewLockFreeQueue() return }
然后看一下loopAccept 這個(gè)方法
func (el *eventloop) loopAccept(_ netpoll.IOEvent) error { if el.ln.network == "udp" { return el.loopReadUDP(el.ln.fd) } // 因?yàn)榍懊嬖趇nitListener這里只運(yùn)行了bind方法 所以這里accept nfd, sa, err := unix.Accept(el.ln.fd) if err != nil { if err == unix.EAGAIN { return nil } el.getLogger().Errorf("Accept() fails due to error: %v", err) return os.NewSyscallError("accept", err) } // 獲取到了以后設(shè)置為非阻塞 if err = os.NewSyscallError("fcntl nonblock", unix.SetNonblock(nfd, true)); err != nil { return err } netAddr := socket.SockaddrToTCPOrUnixAddr(sa) if el.svr.opts.TCPKeepAlive > 0 && el.svr.ln.network == "tcp" { err = socket.SetKeepAlive(nfd, int(el.svr.opts.TCPKeepAlive/time.Second)) logging.LogErr(err) } // 根據(jù)套接字實(shí)例化連接 c := newTCPConn(nfd, el, sa, netAddr) // 在epoll中添加監(jiān)聽(tīng) if err = el.poller.AddRead(c.pollAttachment); err == nil { el.connections[c.fd] = c return el.loopOpen(c) } return err }
然后看一下 startEventLoops 這個(gè)方法
func (svr *server) startEventLoops() { // iterate 就是運(yùn)行下面的方法 svr.lb.iterate(func(i int, el *eventloop) bool { svr.wg.Add(1) go func() { // 調(diào)用loopRun el.loopRun(svr.opts.LockOSThread) svr.wg.Done() }() return true }) } func (el *eventloop) loopRun(lockOSThread bool) { if lockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } defer func() { el.closeAllConns() el.ln.close() el.svr.signalShutdown() }() // 調(diào)用Polling 注意這里Polling里面?zhèn)鞯氖且粋€(gè)方法 err := el.poller.Polling(func(fd int, ev uint32) (err error) { // 注意里面這個(gè)連接有事件發(fā)生的時(shí)候 if c, ok := el.connections[fd]; ok { // Don't change the ordering of processing EPOLLOUT | EPOLLRDHUP / EPOLLIN unless you're 100% // sure what you're doing! // Re-ordering can easily introduce bugs and bad side-effects, as I found out painfully in the past. // We should always check for the EPOLLOUT event first, as we must try to send the leftover data back to // client when any error occurs on a connection. // // Either an EPOLLOUT or EPOLLERR event may be fired when a connection is refused. // In either case loopWrite() should take care of it properly: // 1) writing data back, // 2) closing the connection. if ev&netpoll.OutEvents != 0 && !c.outboundBuffer.IsEmpty() { // 寫(xiě)事件 if err := el.loopWrite(c); err != nil { return err } } // If there is pending data in outbound buffer, then we should omit this readable event // and prioritize the writable events to achieve a higher performance. // // Note that the client may send massive amounts of data to server by write() under blocking mode, // resulting in that it won't receive any responses before the server read all data from client, // in which case if the socket send buffer is full, we need to let it go and continue reading the data // to prevent blocking forever. // 讀事件 if ev&netpoll.InEvents != 0 && (ev&netpoll.OutEvents == 0 || c.outboundBuffer.IsEmpty()) { return el.loopRead(c) } return nil } // 說(shuō)明只是可以建立新的連接 return el.loopAccept(ev) }) el.getLogger().Debugf("event-loop(%d) is exiting due to error: %v", el.idx, err) }
polling
這個(gè)方法是比較重要的,也是阻塞在epoll上面,去監(jiān)聽(tīng)fd的事件
// Polling blocks the current goroutine, waiting for network-events. func (p *Poller) Polling(callback func(fd int, ev uint32) error) error { el := newEventList(InitPollEventsCap) var wakenUp bool msec := -1 for { // 使用epoll_wait n, err := unix.EpollWait(p.fd, el.events, msec) if n == 0 || (n < 0 && err == unix.EINTR) { msec = -1 runtime.Gosched() continue } else if err != nil { logging.Errorf("error occurs in epoll: %v", os.NewSyscallError("epoll_wait", err)) return err } msec = 0 // 判斷每個(gè)套接字的事件 for i := 0; i < n; i++ { ev := &el.events[i] // 判斷是不是喚醒的 if fd := int(ev.Fd); fd != p.wfd { switch err = callback(fd, ev.Events); err { case nil: case errors.ErrAcceptSocket, errors.ErrServerShutdown: return err default: logging.Warnf("error occurs in event-loop: %v", err) } } else { // poller is awaken to run tasks in queues. wakenUp = true _, _ = unix.Read(p.wfd, p.wfdBuf) } } // 進(jìn)行喚醒 if wakenUp { wakenUp = false task := p.priorAsyncTaskQueue.Dequeue() // 運(yùn)行任務(wù) for ; task != nil; task = p.priorAsyncTaskQueue.Dequeue() { switch err = task.Run(task.Arg); err { case nil: case errors.ErrServerShutdown: return err default: logging.Warnf("error occurs in user-defined function, %v", err) } // 放入任務(wù) queue.PutTask(task) } for i := 0; i < MaxAsyncTasksAtOneTime; i++ { if task = p.asyncTaskQueue.Dequeue(); task == nil { break } switch err = task.Run(task.Arg); err { case nil: case errors.ErrServerShutdown: return err default: logging.Warnf("error occurs in user-defined function, %v", err) } queue.PutTask(task) } atomic.StoreInt32(&p.netpollWakeSig, 0) if (!p.asyncTaskQueue.Empty() || !p.priorAsyncTaskQueue.Empty()) && atomic.CompareAndSwapInt32(&p.netpollWakeSig, 0, 1) { for _, err = unix.Write(p.wfd, b); err == unix.EINTR || err == unix.EAGAIN; _, err = unix.Write(p.wfd, b) { } } } if n == el.size { el.expand() } else if n < el.size>>1 { el.shrink() } } }
這里主要分析的是在reuse port的情況下,根據(jù)你開(kāi)多少線(xiàn)程那么開(kāi)多少個(gè)open poll,這樣的話(huà)線(xiàn)程數(shù)量就是固定的,就不會(huì)出現(xiàn)goroutine暴增的情況,同時(shí)因?yàn)槊看蝍ccept連接后,便會(huì)設(shè)置成了非阻塞的,并且不會(huì)阻塞在read和write這樣的io事件上,通過(guò)這些行為保證了整個(gè)流程的高可用
到此這篇關(guān)于go語(yǔ)言 http模型reactor的文章就介紹到這了,更多相關(guān)go http模型reactor內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go module構(gòu)建項(xiàng)目的實(shí)現(xiàn)
本文主要介紹了go module構(gòu)建項(xiàng)目的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03使用Go和Gorm實(shí)現(xiàn)讀取SQLCipher加密數(shù)據(jù)庫(kù)
本文檔主要描述通過(guò)Go和Gorm實(shí)現(xiàn)生成和讀取SQLCipher加密數(shù)據(jù)庫(kù)以及其中踩的一些坑,文章通過(guò)代碼示例講解的非常詳細(xì), 對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-06-06golang默認(rèn)Logger日志庫(kù)在項(xiàng)目中使用Zap日志庫(kù)
這篇文章主要為大家介紹了golang默認(rèn)Logger日志庫(kù)在項(xiàng)目中使用Zap日志庫(kù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04Go語(yǔ)言中利用http發(fā)起Get和Post請(qǐng)求的方法示例
這篇文章主要給大家介紹了關(guān)于Go語(yǔ)言中利用http發(fā)起Get和Post請(qǐng)求的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11golang封裝一個(gè)執(zhí)行命令行的函數(shù)(return?stderr/stdout/exitcode)示例代碼
在?Go?語(yǔ)言中,您可以使用?os/exec?包來(lái)執(zhí)行外部命令,不通過(guò)調(diào)用?shell,并且能夠獲得進(jìn)程的退出碼、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯(cuò)誤輸出,下面給大家分享golang封裝一個(gè)執(zhí)行命令行的函數(shù)(return?stderr/stdout/exitcode)的方法,感興趣的朋友跟隨小編一起看看吧2024-06-06GoAdminGroup/go-admin的安裝和運(yùn)行的教程詳解
這篇文章主要介紹了GoAdminGroup/go-admin的安裝和運(yùn)行的教程詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09Go語(yǔ)言實(shí)現(xiàn)AOI區(qū)域視野管理流程詳解
在游戲中,場(chǎng)景里存在大量的物體.如果我們把所有物體的變化都廣播給玩家.那客戶(hù)端很難承受這么大的壓力.因此我們肯定會(huì)做優(yōu)化.把不必要的信息過(guò)濾掉.如只關(guān)心玩家視野所看到的.減輕客戶(hù)端的壓力,給玩家更流暢的體驗(yàn)2023-03-03使用Go實(shí)現(xiàn)TLS服務(wù)器和客戶(hù)端的示例
本文主要介紹了Go實(shí)現(xiàn)TLS服務(wù)器和客戶(hù)端的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12Golang實(shí)現(xiàn)gRPC的Proxy的原理解析
gRPC是Google開(kāi)始的一個(gè)RPC服務(wù)框架, 是英文全名為Google Remote Procedure Call的簡(jiǎn)稱(chēng),廣泛的應(yīng)用在有RPC場(chǎng)景的業(yè)務(wù)系統(tǒng)中,這篇文章主要介紹了Golang實(shí)現(xiàn)gRPC的Proxy的原理,需要的朋友可以參考下2021-09-09