go 语言实战入门案例之实现Socks5

本文涉及的产品
.cn 域名,1个 12个月
简介: go 语言实战入门案例之实现Socks5

文章和代码已经归档至【Github仓库:https://github.com/timerring/backend-tutorial 】或者公众号【AIShareLab】回复 go 也可获取。

实现 Socks 5 代理

介绍

socks5 协议它虽然是代理协议,但它并不能用来翻,它的协议都是明文传输。
这个协议历史比较久远,诞生于互联网早期。它的用途是, 比如某些企业的内网为了确保安全性,有很严格的防火墙策略,但是带来的副作用就是访问某些资源会很麻烦。socks5 相当于在防火墙开了个口子,让授权的用户可以通过单个端口去访问内部的所有资源。实际上很多翻软件,最终暴露的也是一个 socks5 协议的端口。
例如爬,在爬取过程中很容易会遇到 IP 访问频率超过限制。这个时候很多人就会去网上找一些代理 IP 池,这些代理 IP 池里面的很多代理的协议就是 socks5。

先看一下最终写完的代理服务器的效果。启动程序,然后在浏览器里面配置使用这个代理,此时打开网页。代理服务器的日志,会打印出你访问的网站的域名或者 IP ,这说明我们的网络流量是通过这个代理服务器的。也能在命令行去测试我们的代理服务器。我们可以用 curl -socks5 + 代理服务器地址,后面加一个可访问的 URL,如果代理服务器工作正常的话,那么 curl 命令就会正常返回。

原理

了解 socks5 协议的工作原理。正常浏览器访问一个网站,如果不经过代理服务器的话,就是先和对方的网站建立 TCP 连接,然后三次握手,握手完之后发起 HTTP 请求,然后服务返回 HTTP 响应。

如果设置代理服务器之后,流程会变得复杂一些。首先是浏览器和 socks5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、 relay 阶段

  • 第一个握手阶段,浏览器会向 socks 5 代理发送请求,包的内容包括一个协议的版本号,还有支持的认证的种类,socks 5 服务器会选中一个认证方式,返回给浏览器。如果返回的是 00 的话就代表不需要认证,返回其他类型的话会开始认证流程,这里我们就不对认证流程进行概述了。
  • 第三个阶段是请求阶段,认证通过之后浏览器会对 socks 5 服务器发起请求。主要信息包括版本号,请求的类型,一般主要是 connection 请求,就代表代理服务器要和某个域名或者某个 IP 地址某个端口建立 TCP 连接。代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应。
  • 第四个阶段是 relay 阶段。此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。然后实际上代理服务器并不关心流量的细节,可以是 HTTP 流量,也可以是其它 TCP 流量。

TCP echo server

我们先在 go 里面写一个简单的 TCP echo server。为了方便测试, server 的工作逻辑很简单,你给他发送啥,他就回复啥,代码如下。

package main

import (
    "bufio"
    "log"
    "net"
)

func main() {
   
   
    // 首先在 main 函数里面先用 net.listen 去监听一个端口,会返回一个 server
    server, err := net.Listen("tcp", "127.0.0.1:10803")
    if err != nil {
   
   
        panic(err)
    }
    for {
   
   
        // 然后在一个死循环里面,每次去 accept 一个请求,成功就会返回一个连接
        client, err := server.Accept()
        if err != nil {
   
   
            log.Printf("Accept failed %v", err)
            continue
        }
        // 接下来的话我们在一个 process 函数里面去处理这个连接。
        // 这前面会有个 go 关键字,这个代表启动一个 goroutinue, 可以暂时类比为其他语言里面的启动一个子线程。只是这里的 goroutinue 的开销会比子线程要小很多,可以很轻松地处理上万的并发。
        go process(client)
    }
}
// 接下来是这个 process 函数的实现。
func process(conn net.Conn) {
   
   
    // 首先第一步的话会先加一个 defer connection.close(), defer 是 Golang 里面的一个语法,这一行的含义就是代表在这个函数退出的时候要把这个连接关掉,否则会有资源的泄露。
    defer conn.Close()
    // 用 bufio.NewReader 来创建一个带缓冲的只读流
    reader := bufio.NewReader(conn)
    for {
   
   
        // 带缓冲的流的作用是,可以减少底层系统调用的次数,比如这里为了方便是一个字节一个字节的读取,但是底层可能合并成几次大的读取操作。并且带缓冲的流会有更多的一些工具函数用来读取数据。
        // 可以简单地调用 readbyte 函数来读取单个字节。再把这一个字节写进去连接。
        b, err := reader.ReadByte()
        if err != nil {
   
   
            break
        }
        _, err = conn.Write([]byte{
   
   b})
        if err != nil {
   
   
            break
        }
    }
}

我们来简单测试一下我们的第一个 TCP 服务器,然后测试会需要用到 nc 命令。如果没有的话可以进行安装,这里用 nc 127.0.0.1 10803,输入 timerring 然后服务器就会给你返回 timerring。
先运行代码

然后另开一个终端进行测试:

auth

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
   
   
    server, err := net.Listen("tcp", "127.0.0.1:1080")
    if err != nil {
   
   
        panic(err)
    }
    for {
   
   
        client, err := server.Accept()
        if err != nil {
   
   
            log.Printf("Accept failed %v", err)
            continue
        }
        go process(client)
    }
}

func process(conn net.Conn) {
   
   
    defer conn.Close()
    reader := bufio.NewReader(conn)
    // 我们实现一个空的 auth 函数,在 process 函数里面调用,再来编写 auth 函数的代码。
    err := auth(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    log.Println("auth success")
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
   
   
    // +----+----------+----------+
    // |VER | NMETHODS | METHODS  |
    // +----+----------+----------+
    // | 1  |    1     | 1 to 255 |
    // +----+----------+----------+
    // VER: 协议版本,socks5为0x05
    // NMETHODS: 支持认证的方法数量
    // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
    // X’00’ NO AUTHENTICATION REQUIRED
    // X’02’ USERNAME/PASSWORD

    // 先用 read bytes 来把版本号读出来
    ver, err := reader.ReadByte()
    if err != nil {
   
   
        return fmt.Errorf("read ver failed:%w", err)
    }
    // 然后如果版本号不是 socket 5 的话直接返回报错
    if ver != socks5Ver {
   
   
        return fmt.Errorf("not supported ver:%v", ver)
    }
    // 接下来我们再读取 method size ,也是一个字节。
    methodSize, err := reader.ReadByte()
    if err != nil {
   
   
        return fmt.Errorf("read methodSize failed:%w", err)
    }
    // 然后需要我们去 make 一个相应长度的一个 slice ,用 io.ReadFull 把它去填充进去。
    method := make([]byte, methodSize)
    _, err = io.ReadFull(reader, method)
    if err != nil {
   
   
        return fmt.Errorf("read method failed:%w", err)
    }
    // 写到这里,我们把获取到的版本号和认证方式打印一下。
    log.Println("ver", ver, "method", method)
    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    // 此时,代理服务器还需要返回一个response, 返回包包括 两个字段,一个是 version 一个是 method,也就是我们选中的鉴传方式,我们当前只准备实现不需要鉴传的方式,也就是00。
    _, err = conn.Write([]byte{
   
   socks5Ver, 0x00})
    if err != nil {
   
   
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}

我们回忆一下认证阶段的逻辑,首先第一步的话,浏览器会给代理服务器发送一个包,然后这个包有三个字段,

  • 第一个字段, version 也就是协议版本号,固定是 5
  • 第二个字段 methods,认证的方法数目
  • 第三个字段每个 method 的编码, 0代表不需要认证, 2 代表用户名密码认证
    我们用 curl 命令测试一下当前版本的效果。此时 curl 命令肯定是不成功的,因为我们的协议还没实现完成。

但是我们看日志会发现, version 和 method 可以正常打印,说明当前我们的实现是正确的。

请求阶段

接下来我们开始做第三步,实现请求阶段,我们试图读取到携带 URL 或者 IP 地址+端口的包,然后把它打印出来。

package main

import (
    "bufio"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "log"
    "net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
   
   
    server, err := net.Listen("tcp", "127.0.0.1:1080")
    if err != nil {
   
   
        panic(err)
    }
    for {
   
   
        client, err := server.Accept()
        if err != nil {
   
   
            log.Printf("Accept failed %v", err)
            continue
        }
        go process(client)
    }
}

func process(conn net.Conn) {
   
   
    defer conn.Close()
    reader := bufio.NewReader(conn)
    err := auth(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    // 我们实现一个和 auth 函数类似的 connect 函数,同样在 process 里面去调用。再来实现 connect 函数的代码。
    err = connect(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
   
   
    // +----+----------+----------+
    // |VER | NMETHODS | METHODS  |
    // +----+----------+----------+
    // | 1  |    1     | 1 to 255 |
    // +----+----------+----------+
    // VER: 协议版本,socks5为0x05
    // NMETHODS: 支持认证的方法数量
    // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
    // X’00’ NO AUTHENTICATION REQUIRED
    // X’02’ USERNAME/PASSWORD

    ver, err := reader.ReadByte()
    if err != nil {
   
   
        return fmt.Errorf("read ver failed:%w", err)
    }
    if ver != socks5Ver {
   
   
        return fmt.Errorf("not supported ver:%v", ver)
    }
    methodSize, err := reader.ReadByte()
    if err != nil {
   
   
        return fmt.Errorf("read methodSize failed:%w", err)
    }
    method := make([]byte, methodSize)
    _, err = io.ReadFull(reader, method)
    if err != nil {
   
   
        return fmt.Errorf("read method failed:%w", err)
    }

    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    _, err = conn.Write([]byte{
   
   socks5Ver, 0x00})
    if err != nil {
   
   
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   
   
    // 我们来回忆一下请求阶段的逻辑。浏览器会发送一个包,包里面包含如下6个字段
    // +----+-----+-------+------+----------+----------+
    // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER 版本号,socks5的值为0x05。version 版本号, 还是 5
    // CMD 0x01表示CONNECT请求。CMD 代表请求的类型,我们只支持 connection 请求,也就是让代理服务建立新的TCP连接。
    // RSV 保留字段,值为0x00,不理会。
    // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。可能是 IPV4 IPV6 或者域名。
    //   0x01表示IPv4地址,DST.ADDR为4个字节
    //   0x03表示域名,DST.ADDR是一个可变长度的域名
    // DST.ADDR 一个可变长度的值,这个地址的长度是根据 atype 的类型而不同的,port 端口号,两个字节, 我们需要逐个去读取这些字段。
    // DST.PORT 目标端口,固定2个字节

    // 后面这四个字段总共四个字节,我们可以一次性把它读出来。我们定义一个长度为 4 的 buffer 然后把它读满。
    buf := make([]byte, 4)
    _, err = io.ReadFull(reader, buf)
    if err != nil {
   
   
        return fmt.Errorf("read header failed:%w", err)
    }
    // 读满之后,然后第0 个、第1个、第3个、分别是 version cmd 和 type
    ver, cmd, atyp := buf[0], buf[1], buf[3]
    // version 需要判断是 socket 5
    if ver != socks5Ver {
   
   
        return fmt.Errorf("not supported ver:%v", ver)
    }
    // cmd 需要判断是 1,这里cmdBind是在前面定义好的
    if cmd != cmdBind {
   
   
        return fmt.Errorf("not supported cmd:%v", cmd)
    }
    // 下面的 atype,可能是 ipv4 ,ipv6,或者是 host。
    addr := ""
    switch atyp {
   
   
    // 如果 IPV4 的话,我们再次读满这个 buffer,因为这个 buffer 长度刚好也是4个字节
    case atypeIPV4:
        _, err = io.ReadFull(reader, buf)
        if err != nil {
   
   
            return fmt.Errorf("read atyp failed:%w", err)
        }
        // 然后逐个字节打印成 IP 地址的格式保存到 addr 变量。
        addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
    // 如果是个 host 的话
    case atypeHOST:
        // 需要先读它的长度
        hostSize, err := reader.ReadByte()
        if err != nil {
   
   
            return fmt.Errorf("read hostSize failed:%w", err)
        }
        // 再 make 一个相应长度的buf 填充它。
        host := make([]byte, hostSize)
        _, err = io.ReadFull(reader, host)
        if err != nil {
   
   
            return fmt.Errorf("read host failed:%w", err)
        }
        // 再转换成字符串保存到 addr 变量。
        addr = string(host)
    // IPV6 用得比较少,我们就暂时先不支持。
    case atypeIPV6:
        return errors.New("IPv6: no supported yet")
    default:
        return errors.New("invalid atyp")
    }
    _, err = io.ReadFull(reader, buf[:2])
    if err != nil {
   
   
        return fmt.Errorf("read port failed:%w", err)
    }
    // 最后还有两个字节那个是 port ,我们读取它,然后按协议规定的大端字节序转换成数字。
    // 由于上面的 buffer 已经不会被其他变量使用了,我们可以直接复用之前的内存,建立一个临时的 slice ,长度是2用于读取,这样的话最多会只读两个字节回来。
    port := binary.BigEndian.Uint16(buf[:2])
    // 接下来我们把这个地址和端口打印出来用于调试。
    log.Println("dial", addr, port)
    // 收到浏览器的这个请求包之后,我们需要返回一个包,这个包有很多字段,但其实大部分都不会使用。

    // +----+-----+-------+------+----------+----------+
    // |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER socks版本,这里为0x05,第一个是版本号还是 socket 5。
    // REP Relay field,内容取值如下 X’00’ succeeded,第二个,就是返回的类型,这里是成功就返回0。
    // RSV 保留字段,第三个是保留字段填 0。
    // ATYPE 地址类型,第四个 atype 地址类型填 1。
    // BND.ADDR 服务绑定的地址,第五个,第六个暂时用不到,都填成 0。
    // BND.PORT 服务绑定的端口DST.PORT

    // 一共 4 + 4 + 2 个字节,后面6个字节都是 0 填充。
    _, err = conn.Write([]byte{
   
   0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
    if err != nil {
   
   
        return fmt.Errorf("write failed: %w", err)
    }
    return nil
}

现在我们来测试一下当前阶段的成果, 简单 curl 一下。

此时请求还是会失败,我们现在已经能看到正常打印出来访问的 IP 地址和端口,这说明我们当前的实现正常,这样我们就可以做最后一步,我们真正和这个端口建立连接,双向转发数据。

我们直接用 net.dial 建立一个 TCP 连接,建立完连接之后,我们同样要加一个 defer 来关闭连接。接下来需要建立浏览器和下游服务器的双向数据转发。

标准库的 io.copy 可以实现一个单向数据转发,双向转发的话,需要启动两个 goroutinue。

现在有一个问题,connect 函数会立刻返回,返回的时候连接就被关闭了。需要等待任意一个方向 copy 出错的时候,再返回 connect 函数。这里可以使用到标准库里面的一个 context 机制,用 context 连 with cancel 来创建一个 context。

在最后等待 ctx.Done() ,只要 cancel 被调用, ctx.Done 就会立刻返回。然后在上面的两个 goroutinue 里面调用一次 cancel 即可。

完整代码

package main

import (
    "bufio"
    "context"
    "encoding/binary"
    "errors"
    "fmt"
    "io"
    "log"
    "net"
)

const socks5Ver = 0x05
const cmdBind = 0x01
const atypeIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04

func main() {
   
   
    server, err := net.Listen("tcp", "127.0.0.1:1080")
    if err != nil {
   
   
        panic(err)
    }
    for {
   
   
        client, err := server.Accept()
        if err != nil {
   
   
            log.Printf("Accept failed %v", err)
            continue
        }
        go process(client)
    }
}

func process(conn net.Conn) {
   
   
    defer conn.Close()
    reader := bufio.NewReader(conn)
    err := auth(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
    err = connect(reader, conn)
    if err != nil {
   
   
        log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
        return
    }
}

func auth(reader *bufio.Reader, conn net.Conn) (err error) {
   
   
    // +----+----------+----------+
    // |VER | NMETHODS | METHODS  |
    // +----+----------+----------+
    // | 1  |    1     | 1 to 255 |
    // +----+----------+----------+
    // VER: 协议版本,socks5为0x05
    // NMETHODS: 支持认证的方法数量
    // METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
    // X’00’ NO AUTHENTICATION REQUIRED
    // X’02’ USERNAME/PASSWORD

    ver, err := reader.ReadByte()
    if err != nil {
   
   
        return fmt.Errorf("read ver failed:%w", err)
    }
    if ver != socks5Ver {
   
   
        return fmt.Errorf("not supported ver:%v", ver)
    }
    methodSize, err := reader.ReadByte()
    if err != nil {
   
   
        return fmt.Errorf("read methodSize failed:%w", err)
    }
    method := make([]byte, methodSize)
    _, err = io.ReadFull(reader, method)
    if err != nil {
   
   
        return fmt.Errorf("read method failed:%w", err)
    }

    // +----+--------+
    // |VER | METHOD |
    // +----+--------+
    // | 1  |   1    |
    // +----+--------+
    _, err = conn.Write([]byte{
   
   socks5Ver, 0x00})
    if err != nil {
   
   
        return fmt.Errorf("write failed:%w", err)
    }
    return nil
}

func connect(reader *bufio.Reader, conn net.Conn) (err error) {
   
   
    // +----+-----+-------+------+----------+----------+
    // |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER 版本号,socks5的值为0x05
    // CMD 0x01表示CONNECT请求
    // RSV 保留字段,值为0x00
    // ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
    //   0x01表示IPv4地址,DST.ADDR为4个字节
    //   0x03表示域名,DST.ADDR是一个可变长度的域名
    // DST.ADDR 一个可变长度的值
    // DST.PORT 目标端口,固定2个字节

    buf := make([]byte, 4)
    _, err = io.ReadFull(reader, buf)
    if err != nil {
   
   
        return fmt.Errorf("read header failed:%w", err)
    }
    ver, cmd, atyp := buf[0], buf[1], buf[3]
    if ver != socks5Ver {
   
   
        return fmt.Errorf("not supported ver:%v", ver)
    }
    if cmd != cmdBind {
   
   
        return fmt.Errorf("not supported cmd:%v", cmd)
    }
    addr := ""
    switch atyp {
   
   
    case atypeIPV4:
        _, err = io.ReadFull(reader, buf)
        if err != nil {
   
   
            return fmt.Errorf("read atyp failed:%w", err)
        }
        addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
    case atypeHOST:
        hostSize, err := reader.ReadByte()
        if err != nil {
   
   
            return fmt.Errorf("read hostSize failed:%w", err)
        }
        host := make([]byte, hostSize)
        _, err = io.ReadFull(reader, host)
        if err != nil {
   
   
            return fmt.Errorf("read host failed:%w", err)
        }
        addr = string(host)
    case atypeIPV6:
        return errors.New("IPv6: no supported yet")
    default:
        return errors.New("invalid atyp")
    }
    _, err = io.ReadFull(reader, buf[:2])
    if err != nil {
   
   
        return fmt.Errorf("read port failed:%w", err)
    }
    port := binary.BigEndian.Uint16(buf[:2])

    dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
    if err != nil {
   
   
        return fmt.Errorf("dial dst failed:%w", err)
    }
    defer dest.Close()
    log.Println("dial", addr, port)

    // +----+-----+-------+------+----------+----------+
    // |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
    // +----+-----+-------+------+----------+----------+
    // | 1  |  1  | X'00' |  1   | Variable |    2     |
    // +----+-----+-------+------+----------+----------+
    // VER socks版本,这里为0x05
    // REP Relay field,内容取值如下 X’00’ succeeded
    // RSV 保留字段
    // ATYPE 地址类型
    // BND.ADDR 服务绑定的地址
    // BND.PORT 服务绑定的端口DST.PORT
    _, err = conn.Write([]byte{
   
   0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
    if err != nil {
   
   
        return fmt.Errorf("write failed: %w", err)
    }
    // 现在有一个问题,connect 函数会立刻返回,返回的时候连接就被关闭了。需要等待任意一个方向copy出错的时候,再返回 connect 函数。
    // 可以使用到标准库里面的一个 context 机制,用 context 连 with cancel 来创建一个context。
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    // 然后在两个 goroutinue 里面 调用一次 cancel 即可。
    go func() {
   
   
        _, _ = io.Copy(dest, reader)
        cancel()
    }()
    go func() {
   
   
        _, _ = io.Copy(conn, dest)
        cancel()
    }()
    // 在最后等待 ctx.Done() , 只要 cancel 被调用, ctx.Done就会立刻返回。
    <-ctx.Done()
    return nil
}

我们可以试着在浏览器里面再测试一下,在浏览器里面测试代理需要安装这个 switchomega 插件,然后里面新建一个情景模式,代理服务器选 socks 5,端口 1080 ,保存并启用。此时你应该还能够正常地访问网站,代理服务器这边会显示出浏览器版本的域名和端口。


学习路线推荐


参考:字节内部课 Go 语言原理与实践

目录
相关文章
|
15天前
|
Go 开发工具
百炼-千问模型通过openai接口构建assistant 等 go语言
由于阿里百炼平台通义千问大模型没有完善的go语言兼容openapi示例,并且官方答复assistant是不兼容openapi sdk的。 实际使用中发现是能够支持的,所以自己写了一个demo test示例,给大家做一个参考。
|
15天前
|
程序员 Go
go语言中结构体(Struct)
go语言中结构体(Struct)
92 71
|
14天前
|
存储 Go 索引
go语言中的数组(Array)
go语言中的数组(Array)
100 67
|
7天前
|
Go 数据安全/隐私保护 UED
优化Go语言中的网络连接:设置代理超时参数
优化Go语言中的网络连接:设置代理超时参数
【Go语言入门100题】026 I Love GPLT (5 分) Go语言 | Golang
L1-026 I Love GPLT (5 分) Go语言|Golang 这道超级简单的题目没有任何输入。 你只需要把这句很重要的话 —— “I Love GPLT”——竖着输出就可以了。 所谓“竖着输出”,是指每个字符占一行(包括空格),即每行只能有1个字符和回车。
610 0
|
存储 Go
【Go语言入门100题】023 输出GPLT (20 分) Go语言 | Golang
L1-023 输出GPLT (20 分) Go语言|Golang 给定一个长度不超过10000的、仅由英文字母构成的字符串。请将字符重新调整顺序,按GPLTGPLT....这样的顺序输出,并忽略其它字符。当然,四种字符(不区分大小写)的个数不一定是一样多的,若某种字符已经输出完,则余下的字符仍按GPLT的顺序打印,直到所有字符都被输出。 下面给出甲、乙两人的酒量(最多能喝多少杯不倒)和划拳记录,请你判断两个人谁先倒。
170 0
|
15天前
|
存储 Go 索引
go语言中数组和切片
go语言中数组和切片
26 7
|
17天前
|
Go 索引
go语言for遍历数组或切片
go语言for遍历数组或切片
88 62
|
19天前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
15天前
|
存储 Go
go语言中映射
go语言中映射
32 11