如何在 Golang 中更快地进行 api 调用?

How to make an api call faster in Golang?

我正在尝试使用公司 api 将一堆文件上传到他们提供的存储服务。 (基本上是我的帐户)。我有很多文件,比如 40-50 之类的。 我得到了文件的完整路径并利用 os.Open,这样我就可以通过 io.Reader。我确实尝试过在没有 goroutines 的情况下使用 client.Files.Upload(),但是上传它们花了很多时间并决定使用 goroutines。这是我尝试过的实现。当我 运行 程序时,它只上传一个文件,该文件是最小的文件或等待很长时间的文件。它有什么问题?不是每次循环 运行 都会创建一个 goroutine 继续它的循环并为每个 file 创建吗?如何使用 goroutines 使其尽可能快?

var filePaths []string
var wg sync.WaitGroup

// fills the string of slice with fullpath of files.
func fill() {
    filepath.Walk(rootpath, func(path string, info os.FileInfo, err error) error {
        if !info.IsDir() {
            filePaths = append(filePaths, path)
        }
        if err != nil {
            fmt.Println("ERROR:", err)
        }
        return nil
    })
}

func main() {
    fill()

    tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
    oauthClient := oauth2.NewClient(context.TODO(), tokenSource)
    client := putio.NewClient(oauthClient)

    for _, path := range filePaths {
        wg.Add(1)

        go func() {
            defer wg.Done()

            f, err := os.Open(path)
            if err != nil {
                log.Println("err:OPEN", err)
            }

            upload, err := client.Files.Upload(context.TODO(), f, path, 0)
            if err != nil {
                log.Println("error uploading file:", err)
            }
            fmt.Println(upload)
        }()
    }
    wg.Wait()
}

考虑这样的工作池模式:https://go.dev/play/p/p6SErj3L6Yc

在此示例应用程序中,我删除了 API 调用并仅列出了文件名。这使得它可以在操场上工作。

  • 启动固定数量的worker goroutines。我们将使用一个渠道来分发他们的工作,我们将关闭渠道来传达工作的结束。这个数字可以是 1 个或 1000 个例程,或更多。应根据您的 putio API 可以合理预期支持的并发 API 操作数来选择数量。
  • paths 是我们将用于此目的的 chan string
  • 工作人员 range 通过 paths 通道接收要上传的新文件路径
package main

import (
    "fmt"
    "os"
    "path/filepath"
    "sync"
)

func main() {
    paths := make(chan string)
    var wg = new(sync.WaitGroup)
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go worker(paths, wg)
    }
    if err := filepath.Walk("/usr", func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return fmt.Errorf("Failed to walk directory: %T %w", err, err)
        }
        if info.IsDir() {
            return nil
        }
        paths <- path
        return nil
    }); err != nil {
        panic(fmt.Errorf("failed Walk: %w", err))
    }
    close(paths)
    wg.Wait()
}

func worker(paths <-chan string, wg *sync.WaitGroup) {
    defer wg.Done()
    for path := range paths {
        // do upload.
        fmt.Println(path)
    }
}

此模式可以处理无限大的文件,而无需在处理之前将整个列表加载到内存中。如您所见,这并没有使代码更复杂——实际上,它更简单。

When I run the program it just uploads one file which is the one

函数文字继承它们被定义的范围。这就是为什么我们的代码只列出了一个路径——for 循环中的 path 变量作用域被每个 go 例程共享,所以当该变量发生变化时,所有例程都会接收到变化。

避免函数文字,除非你真的想要继承范围。在全局范围内定义的函数不会继承任何范围,您必须将所有相关变量传递给这些函数。这是一件好事 - 它使函数更易于理解,并使变量“所有权”转换更加明确。

使用函数字面量的适当情况可能是 os.Walk 参数;它的参数由 os.Walk 定义,因此定义范围是访问其他值的一种方式 - 例如 paths 通道,在我们的例子中。

说到作用域,应该避免使用全局变量,除非它们的使用范围是真正的全局变量。更喜欢在函数之间传递变量而不是共享全局变量。同样,这使变量所有权变得明确,并且可以很容易地理解哪些函数可以访问哪些变量,哪些函数不能访问哪些变量。您的等待组和 filePaths 都没有任何理由成为全球性的。

            f, err := os.Open(path)

不要忘记关闭您打开的所有文件。当你处理 40 或 50 个文件时,让所有这些打开的文件句柄堆积起来直到程序结束并不是那么糟糕,但它是你程序中的一个定时炸弹,当文件数量超过 ulimit 允许打开的文件。因为函数执行大大超过了文件需要打开的部分,所以defer在这种情况下没有意义。我会在上传文件后使用明确的 f.Close()