GVKun编程网logo

在Golang中重新切片(golang切片初始化)

9

对于在Golang中重新切片感兴趣的读者,本文将提供您所需要的所有信息,我们将详细讲解golang切片初始化,并且为您提供关于append()表示在golang中只有一个切片字段、epoll在Gola

对于在Golang中重新切片感兴趣的读者,本文将提供您所需要的所有信息,我们将详细讲解golang切片初始化,并且为您提供关于append()表示在golang中只有一个切片字段、epoll在Golang中的应用、Golang append()何时创建新切片?、golang中切片怎么生产的宝贵知识。

本文目录一览:

在Golang中重新切片(golang切片初始化)

在Golang中重新切片(golang切片初始化)

我最近学习了Go语言,现在对以下代码感到困惑:

package mainimport "fmt"func main() {    a := make([]int, 5)    printSlice("a", a)    b := make([]int, 0, 5)    printSlice("b", b)    c := b[:2]    printSlice("c", c)    d := c[2:5]    printSlice("d", d)}func printSlice(s string, x []int) {    fmt.Printf("%s len=%d cap=%d %v\n",        s, len(x), cap(x), x)}

结果:

a len=5 cap=5 [0 0 0 0 0]b len=0 cap=5 []c len=2 cap=5 [0 0] //why the capacity of c not 2 but 5 insteadd len=3 cap=3 [0 0 0]

答案1

小编典典

c是从数组中获取的一个切片b。这不是副本,而只是的两个前两个元素的窗口b。

由于b容量为5,c可以扩展到其他3个位置(实际上,它会创建一个新的切片,但位于内存中的同一位置)。

切片的最大容量是底层数组的容量减去切片在数组中起始位置的大小:

 array : [0 0 0 0 0 0 0 0 0 0 0 0] array :  <----   capacity   ---> slice :     [0 0 0 0] slice :      <---- capacity --->  ```也许此程序将使c和d仅仅是b上的窗口:

func main() {
b := make([]int, 0, 5)
c := b[:2]
d := c[1:5] // this is equivalent to d := b[1:5]
d[0] = 1
printSlice(“c”, c)
printSlice(“d”, d)
}

输出:

c len=2 cap=5 [0 1] // modifying d has modified c
d len=4 cap=4 [1 0 0 0]
```

append()表示在golang中只有一个切片字段

append()表示在golang中只有一个切片字段

我想将一个元素附加到一个只包含一个匿名切片的结构:

package main

type List []Element

type Element struct {
    Id string
}

func (l *List) addElement(id string) {
    e := &Element{
        Id: id,}
    l = append(l,e)
}

func main() {
    list := List{}
    list.addElement("test")
}

这不起作用,因为addElement不知道l作为切片而是作为* List:

go run plugin.go
# command-line-arguments
./plugin.go:13: first argument to append must be slice; have *List

最有可能的是这样:

type List struct {
    elements []Element
}

并相应地修复addElement func.我有一个比这更好的方法,例如.让我保留List类型的第一个定义的一个?

非常感谢,sontags

解决方法

两个问题,

>您将* Element附加到[] Element,使用Element {}或将列表更改为[] * Element.
>您需要在addElement中取消引用切片.

Example:

func (l *List) addElement(id string) {
    e := Element{
        Id: id,}
    *l = append(*l,e)
}

epoll在Golang中的应用

epoll在Golang中的应用

使用Golang可以轻松地为每一个TCP连接创建一个协程去服务而不用担心性能问题,这是因为Go内部使用goroutine结合IO多路复用实现了一个“异步”的IO模型,这使得开发者不用过多的关注底层,而只需要按照需求编写上层业务逻辑。这种异步的IO是如何实现的呢?下面我会针对Linux系统进行分析。

在Unix/Linux系统下,一切皆文件,每条TCP连接对应了一个socket句柄,这个句柄也可以看做是一个文件,在socket上收发数据,相当于对一个文件进行读写,所以一个socket句柄,通常也用表示文件描述符fd来表示。可以进入/proc/PID/fd/查看进程占用的fd。

系统内核会为每个socket句柄分配一个读(接收)缓冲区和一个写(发送)缓冲区,发送数据就是在这个fd对应的写缓冲区上写数据,而接收数据就是在读缓冲区上读数据,当程序调用write或者send时,并不代表数据发送出去,仅仅是把数据拷贝到了写缓冲区,在时机恰当时候(积累到一定数量),会将数据发送到目的端。

Golang runtime还是需要频繁去检查是否有fd就绪的,严格说并不算真正的异步,算是一种非阻塞IO复用。

IO模型

    借用教科书中几张图

阻塞式IO

程序想在缓冲区读数据时,缓冲区并不一定会有数据,这会造成陷入系统调用,只能等待数据可以读取,没有数据读取时则会阻塞住进程,这就是阻塞式IO。当需要为多个客户端提供服务时,可以使用线程方式,每个socket句柄使用一个线程来服务,这样阻塞住的则是某个线程。虽然如此可以解决进程阻塞,但是还是会有相当一部分CPU资源浪费在了等待数据上,同时,使用线程来服务fd有些浪费资源,因为如果要处理的fd较多,则又是一笔资源开销。

image.png

非阻塞式IO

与之对应的是非阻塞IO,当程序想要读取数据时,如果缓冲区不存在,则直接返回给用户程序,但是需要用户程序去频繁检查,直到有数据准备好。这同样也会造成空耗CPU。

image.png

IO多路复用

而IO多路复用则不同,他会使用一个线程去管理多个fd,可以将多个fd加入IO多路复用函数中,每次调用该函数,传入要检查的fd,如果有就绪的fd,直接返回就绪的fd,再启动线程处理或者顺序处理就绪的fd。这达到了一个线程管理多个fd任务,相对来说较为高效。常见的IO多路复用函数有select,poll,epoll。select与poll的最大缺点是每次调用时都需要传入所有要监听的fd集合,内核再遍历这个传入的fd集合,当并发量大时候,用户态与内核态之间的数据拷贝以及内核轮询fd又要浪费一波系统资源(关于select与poll这里不展开)。

image.png

epoll介绍

接下来介绍一下epoll系统调用

epoll相比于select与poll相比要灵活且高效,他提供给用户三个系统调用函数。Golang底层就是通过这三个系统调用结合goroutine完成的“异步”IO。

//用于创建并返回一个epfd句柄,后续关于fd的添加删除等操作都依据这个句柄。
int epoll_create(int size);
//用于向epfd添加,删除,修改要监听的fd。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
//传入创建返回的epfd句柄,以及超时时间,返回就绪的fd句柄。
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  • 调用epoll_create会在内核创建一个eventpoll对象,这个对象会维护一个epitem集合,可简单理解为fd集合。
  • 调用epoll_ctl函数用于将fd封装成epitem加入这个eventpoll对象,并给这个epitem加了一个回调函数注册到内核,会在这个fd状态改变时候触发,使得该epitem加入eventpoll的就绪列表rdlist。
  • 当相应数据到来,触发中断响应程序,将数据拷贝到fd的socket缓冲区,fd缓冲区状态发生变化,回调函数将fd对应的epitem加入rdlist就绪队列中。
  • 调用epoll_wait时无需遍历,只是返回了这个就绪的rdlist队列,如果rdlist队列为空,则阻塞等待或等待超时时间的到来。

大致工作原理如图

image.png

异步IO

当用户程序想要读取fd数据时,系统调用直接通知到内核并返回处理其他的事情,内核将数据准备好之后,通知用户程序,用户程序再处理这个fd上的事件。

image.png

Golang异步IO实现思路

我们都知道,协程的资源占有量很小,而且协程也拥有多种状态如阻塞,就绪,运行等,可以使用一个协程服务一个fd不用担心资源问题。将监听fd的事件交由runtime来管理,实现协程调度与依赖fd的事件。当要协程读取fd数据但是没有数据时,park住该协程(改为Gwaiting),调度其他协程执行。

在执行协程调度时候,去检查fd是否就绪,如果就绪时,调度器再通知该park住的协程fd可以处理了(改为Grunnable并加入执行队列),该协程处理fd数据,这样既减少了CPU的空耗,也实现了消息的通知,用户层面上看实现了一个异步的IO模型。

image.png

Golang netpoll的大致思想就是这样,接下来看一下具体代码实现,本文基于go1.14。

具体实现

接下来看下Golang netpoll对其的使用。

实验案例

跟随一个很简单的demo探索一下。

func main() {
  fmt.Println("服务端进程id:",os.Getpid())
  lister, err := net.Listen("tcp", "0.0.0.0:9009")
  if err != nil {
    fmt.Println("连接失败", err)
    return
  }
  for {
    conn, err := lister.Accept() //等待建立连接
    if err != nil {
      fmt.Println("建立连接失败", err)
      continue
    }
     //开启协程处理
    go func() {
      defer conn.Close()
      for {
        buf := make([]byte, 128)
        n, err := conn.Read(buf)
        if err != nil{
          fmt.Println("读出错",err)
          return
        }
        fmt.Println("读取到的数据:",string(buf[:n]))
      }
    }()
  }
}

net.Listen的内部调用

net.Listen依次调用lc.Listen->sl.listenTCP->internetSocket->socket到fd.listenStream函数创建了一个监听9009的tcp连接的socket接口,也就是创建了socket fd,

接下来为了监听该socket对象就需要把这个socket fd加入到eventpoll中了。

func (fd *netFD) listenStream(laddr sockaddr, backlog int, ctrlFn func(string, string, syscall.RawConn) error) error {
    ......
  //绑定该socket接口
  if err = syscall.Bind(fd.pfd.Sysfd, lsa); err != nil {
    return os.NewSyscallError("bind", err)
  }
  //监听该socket
  if err = listenFunc(fd.pfd.Sysfd, backlog); err != nil {
    return os.NewSyscallError("listen", err)
  }
  //初始化fd,也就是把socket放入epoll中,进入
  if err = fd.init(); err != nil {
    return err
  }
  lsa, _ = syscall.Getsockname(fd.pfd.Sysfd)
  fd.setAddr(fd.addrFunc()(lsa), nil)
  return nil
}
func (fd *FD) Init(net string, pollable bool) error {
  ......
  //将socket fd加到poll,进入
  err := fd.pd.init(fd)
  ......
  return err
}
//最终跳转到该处,主要关注两个函数runtime_pollServerInit,runtime_pollOpen,
//这两个函数都是runtime实现的,将epoll交由runtime来管理
func (pd *pollDesc) init(fd *FD) error {
  //sync.once方法,调用epoll_create创建eventpoll对象
  serverInit.Do(runtime_pollServerInit)
  //将当前的fd加到epoll中,底层调用epollctl函数
  ctx, errno := runtime_pollOpen(uintptr(fd.Sysfd))
  //如果出错,处理相应的fd,删除epoll中fd以及解除状态等操作
  if errno != 0 {
    if ctx != 0 {
      runtime_pollUnblock(ctx)
      runtime_pollClose(ctx)
    }
    return errnoErr(syscall.Errno(errno))
  }
  pd.runtimeCtx = ctx
  return nil
}

查看runtime_pollServerInit,是对epoll_create的封装。

func poll_runtime_pollServerInit() {
  //初始化全局epoll对象
  netpollinit()
  /全局标志位设置为1
  atomic.Store(&netpollInited, 1)
}
func netpollinit() {
  //系统调用,创建一个eventpoll对象
  epfd = epollcreate1(_EPOLL_CLOEXEC)
  if epfd >= 0 {
    return
  }
  ......
}

查看一下runtime_pollOpen方法,将当前监听的socket fd加入eventpoll对象中。实际上是对epoll_ctl的封装。

func poll_runtime_pollOpen(fd uintptr) (*pollDesc, int) {
  //返回一个存储在Go程序中的一个fd对应的结构体,算是用于记录
  //goroutine与fd之间的关系,后面会分析到
  pd := pollcache.alloc()
  //加锁,防止并发问题
  lock(&pd.lock)
  if pd.wg != 0 && pd.wg != pdReady {
    throw("runtime: blocked write on free polldesc")
  }
  if pd.rg != 0 && pd.rg != pdReady {
    throw("runtime: blocked read on free polldesc")
  }
  pd.fd = fd
  pd.closing = false
  pd.everr = false
  pd.rseq++
  pd.rg = 0
  pd.rd = 0
  pd.wseq++
  pd.wg = 0
  pd.wd = 0
  unlock(&pd.lock)
  var errno int32
  //epoll_ctl系统调用
  errno = netpollopen(fd, pd)
  return pd, int(errno)
}
func netpollopen(fd uintptr, pd *pollDesc) int32 {
  var ev epollevent
  //注册event事件,这里使用了epoll的ET模式,相对于ET,ET需要每次产生事件时候就要处理事件,
  //否则容易丢失事件。
  ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
  //events记录上pd的指针
  *(**pollDesc)(unsafe.Pointer(&ev.data)) = pd
  //系统调用将该fd加到eventpoll对象中,交由内核监听
  return -epollctl(epfd, _EPOLL_CTL_ADD, int32(fd), &ev)
}

Accept的内部调用

接下来返回到主函数。

func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
  ......
   //检查fd状态是否变化
  if err := fd.pd.prepareRead(fd.isFile); err != nil {
    return -1, nil, "", err
  }
  for {
    //accept系统调用,如果有对监听的socket的连接请求,则直接返回发起连接的socket文件描述符
    //,否则返回EAGAIN错误,被下面捕获到
    s, rsa, errcall, err := accept(fd.Sysfd)
    if err == nil {
      return s, rsa, "", err
    }
    switch err {
    case syscall.EAGAIN:
      if fd.pd.pollable() {
         //进入waitRead方法,内部
        if err = fd.pd.waitRead(fd.isFile); err == nil {
          continue
        }
      }
    case syscall.ECONNABORTED:
      continue
    }
    return -1, nil, errcall, err
  }
}
func (pd *pollDesc) wait(mode int, isFile bool) error {
  if pd.runtimeCtx == 0 {
    return errors.New("waiting for unsupported file type")
  }
   //进入runtime_pollWait方法内部,该方法会跳转到runtime包下,条件满足会park住goroutine
  res := runtime_pollWait(pd.runtimeCtx, mode)
  return convertErr(res, isFile)
}
func poll_runtime_pollWait(pd *pollDesc, mode int) int {
  ......
   //进入netpollblock函数,该函数内部会阻塞住该goroutine
  for !netpollblock(pd, int32(mode), false) {
    err = netpollcheckerr(pd, int32(mode))
    if err != 0 {
      return err
    }
  }
  return 0
}
func netpollblock(pd *pollDesc, mode int32, waitio bool) bool {
  gpp := &pd.rg
  if mode == ''w'' {
    gpp = &pd.wg
  }
    ......
  if waitio || netpollcheckerr(pd, mode) == 0 {
    //gark住该g,此时传参主要关注前两个,一个netpollblockcommit函数,一个gpp为当前pd的rg或者wg,
    //用于后面记录fd对应的阻塞的goroutine
    gopark(netpollblockcommit, unsafe.Pointer(gpp), waitReasonIOWait, traceEvGoBlockNet, 5)
  }
  old := atomic.Xchguintptr(gpp, 0)
  if old > pdWait {
    throw("runtime: corrupted polldesc")
  }
  return old == pdReady
}
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
  ......
  //主要关注两个传参,lock是gpp指针
  mp.waitlock = lock
  //unlockf为netpollblockcommit函数
  mp.waitunlockf = unlockf
    ......
  //切换到g0栈去执行park_m
  mcall(park_m)
}
func park_m(gp *g) {
  //获取当前goroutine
  _g_ := getg()
  //修改状态为Gwaiting,代表当前的goroutine被park住了
  casgstatus(gp, _Grunning, _Gwaiting)
  //解除m和g关联
  dropg()
  if fn := _g_.m.waitunlockf; fn != nil {
     //调用刚传入的函数参数,也就是netpollblockcommit
    ok := fn(gp, _g_.m.waitlock)
     //调用完清除
    _g_.m.waitunlockf = nil
    _g_.m.waitlock = nil
    if !ok {
      if trace.enabled {
        traceGoUnpark(gp, 2)
      }
      casgstatus(gp, _Gwaiting, _Grunnable)
      execute(gp, true) // Schedule it back, never returns.
    }
  }
  //调度新的g到m上来
  schedule()
}
func netpollblockcommit(gp *g, gpp unsafe.Pointer) bool {
  //把当前g的指针存为gpp指针,gpp为pd的rg或wg
  r := atomic.Casuintptr((*uintptr)(gpp), pdWait, uintptr(unsafe.Pointer(gp)))
  if r {
    //将全局变量改为1,代表系统有netpoll的等待者
    atomic.Xadd(&netpollWaiters, 1)
  }
  return r
}

到此时,accept函数就被阻塞住了,系统会在这个监听的socket fd事件(0.0.0.0:9009的这个fd)的状态发生变化时候(也就是有新的客户端请求连接的时候),将该park住的goroutine给ready。

//上面提到过的accept函数,根据序号顺序分析
func (fd *FD) Accept() (int, syscall.Sockaddr, string, error) {
    ......
  for {
    //2.使用accept系统调用能获取到新的连接,linux会为新的连接分配一个新的fd,
    //这个函数会返回新的连接的socket fd对应的进程描述符
    s, rsa, errcall, err := accept(fd.Sysfd)
    if err == nil {
      //3.返回新的进程描述符
      return s, rsa, "", err
    }
    switch err {
    case syscall.EAGAIN:
      if fd.pd.pollable() {
         //1.刚才阻塞到了这个goroutine,后来新的连接请求,该goroutine被唤醒
        if err = fd.pd.waitRead(fd.isFile); err == nil {
          continue
        }
      }
    ......
    }
        ......
  }
}
//返回上一层的函数
func (fd *netFD) accept() (netfd *netFD, err error) {
    //此时获取到了新的fd
  d, rsa, errcall, err := fd.pfd.Accept()
  ......
  //创建新的fd结构体
  if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
    poll.CloseFunc(d)
    return nil, err
  }
  //init函数又会进入func (pd *pollDesc) init(fd *FD) error函数,并将新的socket连接通过epoll_ctl传入
  //epoll的监听事件
  if err = netfd.init(); err != nil {
    fd.Close()
    return nil, err
  }
  //系统调用,可以获得客户端的socket的ip信息等
  lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd)
  netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))
  return netfd, nil
}

唤醒park住的协程

go会在调度goroutine时候执行epoll_wait系统调用,检查是否有状态发生改变的fd,有的话就把他取出,唤醒对应的goroutine去处理。该部分对应了runtime中的netpoll方法。

源码调用runtime中的schedule() -> findrunnable() -> netpoll()

func findrunnable() (gp *g, inheritTime bool) {
  _g_ := getg()
   //分别从本地队列和全局队列寻找可执行的g
  ......
  //判断是否满足条件,初始化netpoll对象,是否等待者,以及上次调用时间
  if netpollinited() && atomic.Load(&netpollWaiters) > 0 && atomic.Load64(&sched.lastpoll) != 0 {
    //netpoll底层调用epoll_wait,传参代表epoll_wait时候是阻塞等待或者非阻塞直接返回
    //这里是非阻塞模式,会立即返回内核eventpoll对象的rdlist列表
    if list := netpoll(false); !list.empty() {
      gp := list.pop()
       //将可运行G的列表注入调度程序并清除glist
      injectglist(&list)
       //修改gp状态
      casgstatus(gp, _Gwaiting, _Grunnable)
      if trace.enabled {
        traceGoUnpark(gp, 0)
      }
            //返回可运行的g
      return gp, false
    }
  }
    .......
  stopm()
  goto top
}
//对epoll_wait的进一步封装
func netpoll(block bool) gList {
  if epfd == -1 {
    return gList{}
  }
  waitms := int32(-1)
  if !block {
    waitms = 0
  }
  //声明一个epollevent事件,在epoll_wait系统调用时候,会给该数组赋值并返回一个索引位,
  /之后可以遍历数组取出就绪的fd事件。
  var events [128]epollevent
retry:
  //陷入系统调用,取出内核eventpoll中的rdlist,返回就绪的事件
  n := epollwait(epfd, &events[0], int32(len(events)), waitms)
  if n < 0 {
    if n != -_EINTR {
      println("runtime: epollwait on fd", epfd, "failed with", -n)
      throw("runtime: netpoll failed")
    }
    goto retry
  }
  var toRun gList
  //遍历event事件数组
  for i := int32(0); i < n; i++ {
    ev := &events[i]
    if ev.events == 0 {
      continue
    }
    var mode int32
    //是否有就绪的读写事件,放入mode标志位
    if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
      mode += ''r''
    }
    if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
      mode += ''w''
    }
    if mode != 0 {
      //取出存入的pollDesc的指针
      pd := *(**pollDesc)(unsafe.Pointer(&ev.data))
      pd.everr = false
      if ev.events == _EPOLLERR {
        pd.everr = true
      }
      //取出pd中的rg或wg,后面放到运行队列
      netpollready(&toRun, pd, mode)
    }
  }
  if block && toRun.empty() {
    goto retry
  }
  return toRun
}
func netpollready(toRun *gList, pd *pollDesc, mode int32) {
  var rg, wg *g
  if mode == ''r'' || mode == ''r''+''w'' {
    rg = netpollunblock(pd, ''r'', true)
  }
  if mode == ''w'' || mode == ''r''+''w'' {
    wg = netpollunblock(pd, ''w'', true)
  }
    //将阻塞的goroutine加入gList返回
  if rg != nil {
    toRun.push(rg)
  }
  if wg != nil {
    toRun.push(wg)
  }
}

conn.Read的内部调用

回到主函数,我们使用go func形式使用一个协程去处理一个tcp连接,每个协程里面会有conn.Read,该函数在读取时候如果缓冲区不可读,该goroutine也会陪park住,等待socket fd可读,调度器通过netpoll函数调度它。

func main() {
  ......
  //开启处理
    go func() {
      defer conn.Close()
      for {
        buf := make([]byte, 128)
        //将缓冲区的数据读出来放到buf中
        n, err := conn.Read(buf)
            ......
      }
    }()
  }
}
func (fd *FD) Read(p []byte) (int, error) {
  ......
  for {
    //系统调用读取缓冲区数据,这里没有可读会直接返回,不会阻塞
    n, err := syscall.Read(fd.Sysfd, p)
    if err != nil {
      n = 0
      if err == syscall.EAGAIN && fd.pd.pollable() {
        //不可读,进入waitRead方法,park住该goroutine,
        //并记录goroutine到pd的rg中,等待唤醒
        if err = fd.pd.waitRead(fd.isFile); err == nil {
          continue
        }
      }
    }
    ......
  }
}

后面会等待缓冲区可读写,shchedule函数调用netpoll并进一步调用epoll_wait检测到并唤醒该goroutine。可以查看上面netpoll,这里不做重复工作了。

Golang也提供了对于epoll item节点的删除操作,具体封装函数poll_runtime_pollClose
//当发生某些情况,如连接断开,fd销毁等,会调用到此处
func poll_runtime_pollClose(pd *pollDesc) {
  .......
  netpollclose(pd.fd)
  //释放对应的pd
  pollcache.free(pd)
}
//调用epoll_ctl系统调用,删除该fd在eventpoll上对应的epitem
func netpollclose(fd uintptr) int32 {
  var ev epollevent
  return -epollctl(epfd, _EPOLL_CTL_DEL, int32(fd), &ev)
}

部分系统调用

抓了一部分系统调用分析一下上述程序与内核交互的大致过程。

$ strace -f ./server

部分系统调用函数如下。

#....省略内存管理部分以及线程管理部分
#执行到fmt.Println("服务端进程id:",os.Getpid())
[pid 30307] getpid() = 30307
[pid 30307] write(1, "346234215345212241347253257350277233347250213id357274232 30307n", 27服务端进程id:30307
) = 27
......由于过多,省略关于socket的系统调用
[pid 30308] <... nanosleep resumed> NULL) = 0
#打开系统文件,该文件定义tcp最大连接数,会被设置成pollable,并加入epoll节点中
[pid 30307] openat(AT_FDCWD, "/proc/sys/net/core/somaxconn", O_RDONLY|O_CLOEXEC <unfinished ...>
[pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
[pid 30307] <... openat resumed> ) = 4
#调用epoll_ctl,创建一个eventpoll
[pid 30307] epoll_create1(EPOLL_CLOEXEC) = 5
#将fd加到epoll事件
[pid 30307] epoll_ctl(5, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2174189320, u64=139635855949576}}) = 0
[pid 30307] fcntl(4, F_GETFL) = 0x8000 (flags O_RDONLY|O_LARGEFILE)
[pid 30307] fcntl(4, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0
[pid 30308] <... nanosleep resumed> NULL) = 0
[pid 30307] read(4, <unfinished ...>
#执行epoll_wait查看就绪事件
[pid 30308] epoll_pwait(5, <unfinished ...>
[pid 30307] <... read resumed> "512n", 65536) = 4
[pid 30308] <... epoll_pwait resumed> [{EPOLLIN|EPOLLOUT, {u32=2174189320, u64=139635855949576}}], 128, 0, NULL, 139635812673280) = 1
[pid 30307] read(4, <unfinished ...>
[pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
[pid 30307] <... read resumed> "", 65532) = 0
#将/proc/sys/net/core/somaxconn文件的fd从epoll中删除
[pid 30307] epoll_ctl(5, EPOLL_CTL_DEL, 4, 0xc00005e8d4) = 0
#关掉打开的somaxconn描述符
[pid 30307] close(4) = 0
#设置监听的socket描述符
[pid 30307] setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
[pid 30307] bind(3, {sa_family=AF_INET6, sin6_port=htons(9009), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
[pid 30307] listen(3, 512 <unfinished ...>
[pid 30308] <... nanosleep resumed> NULL) = 0
[pid 30307] <... listen resumed> ) = 0
[pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
#将用于监听的socket fd加入到epoll中
[pid 30307] epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2174189320, u64=139635855949576}}) = 0
[pid 30307] getsockname(3, {sa_family=AF_INET6, sin6_port=htons(9009), inet_pton(AF_INET6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28]) = 0
#执行accept4发现没有连接,返回EAGAIN错误
[pid 30307] accept4(3, 0xc00005eb98, [112], SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)
#查看是否有就绪的fd,此次调用是非阻塞,立即返回
[pid 30307] epoll_pwait(5, [], 128, 0, NULL, 0) = 0
[pid 30308] <... nanosleep resumed> NULL) = 0
#查看是否有就绪的fd,此次会阻塞等待,直到有连接进来
[pid 30307] epoll_pwait(5, <unfinished ...>
[pid 30308] futex(0x60dc70, FUTEX_WAIT_PRIVATE, 0, {tv_sec=60, tv_nsec=0} <unfinished ...>
[pid 30307] <... epoll_pwait resumed> [{EPOLLIN, {u32=2174189320, u64=139635855949576}}], 128, -1, NULL, 0) = 1
[pid 30307] futex(0x60dc70, FUTEX_WAKE_PRIVATE, 1) = 1
[pid 30308] <... futex resumed> ) = 0
#新的连接,代表收到了一个客户端连接,分配了一个fd是4
[pid 30307] accept4(3, <unfinished ...>, <... accept4 resumed> {sa_family=AF_INET6, sin6_port=htons(52082), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28], SOCK_CLOEXEC|SOCK_NONBLOCK) = 4
#把4加入到epoll中管理
[pid 30307] epoll_ctl(5, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2174189112, u64=139635855949368}}) = 0
[pid 30307] getsockname(4, {sa_family=AF_INET6, sin6_port=htons(9009), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [112->28]) = 0
......
#后来将client端关掉,此时tcp连接断掉了,将epoll中的fd移除
[pid 30309] epoll_ctl(5, EPOLL_CTL_DEL, 4, 0xc00005fdd4 <unfinished ...>
[pid 30308] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...>
[pid 30309] <... epoll_ctl resumed> ) = 0
[pid 30309] close(4) = 0
[pid 30309] epoll_pwait(5, [], 128, 0, NULL, 824634114048) = 0
#阻塞等待
[pid 30309] epoll_pwait(5, <unfinished ...>
........

参考资料

  • 《后台开发核心技术与应用实践》第七章:网络IO模型
  • 《Unix环境高级编程》第十四章:高级IO
  • 《Go语言设计与实现》https://draveness.me/golang/d...
  • 《Go netpoller 原生网络模型之源码全面揭秘》https://mp.weixin.qq.com/s/3kqVry3uV6BeMei8WjGN4g

Golang append()何时创建新切片?

Golang append()何时创建新切片?

根据内置的api docs,当原始切片的容量不够大时,append()将重新分配并复制到新的数组块。

这是递归算法(的简化版本),用于创建字母(在这种情况下为布尔值)的组合。字母表中的成员(真,假)将递归地添加到切片中,直到其长度正确为止,然后通过通道将其发送出去。

package main

import (
    "fmt"
)

func AddOption(c chan []bool,combo []bool,length int) {
    if length == 0 {
        fmt.Println(combo,"!")
        c <- combo
        return
    }
    var newCombo []bool
    for _,ch := range []bool{true,false} {
        newCombo = append(combo,ch)
        AddOption(c,newCombo,length-1)
    }
}

func main() {
    c := make(chan []bool)
    go func(c chan []bool) {
        defer close(c)
        AddOption(c,[]bool{},4)
    }(c)
    for combination := range c {
        fmt.Println(combination)
    }
}

这是此代码的操场链接。在输出中:

[true true true true] !
[true true true false] !
[true true true false]
[true true true false]
[true true false true] !
[true true false false] !
[true true false false]
[true true false false]
[true false true true] !
[true false true false] !
[true false true false]
[true false true false]
[true false false true] !
[true false false false] !
[true false false false]
[true false false false]
[false true true true] !
[false true true false] !
[false true true false]
[false true true false]
[false true false true] !
[false true false false] !
[false true false false]
[false true false false]
[false false true true] !
[false false true false] !
[false false true false]
[false false true false]
[false false false true] !
[false false false false] !
[false false false false]
[false false false false]

以感叹号结尾的行是从AddOption发送到通道的行。那些没有的东西则出现在另一端(即main()中)。显然,通过通道发送的切片在发送后会更改。

由于AddOption在发送切片后立即返回,因此修改必须来自代码块

var newCombo []bool
for _,false} {
    newCombo = append(combo,ch)
    AddOption(c,length-1)
}

但是,根据文档,append()应该返回一个新的切片(cap(combo)不够大)。根据这个答案,发送给AddOption的slice描述符应该是一个副本。那不是真的吗?据我所知,作为AddOption()的第二个参数发送的值要么是指向切片描述符的指针,要么append()没有返回新的切片。

golang中切片怎么生产

golang中切片怎么生产

本篇内容主要讲解“golang中切片怎么生产”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“golang中切片怎么生产”吧!

在golang中,切片是对数组的一个连续片段的引用,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集。Go语言中切片的内部结构包含地址、大小和容量,切片一般用于快速地操作一块数据集合,如果将数据集合比作切糕的话,切片就是你要的“那一块”,切的过程包含从哪里开始(切片的起始位置)及切多大(切片的大小),容量可以理解为装切片的口袋大小。

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

Go语言中切片的内部结构包含地址、大小和容量,切片一般用于快速地操作一块数据集合,如果将数据集合比作切糕的话,切片就是你要的“那一块”,切的过程包含从哪里开始(切片的起始位置)及切多大(切片的大小),容量可以理解为装切片的口袋大小,如下图所示。

golang中切片怎么生产


图:切片结构和内存分配

从数组或切片生成新的切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。

从连续内存区域生成切片是常见的操作,格式如下:

slice [开始位置 : 结束位置]

语法说明如下:

  • slice:表示目标切片对象;

  • 开始位置:对应目标切片对象的索引;

  • 结束位置:对应目标切片的结束索引。

从数组生成切片,代码如下:

var a  = [3]int{1, 2, 3}
fmt.Println(a, a[1:2])

其中 a 是一个拥有 3 个整型元素的数组,被初始化为数值 1 到 3,使用 a[1:2] 可以生成一个新的切片,代码运行结果如下:

[1 2 3]  [2]

其中 [2] 就是 a[1:2] 切片操作的结果。

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;

  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;

  • 当缺省开始位置时,表示从连续区域开头到结束位置;

  • 当缺省结束位置时,表示从开始位置到整个连续区域末尾;

  • 两者同时缺省时,与切片本身等效;

  • 两者同时为 0 时,等效于空切片,一般用于切片复位。

根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。

下面通过实例来熟悉切片的特性。

1) 从指定范围中生成切片

切片和数组密不可分,如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者,出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片,示例代码如下:

var highRiseBuilding [30]int
for i := 0; i < 30; i++ {
        highRiseBuilding[i] = i + 1
}
// 区间
fmt.Println(highRiseBuilding[10:15])
// 中间到尾部的所有元素
fmt.Println(highRiseBuilding[20:])
// 开头到中间指定位置的所有元素
fmt.Println(highRiseBuilding[:2])

代码输出如下:

golang中切片怎么生产

代码中构建了一个 30 层的高层建筑,数组的元素值从 1 到 30,分别代表不同的独立楼层,输出的结果是不同的租售方案。

代码说明如下:

  • 第 8 行,尝试出租一个区间楼层。

  • 第 11 行,出租 20 层以上。

  • 第 14 行,出租 2 层以下,一般是商用铺面。

切片有点像C语言里的指针,指针可以做运算,但代价是内存操作越界,切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大。

2) 表示原有的切片

生成切片的格式中,当开始和结束位置都被忽略时,生成的切片将表示和原切片一致的切片,并且生成的切片与原切片在数据内容上也是一致的,代码如下:

a := []int{1, 2, 3}
fmt.Println(a[:])

a 是一个拥有 3 个元素的切片,将 a 切片使用 a[:] 进行操作后,得到的切片与 a 切片一致,代码输出如下:

golang中切片怎么生产

3) 重置切片,清空拥有的元素

把切片的开始和结束位置都设为 0 时,生成的切片将变空,代码如下:

a := []int{1, 2, 3}
fmt.Println(a[0:0])

代码输出如下:

golang中切片怎么生产

直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:

var name []Type

登录后复制

其中 name 表示切片的变量名,Type 表示切片对应的元素类型。

下面代码展示了切片声明的使用过程:

// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListempty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListempty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListempty))
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListempty == nil)

代码输出结果:

golang中切片怎么生产

代码说明如下:

  • 第 2 行,声明一个字符串切片,切片中拥有多个字符串。

  • 第 5 行,声明一个整型切片,切片中拥有多个整型数值。

  • 第 8 行,将 numListempty 声明为一个整型切片,本来会在{}中填充切片的初始化元素,这里没有填充,所以切片是空的,但是此时的 numListempty 已经被分配了内存,只是还没有元素。

  • 第 11 行,切片均没有任何元素,3 个切片输出元素内容均为空。

  • 第 14 行,没有对切片进行任何操作,strList 和 numList 没有指向任何数组或者其他切片。

  • 第 17 行和第 18 行,声明但未使用的切片的默认值是 nil,strList 和 numList 也是 nil,所以和 nil 比较的结果是 true。

  • 第 19 行,numListempty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 false。

切片是动态结构,只能与 nil 判定相等,不能互相判定相等。声明新的切片后,可以使用 append() 函数向切片中添加元素。

使用 make() 函数构造切片

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

make( []Type, size, cap )

其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。

示例如下:

a := make([]int, 2)
b := make([]int, 2, 10)

fmt.Println(a, b)
fmt.Println(len(a), len(b))

代码输出如下:

golang中切片怎么生产

其中 a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。

容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。

温馨提示

使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

到此,相信大家对“golang中切片怎么生产”有了更深的了解,不妨来实际操作一番吧!这里是小编网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

关于在Golang中重新切片golang切片初始化的介绍现已完结,谢谢您的耐心阅读,如果想了解更多关于append()表示在golang中只有一个切片字段、epoll在Golang中的应用、Golang append()何时创建新切片?、golang中切片怎么生产的相关知识,请在本站寻找。

本文标签: