Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go字符串切片

Golang开发之字符串与切片问题踩坑记录

作者:写代码的lorre

字符串和切片,都是golang常用的两种内置数据类型,最近在日常工作中,遇到了一个字符串切片导致的问题,记录一下排查问题的过程,避免后续在这种场景上踩坑

背景

在项目中,我们使用mysql来存储数据信息,其中label表记录了标签相关的信息。表结构如下:

CREATE TABLE `label` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(190) COLLATE utf8mb4_unicode_ci DEFAULT 'label name',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_n` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=7050965 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='label'

其中name字段为varchar类型,190代表字符长度,并且是唯一索引

为什么name字段设置最大字符长度为190?

name是varchar类型,并且是唯一索引。mysql规定varchar类型为索引时,最大长度为767字节

name字段的编码格式为utf8mb4_unicode_ci,一个字符最多用4个字节来表示,767 / 4 = 191.75,所以只需限制最大字符长度小于191.75即可,项目里取190作为限制

业务代码逻辑很简单,主要有两步:

伪代码:

const (
   // 最大字符长度
   LabelNameLengthLimit = 190
)
func GetOrCreateLabel(ctx context.Context, name string) (label dao.Label, err error) {
   var err error
   // 依据name查询label信息
   label, err = db_reader.GetLabel(name)
   // 没有找到对应的label信息,且字符串长度超过190,则切片后再次查询
   if IsRecordNotFoundError(err) && len(name) > LabelNameLengthLimit {
      label, err = db_reader.GetLabel(name[:LabelNameLengthLimit])
   }
   // 还是报错,则尝试创建label信息
   if err != nil {
      label.Name = name
      err = db_writer.CreateLabel(ctx, &label)
      // 报唯一键冲突错误,可能是由于并发创建导致的问题,再次兜底进行查询
      if IsKeyConflict(err) {
         label, err = db_reader.GetLabel(ctx, name)
         if err != nil {
            return label, err
         }
      }
      if err != nil {
         return label, err
      }
   }
   return label, nil
}

问题

部分case,首次执行业务代码成功,后续执行业务代码一直报错

case1:

labelName := "© 2015 Charanga Sí o Ké - Chema Muñoz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos © 2015 Charanga Sí o Ké - Sones de Rumba"

执行:

分析

后续执行业务代码时

var err error
// 依据name查询label信息
label, err = db_reader.GetLabel(name)
// 没有找到对应的label信息,且字符串长度超过190,则切片后再次查询
if IsRecordNotFoundError(err) && len(name) > LabelNameLengthLimit {
   label, err = db_reader.GetLabel(name[:LabelNameLengthLimit])
}

err != nil时,尝试创建label信息,此时报唯一键冲突错误

线上可能是并发创建导致的唯一键冲突,兜底查询一次,此时查询还是报错RecordNotFoundError

err = db_writer.CreateLabel(ctx, &label)
// 报唯一键冲突错误,可能是由于并发创建导致的问题,再次兜底进行查询
if IsKeyConflict(err) {
   label, err = db_reader.GetLabel(ctx, name)
   if err != nil {
      return label, err
   }
}

由于字符长度超过了190,定位到主要的问题是:依据切片后name[:LabelNameLengthLimit]查询时,没有查询到结果,但是依据name去创建label信息时,报唯一键冲突,说明查询的值和实际存储的值不一致

golang string底层实现go/src/reflect/value.go

// StringHeader is the runtime representation of a string.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type StringHeader struct {
   Data uintptr
   Len  int
}

其中:

所以问题就是:查询和存储时,截取字符串的标准不一样

测试代码:

labelName := "© 2015 Charanga Sí o Ké - Chema Muñoz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos © 2015 Charanga Sí o Ké - Sones de Rumba"
fmt.Println("原字符串内容:", labelName)
fmt.Println("字节长度:", len(labelName))
fmt.Println("按照字节截取内容:", labelName[:LabelNameLengthLimit])
fmt.Println("字符长度:", len([]rune(labelName)))
fmt.Println("按照字符截取内容:", string([]rune(labelName)[:LabelNameLengthLimit]))

输出:

原字符串内容: © 2015 Charanga Sí o Ké - Chema Muñoz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos © 2015 Charanga Sí o Ké - Sones de Rumba
字节长度: 214
按照字节截取内容: © 2015 Charanga Sí o Ké - Chema Muñoz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos © 2015 Charanga S�
字符长度: 207
按照字符截取内容: © 2015 Charanga Sí o Ké - Chema Muñoz, Perfecto Artola, Pablo Guerrero, Antonio Flores, Los Gipsy King, Chambao, Adele, La Pegatina, El Chaval de la Peca, Los Manolos © 2015 Charanga Sí o Ké

解决

解决方案:

const (
   // 最大字符长度
   LabelNameLengthLimit = 190
)
func GetOrCreateLabel(ctx context.Context, name string) (label dao.Label, err error) {
   if rs := []rune(name); len(rs) > LabelNameLengthLimit {
      name = string(rs[:LabelNameLengthLimit])
   }
   var err error
   // 依据name查询label信息
   label, err = db_reader.GetLabel(name)
   // 还是报错,则尝试创建label信息
   if err != nil {
      label.Name = name
      err = db_writer.CreateLabel(ctx, &label)
      // 报唯一键冲突错误,可能是由于并发创建导致的问题,再次兜底进行查询
      if IsKeyConflict(err) {
         label, err = db_reader.GetLabel(ctx, name)
         if err != nil {
            return label, err
         }
      }
      if err != nil {
         return label, err
      }
   }
   return label, nil
}

到此这篇关于Golang开发之字符串与切片问题踩坑记录的文章就介绍到这了,更多相关Go字符串切片内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文