Go语言TCP Socket编程(下)

简介: Go语言TCP Socket编程

Go语言TCP Socket编程(上):https://developer.aliyun.com/article/1490865


conn.Write

和读相比,Write遇到的情形一样不少,我们也逐一看一下。


1、成功写

前面例子着重于Read,client端在Write时并未判断Write的返回值。所谓“成功写”指的就是Write调用返回的n与预期要写入的数据长度相等,且error == nil。这是我们在调用Write时遇到的最常见的情形,这里不再举例了。

2、写阻塞

TCP连接通信两端的OS都会为该连接保留数据缓冲,一端调用Write后,实际上数据是写入到OS的协议栈的数据缓冲的。TCP是全双工通信,因此每个方向都有独立的数据缓冲。当发送方将对方的接收缓冲区以及自身的发送缓冲区写满后,Write就会阻塞。我们来看一个例子:client5.go和server.go。


:::details

package main

import (
  "log"
  "net"
  "time"
)

func handleConn(c net.Conn) {
  defer c.Close()
  time.Sleep(time.Second * 10)
  for {
    // read from the connection
    time.Sleep(5 * time.Second)
    var buf = make([]byte, 60000)
    log.Println("start to read from conn")
    n, err := c.Read(buf)
    if err != nil {
      log.Printf("conn read %d bytes,  error: %s", n, err)
      if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
        continue
      }
    }

    log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
  }
}

func main() {
  listen, err := net.Listen("tcp", ":8001")
  if err != nil {
    log.Println("listen occurs an error: ", err)
    return
  }
  for {
    con, err := listen.Accept()
    if err != nil {
      log.Println("accept occurs error: ", err)
      continue
    }
    handleConn(con)
  }
}

package main

import (
  "log"
  "net"
  "time"
)

func main() {
  log.Println("begin dial...")
  conn, err := net.Dial("tcp", ":8001")
  if err != nil {
    log.Println("dial error:", err)
    return
  }
  defer conn.Close()
  log.Println("dial ok")

  data := make([]byte, 65536)
  var total int
  for {
    n, err := conn.Write(data)
    if err != nil {
      total += n
      log.Printf("write %d bytes, error:%s\n", n, err)
      break
    }
    total += n
    log.Printf("write %d bytes this time, %d bytes in total\n", n, total)
  }

  log.Printf("write %d bytes in total\n", total)
  time.Sleep(time.Second * 10000)
}

结果

> go run .\server5.go
2022/04/20 22:46:13 start to read from conn
2022/04/20 22:46:13 read 60000 bytes, content is 
2022/04/20 22:46:18 start to read from conn
2022/04/20 22:46:18 read 60000 bytes, content is 
2022/04/20 22:46:23 start to read from conn
2022/04/20 22:46:23 read 60000 bytes, content is 
2022/04/20 22:46:28 start to read from conn
2022/04/20 22:46:28 read 60000 bytes, content is 
2022/04/20 22:46:33 start to read from conn
2022/04/20 22:46:33 read 60000 bytes, content is 
2022/04/20 22:46:38 start to read from conn
2022/04/20 22:46:38 read 60000 bytes, content is 
2022/04/20 22:46:43 start to read from conn
2022/04/20 22:46:43 read 60000 bytes, content is 
2022/04/20 22:46:48 start to read from conn
2022/04/20 22:46:48 read 60000 bytes, content is 
exit status 0xc000013a

> go run .\client5.go
2022/04/20 22:45:58 begin dial...
2022/04/20 22:45:58 dial ok
2022/04/20 22:45:58 write 65536 bytes this time, 65536 bytes in total

...


2022/04/20 22:46:18 write 65536 bytes this time, 4390912 bytes in total
2022/04/20 22:46:18 write 65536 bytes this time, 4456448 bytes in total
2022/04/20 22:46:18 write 65536 bytes this time, 4521984 bytes in total
2022/04/20 22:46:18 write 65536 bytes this time, 4587520 bytes in total
2022/04/20 22:46:18 write 65536 bytes this time, 4653056 bytes in total
2022/04/20 22:46:50 write 0 bytes, error:write tcp 127.0.0.1:49307->127.0.0.1:8001: wsasend: An existing connection was forcibly closed by the remote host.
2022/04/20 22:46:50 write 4653056 bytes in total
exit status 0xc000013a

Server5在前10s中并不Read数据,因此当client5一直尝试写入时,写到一定量后就会发生阻塞


:::


在Darwin上,这个size大约在679468bytes。后续当server5每隔5s进行Read时,OS socket缓冲区腾出了空间,client5就又可以写入了:

3、写入部分数据

Write操作存在写入部分数据的情况,比如上面例子中,当client端输出日志停留在“write 65536 bytes this time, 655360 bytes in total”时,我们杀掉server5,这时我们会看到client5输出以下日志:

...
> go run .\client5.go
2022/04/20 22:55:03 begin dial...
2022/04/20 22:55:03 dial ok
...

2022/04/20 22:55:03 write 65536 bytes this time, 2293760 bytes in total
2022/04/20 22:55:06 write 0 bytes, error:write tcp 127.0.0.1:50077->127.0.0.1:8001: wsasend: An existing connection was forcibly closed by the remote host.
2022/04/20 22:55:06 write 2293760 bytes in total

测试了很多次,并没有出现以下结果,可能是因为版本的问题。


显然Write并非在65536这个地方阻塞的,而是后续又写入很多数据后发生了阻塞,server端socket关闭后,我们看到Wrote返回er != nil且n = 24108,程序需要对这部分写入的24108字节做特定处理。

4、写入超时

如果非要给Write增加一个期限,那我们可以调用SetWriteDeadline方法。

:::details

package main

import (
  "log"
  "net"
  "time"
)

func handleConn(c net.Conn) {
  defer c.Close()
  time.Sleep(time.Second * 10)
  for {
    // read from the connection
    time.Sleep(5 * time.Second)
    var buf = make([]byte, 60000)
    log.Println("start to read from conn")
    n, err := c.Read(buf)
    if err != nil {
      log.Printf("conn read %d bytes,  error: %s", n, err)
      if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
        continue
      }
    }

    log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
  }
}

func main() {
  listen, err := net.Listen("tcp", ":8001")
  if err != nil {
    log.Println("listen occurs an error: ", err)
    return
  }
  for {
    con, err := listen.Accept()
    if err != nil {
      log.Println("accept occurs error: ", err)
      continue
    }
    handleConn(con)
  }
}

package main

import (
  "log"
  "net"
  "time"
)

func main() {
  log.Println("begin dial...")
  conn, err := net.Dial("tcp", ":8001")
  if err != nil {
    log.Println("dial error:", err)
    return
  }
  defer conn.Close()
  log.Println("dial ok")

  data := make([]byte, 65536)
  var total int
  for {
    // 设置写超时
    conn.SetWriteDeadline(
      time.Now().Add(10 * time.Microsecond))
    n, err := conn.Write(data)
    if err != nil {
      total += n
      log.Printf("write %d bytes, error:%s\n", n, err)
      break
    }
    total += n
    log.Printf("write %d bytes this time, %d bytes in total\n", n, total)
    time.Sleep(100 * time.Millisecond)
  }

  log.Printf("write %d bytes in total\n", total)
}


:::

启动server6.go,启动client6.go,我们可以看到写入超时的情况下,Write的返回结果:

:::details

> go run .\client6.go
2022/04/20 23:02:50 begin dial...
2022/04/20 23:02:50 dial ok
2022/04/20 23:02:50 write 65536 bytes this time, 65536 bytes in total
2022/04/20 23:02:50 write 65536 bytes this time, 131072 bytes in total
2022/04/20 23:02:50 write 65536 bytes this time, 196608 bytes in total
...
2022/04/20 23:02:53 write 65536 bytes this time, 2228224 bytes in total
2022/04/20 23:02:53 write 65536 bytes this time, 2293760 bytes in total
2022/04/20 23:02:54 write 0 bytes, error:write tcp 127.0.0.1:50553->127.0.0.1:8001: i/o timeout
2022/04/20 23:02:54 write 2293760 bytes in total
> go run .\server5.go
2022/04/20 23:03:05 start to read from conn
2022/04/20 23:03:05 read 60000 bytes, content is 
2022/04/20 23:03:10 start to read from conn
2022/04/20 23:03:10 read 60000 bytes, content is 

...

2022/04/20 23:05:05 start to read from conn
2022/04/20 23:05:05 read 60000 bytes, content is 
2022/04/20 23:05:10 start to read from conn
2022/04/20 23:05:10 conn read 0 bytes,  error: read tcp 127.0.0.1:8001->127.0.0.1:50553: wsarecv: An existing connection was forcibly closed by the remote host.
2022/04/20 23:05:10 read 0 bytes, content is 
2022/04/20 23:05:15 start to read from conn

...

2022/04/20 23:06:25 start to read from conn
2022/04/20 23:06:25 conn read 0 bytes,  error: read tcp 127.0.0.1:8001->127.0.0.1:50553: wsarecv: An existing connection was forcibly closed by the remote host.
2022/04/20 23:06:25 read 0 bytes, content is
exit status 0xc000013a


可以看到在写入超时时,依旧存在部分数据写入的情况。

:::


综上例子,虽然Go给我们提供了阻塞I/O的便利,但在调用 ReadWrite 时依旧要综合需要方法返回的nerr的结果,以做出正确处理。net.conn实现了io.Readerio.Writer接口,因此可以试用一些wrapper包进行socket读写,比如bufio包下面的WriterReaderio/ioutil下的函数等。

Goroutine safe

基于goroutine的网络架构模型,存在在不同goroutine间共享conn的情况,那么conn的读写是否是goroutine safe的呢?在深入这个问题之前,我们先从应用意义上来看read操作和write操作的goroutine-safe必要性。

对于read操作而言,由于 TCP 是面向字节流,conn.Read 无法正确区分数据的业务边界,因此多个goroutine对同一个conn进行read的意义不大,goroutine读到不完整的业务包反倒是增加了业务处理的难度。对与 Write 操作而言,倒是有多个goroutine并发写的情况。

不过conn读写是否goroutine-safe的测试不是很好做,我们先深入一下runtime代码,先从理论上给这个问题定个性:

:::tip

源码位置

go/net.go at master · golang/go (github.com)

:::

:::details

net.conn只是*netFD的wrapper结构,最终Write和Read都会落在其中的fd上:

type conn struct {
    fd *netFD
}

netFD在不同平台上有着不同的实现,我们以go/fd_plan9.go at master · golang/go (github.com)中的netFD为例:

// Network file descriptor.
type netFD struct {
  pfd poll.FD

  // immutable until Close
  net               string
  n                 string
  dir               string
  listen, ctl, data *os.File
  laddr, raddr      Addr
  isStream          bool
}

...

func (fd *netFD) ok() bool { return fd != nil && fd.ctl != nil }

func (fd *netFD) destroy() {
  if !fd.ok() {
    return
  }
  err := fd.ctl.Close()
  if fd.data != nil {
    if err1 := fd.data.Close(); err1 != nil && err == nil {
      err = err1
    }
  }
  if fd.listen != nil {
    if err1 := fd.listen.Close(); err1 != nil && err == nil {
      err = err1
    }
  }
  fd.ctl = nil
  fd.data = nil
  fd.listen = nil
}

func (fd *netFD) Read(b []byte) (n int, err error) {
  if !fd.ok() || fd.data == nil {
    return 0, syscall.EINVAL
  }
  n, err = fd.pfd.Read(fd.data.Read, b)
  if fd.net == "udp" && err == io.EOF {
    n = 0
    err = nil
  }
  return
}

func (fd *netFD) Write(b []byte) (n int, err error) {
  if !fd.ok() || fd.data == nil {
    return 0, syscall.EINVAL
  }
  return fd.pfd.Write(fd.data.Write, b)
}

...
// FD is a file descriptor. The net and os packages use this type as a
// field of a larger type representing a network connection or OS file.
type FD struct {
  // Lock sysfd and serialize access to Read and Write methods.
  fdmu fdMutex

  // System file descriptor. Immutable until Close.
  Sysfd int

  // I/O poller.
  pd pollDesc

  // Writev cache.
  iovecs *[]syscall.Iovec

  // Semaphore signaled when file is closed.
  csema uint32

  // Non-zero if this file has been set to blocking mode.
  isBlocking uint32

  // Whether this is a streaming descriptor, as opposed to a
  // packet-based descriptor like a UDP socket. Immutable.
  IsStream bool

  // Whether a zero byte read indicates EOF. This is false for a
  // message based socket connection.
  ZeroReadIsEOF bool

  // Whether this is a file rather than a network socket.
  isFile bool
}

我们看到poll.FD中包含了fdMutex类型字段,从注释上来看,该fdMutex用来串行化对该netFD对应的sysfdWriteRead操作。从这个注释上来看,所有对connReadWrite操作都是有fdMutex互斥的,从netFD的Read和Write方法的实现也证实了这一点:


// Read implements io.Reader.
func (fd *FD) Read(p []byte) (int, error) {
  if err := fd.readLock(); err != nil {
    return 0, err
  }
  defer fd.readUnlock()
  if len(p) == 0 {
    // If the caller wanted a zero byte read, return immediately
    // without trying (but after acquiring the readLock).
    // Otherwise syscall.Read returns 0, nil which looks like
    // io.EOF.
    // TODO(bradfitz): make it wait for readability? (Issue 15735)
    return 0, nil
  }
  if err := fd.pd.prepareRead(fd.isFile); err != nil {
    return 0, err
  }
  if fd.IsStream && len(p) > maxRW {
    p = p[:maxRW]
  }
  for {
    n, err := ignoringEINTRIO(syscall.Read, fd.Sysfd, p)
    if err != nil {
      n = 0
      if err == syscall.EAGAIN && fd.pd.pollable() {
        if err = fd.pd.waitRead(fd.isFile); err == nil {
          continue
        }
      }
    }
    err = fd.eofError(n, err)
    return n, err
  }
}


  • write

go/fd_unix.go at master · golang/go (github.com)


// Write implements io.Writer.
func (fd *FD) Write(p []byte) (int, error) {
  if err := fd.writeLock(); err != nil {
    return 0, err
  }
  defer fd.writeUnlock()
  if err := fd.pd.prepareWrite(fd.isFile); err != nil {
    return 0, err
  }
  var nn int
  for {
    max := len(p)
    if fd.IsStream && max-nn > maxRW {
      max = nn + maxRW
    }
    n, err := ignoringEINTRIO(syscall.Write, fd.Sysfd, p[nn:max])
    if n > 0 {
      nn += n
    }
    if nn == len(p) {
      return nn, err
    }
    if err == syscall.EAGAIN && fd.pd.pollable() {
      if err = fd.pd.waitWrite(fd.isFile); err == nil {
        continue
      }
    }
    if err != nil {
      return nn, err
    }
    if n == 0 {
      return nn, io.ErrUnexpectedEOF
    }
  }
}

:::

每次Write操作都是受lock保护,直到此次数据全部write完。因此在应用层面,要想保证多个Goroutine在一个connwrite操作的Safe,需要一次write完整写入一个“业务包”;一旦将业务包的写入拆分为多次write

那就无法保证某个Goroutine的某“业务包”数据在conn发送的连续性。

同时也可以看出即便是Read操作,也是lock保护的。多个Goroutine对同一conn的并发读不会出现读出内容重叠的情况,但内容断点是依 runtime 调度来随机确定的。

存在一个业务包数据,1/3内容被goroutine-1读走,另外2/3被另外一个goroutine-2读 走的情况。比如一个完整包:world,当goroutine的read slice size < 5时,

存在可能:一个goroutine读到 “worl”,另外一个goroutine读出”d”。

四、Socket属性

原生Socket API提供了丰富的socket设置接口,但Golang有自己的网络架构模型,golang提供的socket options接口也是基于上述模型的必要的属性设置。包括

  • SetKeepAlive
  • SetKeepAlivePeriod
  • SetLinger
  • SetNoDelay (默认no delay)
  • SetWriteBuffer
  • SetReadBuffer

比如posix 的socket option,go/sockopt_posix.go at master · golang/go (github.com)

不过上面的Method是TCPConn的,而不是Conn的,要使用上面的Method的,需要type assertion:

tcpConn, ok := c.(*TCPConn)
if !ok {
    //error handle
}

tcpConn.SetNoDelay(true)

对于listener socket, golang默认采用了 SO_REUSEADDR,这样当你重启 listener程序时,不会因为address in use的错误而启动失败。而listen backlog的默认值是通过获取系统的设置值得到的。不同系统不同:mac 128, linux 512等。

比如linux的backlog

:::details

// Linux stores the backlog as:
//
//   - uint16 in kernel version < 4.1,
//   - uint32 in kernel version >= 4.1
//
// Truncate number to avoid wrapping.
//
// See issue 5030 and 41470.
func maxAckBacklog(n int) int {
  major, minor := kernelVersion()
  size := 16
  if major > 4 || (major == 4 && minor >= 1) {
    size = 32
  }

  var max uint = 1<<size - 1
  if uint(n) > max {
    n = int(max)
  }
  return n
}

func maxListenerBacklog() int {
  fd, err := open("/proc/sys/net/core/somaxconn")
  if err != nil {
    return syscall.SOMAXCONN
  }
  defer fd.close()
  l, ok := fd.readLine()
  if !ok {
    return syscall.SOMAXCONN
  }
  f := getFields(l)
  n, _, ok := dtoi(f[0])
  if n == 0 || !ok {
    return syscall.SOMAXCONN
  }

  if n > 1<<16-1 {
    return maxAckBacklog(n)
  }
  return n
}

:::

至于backlog是啥,可以参考这篇文章,搜索了挺久才找到的。

使用Go和C实例来探究Linux TCP之listen backlog_Tw!light的博客-CSDN博客

简单理解了一下,博客中提到 backlog是“操作系统层面的套接字队列长度”,应该就是可以接受的最大连接数吧(但是还没去验证)。

五、关闭连接

和前面的方法相比,关闭连接算是最简单的操作了。由于socket是全双工的,clientserver端在己方已关闭的socket和对方关闭的socket上操作的结果有不同。看下面例子:

:::details

package main

import (
  "log"
  "net"
)

func handleConn(c net.Conn) {
  defer c.Close()

  // read from the connection
  var buf = make([]byte, 10)
  log.Println("start to read from conn")
  n, err := c.Read(buf)
  if err != nil {
    log.Println("conn read error:", err)
  } else {
    log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
  }

  n, err = c.Write(buf)
  if err != nil {
    log.Println("conn write error:", err)
  } else {
    log.Printf("write %d bytes, content is %s\n", n, string(buf[:n]))
  }
}

func main() {
  listen, err := net.Listen("tcp", ":8888")
  if err != nil {
    return
  }
  log.Println("start to listen")
  for {
    con, err := listen.Accept()
    if err != nil {
      return
    }
    log.Println("a new connection accept")
    handleConn(con)
  }
}


package main

import (
  "log"
  "net"
  "time"
)

func main() {
  log.Println("begin dial...")
  conn, err := net.Dial("tcp", ":8888")
  if err != nil {
    log.Println("dial error:", err)
    return
  }
  conn.Close()
  log.Println("close ok")

  var buf = make([]byte, 32)
  n, err := conn.Read(buf)
  if err != nil {
    log.Println("read error:", err)
  } else {
    log.Printf("read % bytes, content is %s\n", n, string(buf[:n]))
  }

  n, err = conn.Write(buf)
  if err != nil {
    log.Println("write error:", err)
  } else {
    log.Printf("write % bytes, content is %s\n", n, string(buf[:n]))
  }

  time.Sleep(time.Second * 1000)
}


上述例子的执行结果如下:


> go run .\server.go
2022/04/21 00:29:04 start to listen
2022/04/21 00:29:13 a new connection accept
2022/04/21 00:29:13 start to read from conn
2022/04/21 00:29:13 conn read error: EOF
2022/04/21 00:29:13 write 10 bytes, content is 
2022/04/21 00:29:37 a new connection accept
2022/04/21 00:29:37 start to read from conn
2022/04/21 00:29:37 conn read error: EOF
2022/04/21 00:29:37 write 10 bytes, content is 
exit status 0xc000013a

> go run .\client.go
2022/04/21 00:29:13 begin dial...
2022/04/21 00:29:13 close ok
2022/04/21 00:29:13 read error: read tcp 127.0.0.1:61459->127.0.0.1:8888: use of closed network connection
2022/04/21 00:29:13 write error: write tcp 127.0.0.1:61459->127.0.0.1:8888: use of closed network connection
exit status 0xc000013a


> go run .\client.go
2022/04/21 00:29:37 begin dial...
2022/04/21 00:29:37 close ok
2022/04/21 00:29:37 read error: read tcp 127.0.0.1:61534->127.0.0.1:8888: use of closed network connection
2022/04/21 00:29:37 write error: write tcp 127.0.0.1:61534->127.0.0.1:8888: use of closed network connection
exit status 0xc000013a

:::

从client1的结果来看,在己方已经关闭的socket上再进行readwrite操作,会得到”use of closed network connection” error;server的执行结果来看,在对方关闭的socket上执行read操作会得到EOF error,但write操作会成功,因为数据会成功写入己方的内核socket缓冲区中,即便最终发不到对方socket缓冲区了,因为己方socket并未关闭。因此当发现对方socket关闭后,己方应该正确合理处理自己的socket,再继续write已经无任何意义了。

比如,EOF

:::details

package main

import (
  "errors"
  "io"
  "log"
  "net"
)

func handleConn(c net.Conn) {
  defer c.Close()

  // read from the connection
  var buf = make([]byte, 10)
  log.Println("start to read from conn")
  n, err := c.Read(buf)
  if err != nil {
    if errors.Is(err, io.EOF) {
      log.Println("EOF occur----")
      return
    }
    log.Println("conn read error:", err)

  } else {
    log.Printf("read %d bytes, content is %s\n", n, string(buf[:n]))
  }

  n, err = c.Write(buf)
  if err != nil {
    log.Println("conn write error:", err)
  } else {
    log.Printf("write %d bytes, content is %s\n", n, string(buf[:n]))
  }
}

func main() {
  listen, err := net.Listen("tcp", ":8888")
  if err != nil {
    return
  }
  log.Println("start to listen")
  for {
    con, err := listen.Accept()
    if err != nil {
      return
    }
    log.Println("a new connection accept")
    handleConn(con)
  }
}


package main

import (
  "log"
  "net"
  "time"
)

func main() {
  log.Println("begin dial...")
  conn, err := net.Dial("tcp", ":8888")
  if err != nil {
    log.Println("dial error:", err)
    return
  }
  conn.Close()
  log.Println("close ok")

  var buf = make([]byte, 32)
  n, err := conn.Read(buf)
  if err != nil {
    log.Println("read error:", err)
  } else {
    log.Printf("read % bytes, content is %s\n", n, string(buf[:n]))
  }

  n, err = conn.Write(buf)
  if err != nil {
    log.Println("write error:", err)
  } else {
    log.Printf("write % bytes, content is %s\n", n, string(buf[:n]))
  }

  time.Sleep(time.Second * 1000)
}

输出结果

> go run .\server.go
2022/04/21 00:53:18 start to listen
2022/04/21 00:53:24 a new connection accept
2022/04/21 00:53:24 start to read from conn
2022/04/21 00:53:24 EOF occur----
> go run .\client.go
2022/04/21 00:53:24 begin dial...
2022/04/21 00:53:24 close ok
2022/04/21 00:53:24 read error: read tcp 127.0.0.1:63470->127.0.0.1:8888: use of closed network connection
2022/04/21 00:53:24 write error: write tcp 127.0.0.1:63470->127.0.0.1:8888: use of closed network connection
exit status 0xc000013a

从输出结果来看,在遭遇EOF之后,server不再write,避免了server buf 的浪费。

:::

六、小结

本文比较基础,但却很重要,毕竟golang是面向大规模服务后端的,对通信环节的细节的深入理解会大有裨益。另外Go的goroutine+阻塞通信的网络通信模型降低了开发者心智负担,简化了通信的复杂性,这点尤为重要。


相关文章
|
24天前
|
运维 监控 算法
监控局域网其他电脑:Go 语言迪杰斯特拉算法的高效应用
在信息化时代,监控局域网成为网络管理与安全防护的关键需求。本文探讨了迪杰斯特拉(Dijkstra)算法在监控局域网中的应用,通过计算最短路径优化数据传输和故障检测。文中提供了使用Go语言实现的代码例程,展示了如何高效地进行网络监控,确保局域网的稳定运行和数据安全。迪杰斯特拉算法能减少传输延迟和带宽消耗,及时发现并处理网络故障,适用于复杂网络环境下的管理和维护。
|
25天前
|
编译器 Go
揭秘 Go 语言中空结构体的强大用法
Go 语言中的空结构体 `struct{}` 不包含任何字段,不占用内存空间。它在实际编程中有多种典型用法:1) 结合 map 实现集合(set)类型;2) 与 channel 搭配用于信号通知;3) 申请超大容量的 Slice 和 Array 以节省内存;4) 作为接口实现时明确表示不关注值。此外,需要注意的是,空结构体作为字段时可能会因内存对齐原因占用额外空间。建议将空结构体放在外层结构体的第一个字段以优化内存使用。
|
1月前
|
存储 Go
Go 语言入门指南:切片
Golang中的切片(Slice)是基于数组的动态序列,支持变长操作。它由指针、长度和容量三部分组成,底层引用一个连续的数组片段。切片提供灵活的增减元素功能,语法形式为`[]T`,其中T为元素类型。相比固定长度的数组,切片更常用,允许动态调整大小,并且多个切片可以共享同一底层数组。通过内置的`make`函数可创建指定长度和容量的切片。需要注意的是,切片不能直接比较,只能与`nil`比较,且空切片的长度为0。
Go 语言入门指南:切片
|
1月前
|
算法 安全 Go
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
本文探讨了如何利用 Go 语言中的 Bloom Filter 算法提升公司局域网管理系统的性能。Bloom Filter 是一种高效的空间节省型数据结构,适用于快速判断元素是否存在于集合中。文中通过具体代码示例展示了如何在 Go 中实现 Bloom Filter,并应用于局域网的 IP 访问控制,显著提高系统响应速度和安全性。随着网络规模扩大和技术进步,持续优化算法和结合其他安全技术将是企业维持网络竞争力的关键。
50 2
公司局域网管理系统里的 Go 语言 Bloom Filter 算法,太值得深挖了
|
1月前
|
开发框架 前端开发 Go
eino — 基于go语言的大模型应用开发框架(二)
本文介绍了如何使用Eino框架实现一个基本的LLM(大语言模型)应用。Eino中的`ChatModel`接口提供了与不同大模型服务(如OpenAI、Ollama等)交互的统一方式,支持生成完整响应、流式响应和绑定工具等功能。`Generate`方法用于生成完整的模型响应,`Stream`方法以流式方式返回结果,`BindTools`方法为模型绑定工具。此外,还介绍了通过`Option`模式配置模型参数及模板功能,支持基于前端和用户自定义的角色及Prompt。目前主要聚焦于`ChatModel`的`Generate`方法,后续将继续深入学习。
276 7
|
25天前
|
存储 缓存 监控
企业监控软件中 Go 语言哈希表算法的应用研究与分析
在数字化时代,企业监控软件对企业的稳定运营至关重要。哈希表(散列表)作为高效的数据结构,广泛应用于企业监控中,如设备状态管理、数据分类和缓存机制。Go 语言中的 map 实现了哈希表,能快速处理海量监控数据,确保实时准确反映设备状态,提升系统性能,助力企业实现智能化管理。
33 3
|
1月前
|
监控 Linux PHP
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
【02】客户端服务端C语言-go语言-web端PHP语言整合内容发布-优雅草网络设备监控系统-2月12日优雅草简化Centos stream8安装zabbix7教程-本搭建教程非docker搭建教程-优雅草solution
88 20
|
1月前
|
存储 开发框架 Devops
eino — 基于go语言的大模型应用开发框架(一)
Eino 是一个受开源社区优秀LLM应用开发框架(如LangChain和LlamaIndex)启发的Go语言框架,强调简洁性、可扩展性和可靠性。它提供了易于复用的组件、强大的编排框架、简洁明了的API、最佳实践集合及实用的DevOps工具,支持快速构建和部署LLM应用。Eino不仅兼容多种模型库(如OpenAI、Ollama、Ark),还提供详细的官方文档和活跃的社区支持,便于开发者上手使用。
200 8
|
25天前
|
存储 缓存 安全
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
|
1月前
|
存储 算法 Go
Go语言实战:错误处理和panic_recover之自定义错误类型
本文深入探讨了Go语言中的错误处理和panic/recover机制,涵盖错误处理的基本概念、自定义错误类型的定义、panic和recover的工作原理及应用场景。通过具体代码示例介绍了如何定义自定义错误类型、检查和处理错误值,并使用panic和recover处理运行时错误。文章还讨论了错误处理在实际开发中的应用,如网络编程、文件操作和并发编程,并推荐了一些学习资源。最后展望了未来Go语言在错误处理方面的优化方向。