feat(stringx): 自 go-hua/common 迁入字符串/切片/拼音工具并独立成包

- common 的 string.go/array.go/pinyin.go 拆为 stringx 包(含随机串、GUID、
  通配符匹配、校验规则、链式缓冲、驼峰转下划线、切片删元素、拼音首字母)
- 附单元测试与 examples/stringx 可运行例程、包 README
- go.mod 新增直接依赖 mozillazg/go-pinyin(go-hua 同版本锁 v0.20.0→实际解析 v0.21.0)
This commit is contained in:
2026-09-20 12:20:28 +08:00
parent 1d0120d2c2
commit d13de8f3fa
8 changed files with 524 additions and 1 deletions
+51
View File
@@ -0,0 +1,51 @@
# stringx
字符串与切片通用工具:随机串、UUID、通配符匹配、中间串提取、手机号/身份证校验、
链式字节缓冲、驼峰转下划线、切片删元素、中文拼音首字母。
> 迁移自 go-hua/common(原 string.go / array.go / pinyin.go)。
## 用法
```go
import "git.zeroonesoft.cn/golib/zogo/stringx"
// 随机串(字母数字 / 纯数字 / 十六进制)
stringx.CreateRandomString(16)
stringx.CreateRandomStringInt(6)
stringx.CreateRandomStringHex(8)
// GUID(带连字符)/ UUID(不带连字符),基于 crypto/rand
stringx.GetGUID()
stringx.GetUUID()
// 字符串处理
stringx.GetBetweenStr("key=value&x=1", "key=", "&") // "value"
stringx.Camel2Case("GetUserName") // "get_user_name"
stringx.IsMatchGreed("hello.txt", "*.txt") // true(另有 IsMatchDynamic)
// 校验
stringx.CheckMobile("13812345678")
stringx.CheckIdCard("110101199001011234")
stringx.PhoneCheckRule() // 返回规则正则
stringx.EmailCheckRule()
// 中文拼音首字母(非中文字符原样返回)
stringx.GetFirstLetter("中国") // "zg"
// 切片删除指定下标元素(泛型)
s := stringx.DeleteElement([]int{1, 2, 3, 4, 5}, 2) // [1 2 4 5]
// 链式字节缓冲
b := stringx.NewBuffer()
b.Append("a").Append(1).Append([]byte("b")).Append(rune('c'))
```
完整可运行例程:[examples/stringx/main.go](../examples/stringx/main.go)
## 注意
- `CreateRandomString*` 内部 `rand.Int` 失败时该位为空字符,正常环境不会发生。
- `GetGUID/GetUUID` 是随机 UUID(v4 风格),非确定性 UUID;需要标准 RFC 4122
格式请用 `github.com/google/uuid`。
- 通配符匹配仅支持 `*` 和 `?` 两个通配符。
+12
View File
@@ -0,0 +1,12 @@
package stringx
// DeleteElement 删除切片中指定下标的元素(泛型,支持任意切片类型)
func DeleteElement[S []T, T any](slice S, index int) S {
if index < 0 || index >= len(slice) {
return slice
}
// 计算要删除的元素后面的元素需要向前移动多少位置
n := len(slice) - 1
copy(slice[index:n], slice[index+1:n+1])
return slice[:n]
}
+26
View File
@@ -0,0 +1,26 @@
package stringx
import (
"strings"
"github.com/mozillazg/go-pinyin"
)
// GetFirstLetter 获取中文首字母(非中文字符原样返回)
func GetFirstLetter(value string) string {
// 默认
py := pinyin.NewArgs()
py.Style = pinyin.FirstLetter
py.Fallback = func(r rune, a pinyin.Args) []string {
return []string{string(r)}
}
retText := ""
result := pinyin.Pinyin(value, py)
for _, res := range result {
for _, s := range res {
retText += strings.ToLower(s)
break
}
}
return retText
}
+257
View File
@@ -0,0 +1,257 @@
// Package stringx 字符串与切片通用工具:随机串、UUID、通配符匹配、中间串提取、
// 手机号/身份证校验、链式字节缓冲、驼峰转下划线、切片删元素、中文拼音首字母。
package stringx
import (
"bytes"
"crypto/rand"
"fmt"
"log"
"math/big"
"regexp"
"strconv"
"strings"
"unicode"
)
// CreateRandomString 生成指定长度的随机字符串(含大小写字母与数字)
func CreateRandomString(len int) string {
var container string
var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
b := bytes.NewBufferString(str)
length := b.Len()
bigInt := big.NewInt(int64(length))
for i := 0; i < len; i++ {
randomInt, _ := rand.Int(rand.Reader, bigInt)
container += string(str[randomInt.Int64()])
}
return container
}
// CreateRandomStringHex 生成指定长度的随机字符串(十六进制字符)
func CreateRandomStringHex(len int) string {
var container string
var str = "ABCDEF1234567890"
b := bytes.NewBufferString(str)
length := b.Len()
bigInt := big.NewInt(int64(length))
for i := 0; i < len; i++ {
randomInt, _ := rand.Int(rand.Reader, bigInt)
container += string(str[randomInt.Int64()])
}
return container
}
// CreateRandomStringInt 生成指定长度的纯数字随机字符串
func CreateRandomStringInt(len int) string {
var container string
var str = "1234567890"
b := bytes.NewBufferString(str)
length := b.Len()
bigInt := big.NewInt(int64(length))
for i := 0; i < len; i++ {
randomInt, _ := rand.Int(rand.Reader, bigInt)
container += string(str[randomInt.Int64()])
}
return container
}
// IsMatchDynamic 动态规划;时间复杂度O(n^2),空间复杂度O(n^2)
func IsMatchDynamic(s string, p string) bool {
n, m := len(s), len(p)
dp := make([][]bool, n+1)
for i := 0; i <= n; i++ {
dp[i] = make([]bool, m+1)
}
dp[0][0] = true
for i := 1; i <= m; i++ {
if p[i-1] == '*' {
// 可以匹配任意字符串(包括空字符串)
dp[0][i] = true
} else {
break
}
}
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
if p[j-1] == '*' {
// dp[i][j-1]=>不使用这个*,dp[i-1][j]=>使用这个*
dp[i][j] = dp[i][j-1] || dp[i-1][j]
} else if p[j-1] == '?' || s[i-1] == p[j-1] {
dp[i][j] = dp[i-1][j-1]
}
}
}
return dp[n][m]
}
// IsMatchGreed 贪心;时间复杂度O(n^2),空间复杂度O(1)
func IsMatchGreed(s string, p string) bool {
i, j := 0, 0
start, last := 0, 0
for i = 0; i < len(s); {
if j < len(p) && (s[i] == p[j] || p[j] == '?') {
i++
j++
} else if j < len(p) && p[j] == '*' {
last = i
// 记录s下一个的位置
start = j + 1
// 记录*下一个的位置
j++
} else if start != 0 {
last++
i = last
// 更新到s记录位置的下一个
j = start
} else {
return false
}
}
for ; j < len(p) && p[j] == '*'; j++ {
}
return j == len(p)
}
// GetBetweenStr 取中间字符串
func GetBetweenStr(str, starting, ending string) string {
s := strings.Index(str, starting)
if s < 0 {
return ""
}
s += len(starting)
if ending == "" {
return str[s:]
}
e := strings.Index(str[s:], ending)
if e < 0 {
return str[s:]
}
return str[s : s+e]
}
// GetGUID 使用来自 crypto/rand 包的 rand.Read 函数来生成基本的 UUID。
func GetGUID() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return ""
}
uuid := fmt.Sprintf("%x-%x-%x-%x-%x",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
return uuid
}
// GetUUID 使用来自 crypto/rand 包的 rand.Read 函数来生成基本的 UUID。
func GetUUID() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return ""
}
uuid := fmt.Sprintf("%x%x%x%x%x",
b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
return uuid
}
// CheckMobile 检验手机号
func CheckMobile(phone string) bool {
// 匹配规则
// ^1第一位为一
// [345789]{1} 后接一位345789 的数字
// \\d \d的转义 表示数字 {9} 接9位
// $ 结束符
regRuler := "^1[345789]{1}\\d{9}$"
// 正则调用规则
reg := regexp.MustCompile(regRuler)
// 返回 MatchString 是否匹配
return reg.MatchString(phone)
}
// PhoneCheckRule 手机号验证规则
func PhoneCheckRule() string {
return "^1[3|4|5|6|7|8|9][0-9]\\d{8}$"
}
// EmailCheckRule 邮箱验证规则
func EmailCheckRule() string {
return "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$"
}
// CheckIdCard 检验身份证
func CheckIdCard(card string) bool {
//18位身份证 ^(\d{17})([0-9]|X)$
// 匹配规则
// (^\d{15}$) 15位身份证
// (^\d{18}$) 18位身份证
// (^\d{17}(\d|X|x)$) 18位身份证 最后一位为X的用户
regRuler := "(^\\d{15}$)|(^\\d{18}$)|(^\\d{17}(\\d|X|x)$)"
// 正则调用规则
reg := regexp.MustCompile(regRuler)
// 返回 MatchString 是否匹配
return reg.MatchString(card)
}
// Buffer 内嵌bytes.Buffer,支持连写
type Buffer struct {
*bytes.Buffer
}
// NewBuffer 创建空字节缓冲
func NewBuffer() *Buffer {
return &Buffer{Buffer: new(bytes.Buffer)}
}
// Append 追加任意类型数据并返回自身(支持链式调用)
func (b *Buffer) Append(i interface{}) *Buffer {
switch val := i.(type) {
case int:
b.append(strconv.Itoa(val))
case int64:
b.append(strconv.FormatInt(val, 10))
case uint:
b.append(strconv.FormatUint(uint64(val), 10))
case uint64:
b.append(strconv.FormatUint(val, 10))
case string:
b.append(val)
case []byte:
b.Write(val)
case rune:
b.WriteRune(val)
}
return b
}
// append 追加字符串(容量不足时自动扩容)
func (b *Buffer) append(s string) *Buffer {
defer func() {
if err := recover(); err != nil {
log.Println("*****内存不够了!******")
}
}()
b.WriteString(s)
return b
}
// Camel2Case 驼峰式写法转为下划线写法
func Camel2Case(name string) string {
buffer := NewBuffer()
for i, r := range name {
if unicode.IsUpper(r) {
if i != 0 {
buffer.Append('_')
}
buffer.Append(unicode.ToLower(r))
} else {
buffer.Append(r)
}
}
return buffer.String()
}
+141
View File
@@ -0,0 +1,141 @@
package stringx
import (
"strings"
"testing"
)
func TestCreateRandomString(t *testing.T) {
s := CreateRandomString(16)
if len(s) != 16 {
t.Errorf("CreateRandomString(16) 长度 = %d, want 16", len(s))
}
for _, c := range s {
if !strings.ContainsRune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890", c) {
t.Errorf("字符 %q 不在允许字符集内", c)
}
}
if s2 := CreateRandomString(16); s2 == s {
t.Error("两次生成结果不应相同")
}
}
func TestCreateRandomStringInt(t *testing.T) {
s := CreateRandomStringInt(8)
if len(s) != 8 {
t.Errorf("长度 = %d, want 8", len(s))
}
for _, c := range s {
if c < '0' || c > '9' {
t.Errorf("字符 %q 应为数字", c)
}
}
}
func TestCreateRandomStringHex(t *testing.T) {
s := CreateRandomStringHex(8)
if len(s) != 8 {
t.Errorf("长度 = %d, want 8", len(s))
}
for _, c := range s {
if !strings.ContainsRune("ABCDEF1234567890", c) {
t.Errorf("字符 %q 不在十六进制字符集内", c)
}
}
}
func TestGetBetweenStr(t *testing.T) {
if got := GetBetweenStr("key=value&x=1", "key=", "&"); got != "value" {
t.Errorf("GetBetweenStr = %q, want value", got)
}
if got := GetBetweenStr("abc", "x", "y"); got != "" {
t.Errorf("起始不存在应返回空: %q", got)
}
if got := GetBetweenStr("a<..>b", "<..>", ""); got != "b" {
t.Errorf("结束为空应返回到结尾: %q", got)
}
}
func TestGetGUIDFormat(t *testing.T) {
guid := GetGUID()
if strings.Count(guid, "-") != 4 {
t.Errorf("GetGUID 应为 8-4-4-4-12 格式: %q", guid)
}
if len(strings.ReplaceAll(guid, "-", "")) != 32 {
t.Errorf("GetGUID 总长应为 32 位十六进制: %q", guid)
}
uuid := GetUUID()
if strings.Contains(uuid, "-") {
t.Errorf("GetUUID 不应含连字符: %q", uuid)
}
}
func TestCheckMobile(t *testing.T) {
if !CheckMobile("13812345678") {
t.Error("合法手机号应通过")
}
if CheckMobile("12345678901") {
t.Error("第二位为 2 应不通过")
}
if CheckMobile("1381234567") {
t.Error("10 位应不通过")
}
}
func TestCheckIdCard(t *testing.T) {
if !CheckIdCard("110101199001011234") {
t.Error("18 位数字应通过")
}
if !CheckIdCard("11010119900101123X") {
t.Error("17 位数字 + X 应通过")
}
if CheckIdCard("abc") {
t.Error("非法字符应不通过")
}
}
func TestIsMatch(t *testing.T) {
if !IsMatchGreed("hello.txt", "*.txt") {
t.Error("IsMatchGreed(hello.txt, *.txt) 应为 true")
}
if IsMatchGreed("hello.txt", "*.jpg") {
t.Error("IsMatchGreed(hello.txt, *.jpg) 应为 false")
}
if !IsMatchDynamic("hello.txt", "*.txt") {
t.Error("IsMatchDynamic(hello.txt, *.txt) 应为 true")
}
}
func TestCamel2Case(t *testing.T) {
if got := Camel2Case("GetUserName"); got != "get_user_name" {
t.Errorf("Camel2Case = %q, want get_user_name", got)
}
if got := Camel2Case("id"); got != "id" {
t.Errorf("Camel2Case = %q, want id", got)
}
}
func TestDeleteElement(t *testing.T) {
s := []int{1, 2, 3, 4, 5}
got := DeleteElement(s, 2)
if len(got) != 4 || got[0] != 1 || got[1] != 2 || got[2] != 4 || got[3] != 5 {
t.Errorf("DeleteElement = %v, want [1 2 4 5]", got)
}
}
func TestBuffer(t *testing.T) {
b := NewBuffer()
b.Append("a").Append(1).Append(int64(2)).Append([]byte("b")).Append(uint64(3))
if b.String() != "a12b3" {
t.Errorf("Buffer = %q, want a12b3", b.String())
}
}
func TestGetFirstLetter(t *testing.T) {
if got := GetFirstLetter("中国"); got != "zg" {
t.Errorf("GetFirstLetter(中国) = %q, want zg", got)
}
if got := GetFirstLetter("abc123"); got != "abc123" {
t.Errorf("非中文字符应原样返回: %q", got)
}
}