本文最后更新于 2024-06-04,文章内容可能已经过时。

获取minio文件列表报错invalid URL escape

事件起因是需要做一个minio文件夹展示

在获取桶中的文件列表时报错:invalid URL escape "%#$"

打断点一路排查到下面的源码:

func (c *Client) listObjectsV2Query(ctx context.Context, bucketName, objectPrefix, continuationToken   // Validate bucket name.   if err := s3utils.CheckValidBucketName(bucketName); err != nil {      return ListBucketV2Result{}, err   }   // Validate object prefix.   if err := s3utils.CheckValidObjectNamePrefix(objectPrefix); err != nil {      return ListBucketV2Result{}, err   }   // Get resources properly escaped and lined up before   // using them in http request.   urlValues := make(url.Values)
   // Always set list-type in ListObjects V2   urlValues.Set("list-type", "2")
   if metadata {      urlValues.Set("metadata", "true")   }
   // Set this conditionally if asked   if startAfter != "" {      urlValues.Set("start-after", startAfter)   }
   // Always set encoding-type in ListObjects V2   urlValues.Set("encoding-type", "url")
   // Set object prefix, prefix value to be set to empty is okay.   urlValues.Set("prefix", objectPrefix)
   // Set delimiter, delimiter value to be set to empty is okay.   urlValues.Set("delimiter", delimiter)
   // Set continuation token   if continuationToken != "" {      urlValues.Set("continuation-token", continuationToken)   }
   // Fetch owner when listing   if fetchOwner {      urlValues.Set("fetch-owner", "true")   }
   // Set max keys.   if maxkeys > 0 {      urlValues.Set("max-keys", fmt.Sprintf("%d", maxkeys))   }
   // Execute GET on bucket to list objects.   resp, err := c.executeMethod(ctx, http.MethodGet, requestMetadata{      bucketName:       bucketName,      queryValues:      urlValues,      contentSHA256Hex: emptySHA256Hex,      customHeader:     headers,   })   defer closeResponse(resp)   if err != nil {      return ListBucketV2Result{}, err   }   if resp != nil {      if resp.StatusCode != http.StatusOK {         return ListBucketV2Result{}, httpRespToErrorResponse(resp, bucketName, "")      }   }
   // Decode listBuckets XML.   listBucketResult := ListBucketV2Result{}   if err = xmlDecoder(resp.Body, &listBucketResult); err != nil {      return listBucketResult, err   }
   // This is an additional verification check to make   // sure proper responses are received.   if listBucketResult.IsTruncated && listBucketResult.NextContinuationToken == "" {      return listBucketResult, ErrorResponse{         Code:    "NotImplemented",         Message: "Truncated response should have continuation token set",      }   }
   for i, obj := range listBucketResult.Contents {      listBucketResult.Contents[i].Key, err = decodeS3Name(obj.Key, listBucketResult.EncodingType)      if err != nil {         return listBucketResult, err      }      listBucketResult.Contents[i].LastModified = listBucketResult.Contents[i].LastModified.Truncate(time.Millisecond)   }
   for i, obj := range listBucketResult.CommonPrefixes {      listBucketResult.CommonPrefixes[i].Prefix, err = decodeS3Name(obj.Prefix, listBucketResult.EncodingType)      if err != nil {         return listBucketResult, err      }   }
   // Success.   return listBucketResult, nil}

Go

最后一个for循环这里报错

   for i, obj := range listBucketResult.CommonPrefixes {      listBucketResult.CommonPrefixes[i].Prefix, err = decodeS3Name(obj.Prefix, listBucketResult.EncodingType)      if err != nil {         return listBucketResult, err      }   }

Go

通过断点查看到listBucketResult.CommonPrefixes第一个元素是"'空字符串,但是obj 解析出来却是#@$!#%#$%/ ,很神奇

通义千问的回答:

这个问题看起来你遇到的情况是由于数据解析或解码时出现了错误,导致期望的数据与实际获得的数据不一致。具体来说,你提到的listBucketResult.CommonPrefixes的第一个元素在预期是空字符串(""),但实际获取到的是#@!#%#$%/这样的非法或乱码字符序列。这可能涉及几个方面的原因和解决方向:1. 数据编码解码问题:数据在存储或传输过程中可能被错误地编码或解码。特别是如果数据中包含特殊字符(如#, $, @, #, % 等)且没有正确处理,可能导致解码时出现问题。检查decodeS3Name函数的实现,确保它能正确处理了所有可能的字符,特别是特殊字符和编码类型。2. 解码类型混淆:确保listBucketResult.CommonPrefixes中数据的编码类型与你解码的预期一致。如果数据被错误地以一种编码类型存储(比如URL编码,Base64,百分比编码等),而解码时使用了另一种方式,可能会导致混乱的结果。3. 错误处理:decodeS3Name函数内部可能存在逻辑错误处理了异常情况,导致错误字符没有被正确捕获并返回。确保对非法或意外情况有合适的错误处理机制,而不是错误地生成了奇怪的输出。4. 字符串截断点检查:你提到的错误输出#@!#%#$%/,看起来像是一种不完整的转义序列,可能是因为错误处理导致的截断点问题。检查是否在解码过程中正确处理了字符串的边界,没有提前结束或截断开了未完成的转义序列。5. 测试和日志记录:在decodeS3Name函数的关键点增加日志记录,记录传入和传出的数据,有助于跟踪问题具体发生在哪个环节。总之,你需要详细审查数据的处理流程,特别是解码和解码步骤,确保所有数据一致性,以及错误处理的逻辑健全。如果decodeS3Name是自定义的函数,复审阅其源码找出可能的逻辑缺陷是关键

Plain Text

但是这个是minio官方的包,ai回答的几个问题都不切实际.

猜测是以前minio上传的文件有问题,存在一些奇怪的命名?

用另一个环境的s3正常获取文件列表,估计报错的s3是测试用的,以前只管丢文件进去,现在新增个需求需要文件列表,哦豁,给我遇到了.

Gorm

goalng的orm框架

gorm.model 自带ID、软删除deleteAt、createAt、updateAt

gorm的软删除是更新删除时间,没有删除的时候是NULL

使用 IS NULL可以走索引

但是IS NOT NULL不可以

并且NULL值作为联合索引会导致索引失效

硬删除使用Unscoped()

init函数执行顺序

在main包中导入别的包时,会优先执行导入包中的init函数,再执行main包的init函数

同一个包中不同文件的init函数的执行顺序,是根据文件名的先后顺序来确定的

当递归导入包时,init函数的执行顺序也是递归的

在同一个文件中导入不同包时,包的init执行顺序跟导入的顺序一致

filepath.walkDir

最后还是递归去推文件夹...

预分配内存和直接使用 性能差距

go 版本1.21

100,000以内直接使用性能比 预分配更好??? 偶尔预分配内存比直接使用好一点 差距也不大..100,000Pre-allocate: 1.4812131s
Direct append: 1.3112955s10,000,000
Pre-allocate: 1.1199524s
Direct append: 1.0088049s


Plain Text

func main() {   	result := testing.Benchmark(BenchmarkPreAllocate)   fmt.Printf("Pre-allocate: %v\n", result.T)
   result = testing.Benchmark(BenchmarkDirectAppend)   fmt.Printf("Direct append: %v\n", result.T)}func BenchmarkPreAllocate(b *testing.B) {   for i := 0; i < b.N; i++ {      arr := make([]string, 0, 100)      for j := 0; j < 100; j++ {         arr = append(arr, "F1747902535533854721")         //模拟业务操作耗时         time.Sleep(1 * time.Millisecond)      }   }}
func BenchmarkDirectAppend(b *testing.B) {   for i := 0; i < b.N; i++ {      var arr []string      for j := 0; j < 100; j++ {         arr = append(arr, "F1747902535533854721")         //模拟业务操作耗时         time.Sleep(1 * time.Millisecond)      }   }}

Go

实际应该看下面的代码块执行结果pre: 84.4864ms
pre: 72.2162ms
pre: 94.6278ms
pre: 63.8266ms
pre: 67.3263ms
pre: 56.8733ms
pre: 60.8382ms
pre: 60.8385ms
pre: 62.8336ms
pre: 61.8554ms
pre: 87.7657ms
pre: 58.8441ms
pre: 70.828ms
pre: 71.7597ms
pre: 59.8416ms
pre: 64.8255ms
pre: 62.8421ms
pre: 53.471ms
pre: 74.8001ms
pre: 81.7836ms
pre: 44.8797ms
pre: 58.843ms
pre: 56.8474ms
pre: 58.8537ms
pre: 46.8743ms
pre: 57.8457ms
pre: 64.8274ms
pre: 58.842ms
pre: 50.8645ms
pre: 57.8452ms
Pre-allocate: 1.0164032s
dir: 554.4596ms
dir: 583.0342ms
dir: 593.4523ms
Direct append: 1.1774837s

进程 已完成,退出代码为 0

很明显在相同时间内,预分配内存执行了30次而切片append执行执行了三次。。。差距过大!

Plain Text

func main() { result := testing.Benchmark(BenchmarkPreAllocate)   fmt.Printf("Pre-allocate: %v\n", result.T)
   result = testing.Benchmark(BenchmarkDirectAppend)   fmt.Printf("Direct append: %v\n", result.T)}func BenchmarkPreAllocate(b *testing.B) {   for i := 0; i < b.N; i++ {      s := time.Now()      arr := make([]string, 0, 10000000)      for j := 0; j < 10000000; j++ {         arr = append(arr, "F1747902535533854721")         //模拟业务操作耗时         //time.Sleep(1 * time.Millisecond)      }      fmt.Println("pre:", time.Since(s))   }}func BenchmarkDirectAppend(b *testing.B) {   for i := 0; i < b.N; i++ {      s := time.Now()      var arr []string      for j := 0; j < 10000000; j++ {         arr = append(arr, "F1747902535533854721")         //模拟业务操作耗时         //time.Sleep(1 * time.Millisecond)      }      fmt.Println("dir:", time.Since(s))   }}

Go

context

golang context处理中断和异常

Go 语言中处理中断和异常通常是通过 panic/recover 机制来实现的。panic 可以触发一个运行时异常,而 recover 可以在 defer 函数中捕获这个异常,从而防止程序崩溃。以下是一个简单的例子,展示了如何使用 panicrecover 来处理异常:

package main
 
import (
    "fmt"
)
 
func main() {
    f := func() {
        defer func() {
            if r := recover(); r != nil {
                fmt.Println("Recovered in f", r)
            }
        }()
        panic("Panic in f")
    }
 
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in main", r)
        }
    }()
 
    f()
    panic("Panic in main")
}

Go

在这个例子中,main 函数中首先调用了 f 函数,在 f 函数中触发了一个 panic。通过在 f 函数外部设置一个 defer 语句,并在其中调用 recover,可以捕获并处理 f 函数中的 panic。同样,在 main 函数的 defer 中也调用了 recover,以处理在 main 函数中可能发生的 panic。注意,recover 必须在 defer 函数中直接被调用,否则它不会有效。此外,panic 一旦被触发,程序会立即停止当前函数的执行,并开始逐层返回调用栈,查找是否有 recover。如果没有找到,程序最终会导致崩溃。

context处理超时和退出示例

package main

import (
	"context"
	"fmt"
	"time"
)

func timeConsuming(ctx context.Context, costTime int) {

	ctx.Done()

	for i := 1; i <= costTime; i++ {
		// 模拟一些耗时操作
		time.Sleep(1 * time.Second)
		fmt.Printf("协程正在运行第%v次...\n", i)
	}
}

func main() {
	// 创建一个父级 context,设置超时时间为 5 秒钟
	parentCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// 创建一个子级 context,用于控制协程
	childCtx, childCancel := context.WithCancel(parentCtx)
	defer childCancel()

	costTime := 5 // 模拟耗时 5 秒钟

	// 启动一个协程
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				// 如果收到取消信号,退出协程
				fmt.Println("协程退出")
				return
			case <-time.After(15 * time.Second):
				fmt.Println("协程超时")
			default:
				timeConsuming(childCtx, costTime)
			}
		}
	}(childCtx)

	// 等待 3 秒钟,然后取消子级 context
	time.Sleep(3 * time.Second)
	fmt.Println("取消协程")
	childCancel()

	// 继续等待 3 秒钟,模拟主协程的一些其他操作
	time.Sleep(3 * time.Second)
	fmt.Println("主协程退出")
}

Go

context详解:

详解Golang中Context的原理和使用技巧_Golang_脚本之家

Golang 的 Context 包,中文可以称之为“上下文”,是用来在 goroutine 协程之间进行上下文信息传递的,这些上下文信息包括 kv 数据、取消信号、超时时间、截止时间等。本文主要介绍了Context的原理和使用技巧,希望对大家有所帮助

https://www.jb51.net/article/268744.htm

图解 Golang Context

介绍context 是整个 Go 生态系统的关键部分。在这篇简短的文章中,我将解释 context 在 Go 应用程序中是如何工作的。 概述从非常高级的角度来看,可以将 Context 与n叉树数据结构进行比较。查看下图: 在本文中,…

https://zhuanlan.zhihu.com/p/467991975