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+阻塞通信的网络通信模型降低了开发者心智负担,简化了通信的复杂性,这点尤为重要。


相关文章
|
19小时前
|
缓存 测试技术 持续交付
Golang深入浅出之-Go语言中的持续集成与持续部署(CI/CD)
【5月更文挑战第5天】本文介绍了Go语言项目中的CI/CD实践,包括持续集成与持续部署的基础知识,常见问题及解决策略。测试覆盖不足、版本不一致和构建时间过长是主要问题,可通过全面测试、统一依赖管理和利用缓存优化。文中还提供了使用GitHub Actions进行自动化测试和部署的示例,强调了持续优化CI/CD流程以适应项目需求的重要性。
27 1
|
19小时前
|
Kubernetes Cloud Native Go
Golang深入浅出之-Go语言中的云原生开发:Kubernetes与Docker
【5月更文挑战第5天】本文探讨了Go语言在云原生开发中的应用,特别是在Kubernetes和Docker中的使用。Docker利用Go语言的性能和跨平台能力编写Dockerfile和构建镜像。Kubernetes,主要由Go语言编写,提供了方便的客户端库与集群交互。文章列举了Dockerfile编写、Kubernetes资源定义和服务发现的常见问题及解决方案,并给出了Go语言构建Docker镜像和与Kubernetes交互的代码示例。通过掌握这些技巧,开发者能更高效地进行云原生应用开发。
27 1
|
19小时前
|
负载均衡 监控 Go
Golang深入浅出之-Go语言中的服务网格(Service Mesh)原理与应用
【5月更文挑战第5天】服务网格是处理服务间通信的基础设施层,常由数据平面(代理,如Envoy)和控制平面(管理配置)组成。本文讨论了服务发现、负载均衡和追踪等常见问题及其解决方案,并展示了使用Go语言实现Envoy sidecar配置的例子,强调Go语言在构建服务网格中的优势。服务网格能提升微服务的管理和可观测性,正确应对问题能构建更健壮的分布式系统。
17 1
|
1天前
|
消息中间件 Go API
Golang深入浅出之-Go语言中的微服务架构设计与实践
【5月更文挑战第4天】本文探讨了Go语言在微服务架构中的应用,强调了单一职责、标准化API、服务自治和容错设计等原则。同时,指出了过度拆分、服务通信复杂性、数据一致性和部署复杂性等常见问题,并提出了DDD拆分、使用成熟框架、事件驱动和配置管理与CI/CD的解决方案。文中还提供了使用Gin构建HTTP服务和gRPC进行服务间通信的示例。
12 0
|
1天前
|
Prometheus 监控 Cloud Native
Golang深入浅出之-Go语言中的分布式追踪与监控系统集成
【5月更文挑战第4天】本文探讨了Go语言中分布式追踪与监控的重要性,包括追踪的三个核心组件和监控系统集成。常见问题有追踪数据丢失、性能开销和监控指标不当。解决策略涉及使用OpenTracing或OpenTelemetry协议、采样策略以及聚焦关键指标。文中提供了OpenTelemetry和Prometheus的Go代码示例,强调全面可观测性对微服务架构的意义,并提示选择合适工具和策略以确保系统稳定高效。
13 5
|
1天前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
14 0
|
1天前
|
负载均衡 算法 Go
Golang深入浅出之-Go语言中的服务注册与发现机制
【5月更文挑战第4天】本文探讨了Go语言中服务注册与发现的关键原理和实践,包括服务注册、心跳机制、一致性问题和负载均衡策略。示例代码演示了使用Consul进行服务注册和客户端发现服务的实现。在实际应用中,需要解决心跳失效、注册信息一致性和服务负载均衡等问题,以确保微服务架构的稳定性和效率。
11 3
|
1天前
|
中间件 Go
Go语言中的中间件设计与实现
【5月更文挑战第4天】Go语言中的中间件在HTTP请求处理中扮演重要角色,提供了一种插入逻辑层的方式,便于实现日志、认证和限流等功能,而不增加核心代码复杂性。中间件遵循`http.Handler`接口,通过函数组合实现。常见问题包括错误处理(确保中间件能正确处理并传递错误)和请求上下文管理(使用`context.Context`共享数据以避免并发问题)。通过理解中间件机制和最佳实践,可以构建更健壮的Web应用。
16 0
|
3天前
|
前端开发 Go
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
【5月更文挑战第3天】Go语言通过goroutines和channels实现异步编程,虽无内置Future/Promise,但可借助其特性模拟。本文探讨了如何使用channel实现Future模式,提供了异步获取URL内容长度的示例,并警示了Channel泄漏、错误处理和并发控制等常见问题。为避免这些问题,建议显式关闭channel、使用context.Context、并发控制机制及有效传播错误。理解并应用这些技巧能提升Go语言异步编程的效率和健壮性。
14 5
Golang深入浅出之-Go语言中的异步编程与Future/Promise模式
|
3天前
|
监控 负载均衡 算法
Golang深入浅出之-Go语言中的协程池设计与实现
【5月更文挑战第3天】本文探讨了Go语言中的协程池设计,用于管理goroutine并优化并发性能。协程池通过限制同时运行的goroutine数量防止资源耗尽,包括任务队列和工作协程两部分。基本实现思路涉及使用channel作为任务队列,固定数量的工作协程处理任务。文章还列举了一个简单的协程池实现示例,并讨论了常见问题如任务队列溢出、协程泄露和任务调度不均,提出了解决方案。通过合理设置缓冲区大小、确保资源释放、优化任务调度以及监控与调试,可以避免这些问题,提升系统性能和稳定性。
14 6