字符串与文本处理
strings 包
strings 包提供了大量用于操作 UTF-8 编码字符串的函数,包括查找、替换、分割、连接、大小写转换、修剪空白等。它是 Go 中最常用的标准库包之一。
查找与判断
package main
import (
"fmt"
"strings"
)
func main() {
s := "Hello, Go 语言世界"
// Contains — 是否包含子串
fmt.Println(strings.Contains(s, "Go")) // true
fmt.Println(strings.Contains(s, "Python")) // false
// HasPrefix — 是否以某前缀开头
fmt.Println(strings.HasPrefix(s, "Hello")) // true
// HasSuffix — 是否以某后缀结尾
fmt.Println(strings.HasSuffix(s, "世界")) // true
// Index — 返回子串首次出现的索引,未找到返回 -1
fmt.Println(strings.Index(s, "Go")) // 7
fmt.Println(strings.Index(s, "Python")) // -1
// LastIndex — 返回子串最后一次出现的索引
fmt.Println(strings.LastIndex(s, "l")) // 3
// Count — 返回子串出现的次数(不重叠)
fmt.Println(strings.Count("ababab", "ab")) // 3
}分割与连接
package main
import (
"fmt"
"strings"
)
func main() {
// Split — 按分隔符分割字符串
parts := strings.Split("a,b,c,d", ",")
fmt.Println(parts) // [a b c d]
// SplitN — 限制分割次数(返回 n 个子串)
parts2 := strings.SplitN("a,b,c,d", ",", 2)
fmt.Println(parts2) // [a b,c,d]
// Fields — 按连续空白字符分割
fields := strings.Fields(" Hello World Go ")
fmt.Println(fields) // [Hello World Go]
// Join — 用分隔符连接字符串切片
result := strings.Join([]string{"Go", "is", "awesome"}, " ")
fmt.Println(result) // Go is awesome
}替换与修剪
package main
import (
"fmt"
"strings"
)
func main() {
s := "hello world, hello go"
// Replace — 替换子串
fmt.Println(strings.Replace(s, "hello", "Hi", 1)) // Hi world, hello go
fmt.Println(strings.Replace(s, "hello", "Hi", -1)) // Hi world, Hi go
// ReplaceAll — 替换所有匹配(Go 1.12+)
fmt.Println(strings.ReplaceAll(s, "hello", "Hi")) // Hi world, Hi go
// TrimSpace — 去除两端空白
fmt.Printf("[%s]\n", strings.TrimSpace(" hello ")) // [hello]
// Trim / TrimLeft / TrimRight — 去除指定字符
fmt.Println(strings.Trim("!!!hello!!!", "!")) // hello
fmt.Println(strings.TrimLeft("!!!hello!!!", "!")) // hello!!!
fmt.Println(strings.TrimRight("!!!hello!!!", "!")) // !!!hello
// TrimPrefix / TrimSuffix — 去除指定前缀/后缀
fmt.Println(strings.TrimPrefix("hello-world", "hello-")) // world
fmt.Println(strings.TrimSuffix("hello-world", "-world")) // hello
}大小写转换
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("hello")) // HELLO
fmt.Println(strings.ToLower("HELLO")) // hello
// Title — 每个单词首字母大写(已不推荐使用,见下方说明)
fmt.Println(strings.Title("hello world")) // Hello World
// ToTitle — 将字符串转为标题格式
fmt.Println(strings.ToTitle("hello world")) // HELLO WORLD
// EqualFold — 大小写不敏感比较(Unicode 安全)
fmt.Println(strings.EqualFold("Hello", "HELLO")) // true
fmt.Println(strings.EqualFold("你好", "你好")) // true
}strings.Title 已废弃
Go 1.18 起 strings.Title 被标记为 deprecated,因为它对 Unicode 的处理不正确。应使用 cases.Title 包替代。
strings.Builder
strings.Builder 用于高效地构建字符串。与反复使用 + 拼接字符串相比,Builder 通过内部 buffer 减少内存分配和拷贝,适合大规模字符串拼接场景。
package main
import (
"fmt"
"strings"
)
func main() {
var sb strings.Builder
// 写入字符串
sb.WriteString("Hello")
sb.WriteByte(',')
sb.WriteString(" ")
sb.WriteString("Go!")
// 写入任意类型
fmt.Fprintf(&sb, " The answer is %d.", 42)
result := sb.String()
fmt.Println(result) // Hello, Go! The answer is 42.
// 也可以预分配容量减少扩容
var sb2 strings.Builder
sb2.Grow(100) // 预分配 100 字节
for i := 0; i < 10; i++ {
sb2.WriteString("hello")
}
fmt.Println(sb2.Len()) // 50
}Builder 不可拷贝
strings.Builder 内部持有 byte slice,在 WriteString 等方法调用后会持有其引用。不要在赋值或传参时拷贝 Builder 值(不要 b2 := b1),否则可能产生不可预期的行为。应始终通过指针传递。
strconv 包
strconv 包提供了字符串与基本数据类型之间的转换函数,包括整数、浮点数、布尔值与字符串的相互转换。转换失败时通常返回 error,应始终检查。
字符串与整数转换
package main
import (
"fmt"
"strconv"
)
func main() {
// Atoi — 字符串转 int(最常用)
n, err := strconv.Atoi("42")
if err != nil {
fmt.Println("转换失败:", err)
}
fmt.Printf("n = %d, type: %T\n", n, n) // n = 42, type: int
// Atoi 错误处理
n2, err := strconv.Atoi("abc")
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "abc": invalid syntax
// Itoa — int 转字符串
s := strconv.Itoa(42)
fmt.Printf("s = %q, type: %T\n", s, s) // s = "42", type: string
// ParseInt — 更灵活,可指定进制和位数
n3, err := strconv.ParseInt("FF", 16, 64) // 16 进制解析
fmt.Println(n3) // 255
n4, err := strconv.ParseInt("1010", 2, 8) // 2 进制解析
fmt.Println(n4) // 10
// FormatInt — 整数转字符串,指定进制
hex := strconv.FormatInt(255, 16)
fmt.Println(hex) // ff
bin := strconv.FormatInt(10, 2)
fmt.Println(bin) // 1010
}字符串与浮点数转换
package main
import (
"fmt"
"strconv"
)
func main() {
// ParseFloat — 字符串转浮点数
f, err := strconv.ParseFloat("3.14159", 64)
if err != nil {
fmt.Println("转换失败:", err)
}
fmt.Printf("f = %v, type: %T\n", f, f) // f = 3.14159, type: float64
// FormatFloat — 浮点数转字符串
// 参数:值, 格式('f'/'e'/'g'/'b'), 精度, 位大小(32/64)
s1 := strconv.FormatFloat(3.14159, 'f', 2, 64) // "3.14"
s2 := strconv.FormatFloat(3.14159, 'f', -1, 64) // "3.14159"
s3 := strconv.FormatFloat(3.14159e10, 'e', 2, 64) // "3.14e+10"
s4 := strconv.FormatFloat(3.14159, 'g', -1, 64) // "3.14159"
fmt.Println(s1) // 3.14
fmt.Println(s2) // 3.14159
fmt.Println(s3) // 3.14e+10
fmt.Println(s4) // 3.14159
// FormatFloat 格式说明
// 'f': 十进制小数 → 3.14
// 'e': 科学计数法 → 3.14e+00
// 'g': 自动选择最短 → 3.14 或 3.14e+10
// 'b': 二进制指数 → 精确表示(少用)
}布尔值与其他转换
package main
import (
"fmt"
"strconv"
)
func main() {
// ParseBool — 字符串转布尔值
b1, _ := strconv.ParseBool("true")
b2, _ := strconv.ParseBool("1")
b3, _ := strconv.ParseBool("t")
b4, _ := strconv.ParseBool("TRUE")
b5, _ := strconv.ParseBool("0")
fmt.Println(b1, b2, b3, b4, b5) // true true true true false
// FormatBool — 布尔值转字符串
fmt.Println(strconv.FormatBool(true)) // "true"
fmt.Println(strconv.FormatBool(false)) // "false"
// Quote / Unquote — 字符串的引号处理
fmt.Println(strconv.Quote("hello\nworld")) // "hello\nworld"
fmt.Println(strconv.QuoteToASCII("你好")) // "\u4f60\u597d"
unquoted, _ := strconv.Unquote(`"hello\nworld"`)
fmt.Println(unquoted) // hello
// world
}ParseBool 接受的值
ParseBool 接受:"1", "t", "T", "TRUE", "true", "True" → true;"0", "f", "F", "FALSE", "false", "False" → false。其他任何输入都会返回错误。
fmt 包进阶
fmt 包提供了丰富的格式化动词(verb)用于控制输出的格式。常用动词包括 %v(默认值)、%T(类型)、%s(字符串)、%d(整数)、%f(浮点数)等。
格式化动词速查表
| 动词 | 说明 | 示例 | 输出 |
|---|---|---|---|
%v | 默认格式 | fmt.Printf("%v", []int{1,2}) | [1 2] |
%+v | 结构体带字段名 | fmt.Printf("%+v", p) | {Name:Alice Age:25} |
%#v | Go 语法表示 | fmt.Printf("%#v", 42) | 42 |
%T | 类型 | fmt.Printf("%T", 42) | int |
%d | 十进制整数 | fmt.Printf("%d", 42) | 42 |
%b | 二进制 | fmt.Printf("%b", 10) | 1010 |
%o | 八进制 | fmt.Printf("%o", 8) | 10 |
%x | 十六进制小写 | fmt.Printf("%x", 255) | ff |
%X | 十六进制大写 | fmt.Printf("%X", 255) | FF |
%s | 字符串 | fmt.Printf("%s", "hi") | hi |
%q | 带引号的字符串 | fmt.Printf("%q", "hi") | "hi" |
%f | 十进制浮点 | fmt.Printf("%f", 3.14) | 3.140000 |
%.2f | 保留 2 位小数 | fmt.Printf("%.2f", 3.14) | 3.14 |
%e | 科学计数法 | fmt.Printf("%e", 3.14) | 3.140000e+00 |
%t | 布尔值 | fmt.Printf("%t", true) | true |
%p | 指针地址 | fmt.Printf("%p", &x) | 0xc000... |
%% | 百分号本身 | fmt.Printf("%%") | % |
宽度与精度控制
package main
import "fmt"
func main() {
// 宽度:右对齐(默认)
fmt.Printf("[%10s]\n", "hi") // [ hi]
// 宽度:左对齐(使用 -)
fmt.Printf("[%-10s]\n", "hi") // [hi ]
// 宽度补零
fmt.Printf("[%05d]\n", 42) // [00042]
// 精度
fmt.Printf("%.2f\n", 3.14159) // 3.14
fmt.Printf("%.4s\n", "Hello") // Hell(截断字符串)
fmt.Printf("|%8.2f|\n", 3.14) // | 3.14|(宽度 8,精度 2)
fmt.Printf("|%-8.2f|\n", 3.14) // |3.14 |(左对齐)
}Sprintf 与 Fprintf
package main
import (
"fmt"
"os"
"strings"
)
func main() {
// Sprintf — 格式化后返回字符串(不打印)
name := "Alice"
age := 25
s := fmt.Sprintf("姓名: %s, 年龄: %d", name, age)
fmt.Println(s) // 姓名: Alice, 年龄: 25
// Fprintf — 格式化输出到指定 Writer
// 写入文件
f, _ := os.Create("output.txt")
fmt.Fprintf(f, "写入文件的内容: %s\n", "Hello")
f.Close()
// 写入标准错误
fmt.Fprintf(os.Stderr, "错误: %s\n", "操作失败")
// 写入 strings.Builder
var sb strings.Builder
fmt.Fprintf(&sb, "第一行: %d\n", 1)
fmt.Fprintf(&sb, "第二行: %d\n", 2)
fmt.Println(sb.String())
// Sprint / Fprint — 不添加换行符
s2 := fmt.Sprint("a", "b", 1, 2) // "ab12"
fmt.Println(s2)
}Sscanf — 从字符串解析
package main
import "fmt"
func main() {
var name string
var age int
// Sscanf 从字符串按格式解析
n, _ := fmt.Sscanf("Alice 25", "%s %d", &name, &age)
fmt.Printf("读取了 %d 个值: name=%s, age=%d\n", n, name, age)
// 读取了 2 个值: name=Alice, age=25
// 解析日期
var year, month, day int
fmt.Sscanf("2024-03-15", "%d-%d-%d", &year, &month, &day)
fmt.Printf("%d年%d月%d日\n", year, month, day) // 2024年3月15日
// 解析十六进制
var val int
fmt.Sscanf("0xFF", "0x%x", &val)
fmt.Println(val) // 255
}regexp 包
regexp 包提供了 Go 的正则表达式功能,底层使用 RE2 语法。与 PCRE 不同,RE2 保证执行时间为线性(O(n)),不存在回溯导致的性能问题,但不支持反向引用。
基础用法
package main
import (
"fmt"
"regexp"
)
func main() {
// Compile — 编译正则表达式(返回 error)
re, err := regexp.Compile(`\d+`)
if err != nil {
panic(err)
}
// MustCompile — 编译正则(失败时 panic,适合全局变量)
re2 := regexp.MustCompile(`^[a-zA-Z]+$`)
// MatchString — 检查字符串是否匹配
fmt.Println(re.MatchString("abc123")) // true
fmt.Println(re.MatchString("hello")) // false
fmt.Println(re2.MatchString("Hello")) // true
fmt.Println(re2.MatchString("123")) // false
// Match — 检查字节切片是否匹配
fmt.Println(re.Match([]byte("abc123"))) // true
}查找与提取
package main
import (
"fmt"
"regexp"
)
func main() {
// FindString — 查找第一个匹配
re := regexp.MustCompile(`\d+`)
fmt.Println(re.FindString("abc123def456")) // 123
// FindAllString — 查找所有匹配
results := re.FindAllString("abc123def456", -1)
fmt.Println(results) // [123 456]
// FindAllString 限制数量
results2 := re.FindAllString("abc123def456ghi789", 2)
fmt.Println(results2) // [123 456]
// FindStringIndex — 查找匹配的索引
loc := re.FindStringIndex("abc123def")
fmt.Println(loc) // [3 6](起始位置和结束位置)
}
// 使用捕获组提取数据
func extractEmails(text string) []string {
re := regexp.MustCompile(`([\w.]+)@([\w.]+)`)
matches := re.FindAllStringSubmatch(text, -1)
for _, match := range matches {
// match[0] = 完整匹配
// match[1] = 第一个捕获组(用户名)
// match[2] = 第二个捕获组(域名)
fmt.Printf("用户: %s, 域名: %s\n", match[1], match[2])
}
// 仅提取第一个捕获组
names := re.FindAllStringSubmatch(text, -1)
var result []string
for _, m := range names {
result = append(result, m[1])
}
return result
}替换
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
re := regexp.MustCompile(`\d+`)
// ReplaceAllString — 替换所有匹配
result := re.ReplaceAllString("版本 1.2.3", "X")
fmt.Println(result) // 版本 X.X.X
// ReplaceAllStringFunc — 使用函数替换
result2 := re.ReplaceAllStringFunc("abc123def456", func(s string) string {
return strings.ToUpper(s)
})
fmt.Println(result2) // abcABCdefDEF
// Expand — 使用模板替换(捕获组)
re2 := regexp.MustCompile(`(\w+),(\w+)`)
template := "$2 $1" // 引用捕获组
result3 := re2.ReplaceAllString("World,Hello", template)
fmt.Println(result3) // Hello World
}预编译正则提升性能
如果同一个正则表达式会被多次使用,应在包级别或初始化时使用 regexp.MustCompile 预编译,避免重复编译的开销。
// 推荐:包级别预编译
var emailRegex = regexp.MustCompile(`^[\w.]+@[\w.]+\.\w+$`)
func isValidEmail(s string) bool {
return emailRegex.MatchString(s)
}unicode 包
unicode 包提供了对 Unicode 字符的分类和判断功能,包括判断字符是否为字母、数字、空白、大小写、CJK 字符等。配合 unicode/utf8 包可以进行完整的 Unicode 处理。
package main
import (
"fmt"
"unicode"
)
func main() {
// 判断字符类别
fmt.Println(unicode.IsLetter('A')) // true
fmt.Println(unicode.IsLetter('中')) // true
fmt.Println(unicode.IsDigit('5')) // true
fmt.Println(unicode.IsDigit('五')) // false
fmt.Println(unicode.IsSpace(' ')) // true
fmt.Println(unicode.IsSpace('\t')) // true
fmt.Println(unicode.IsSpace('\n')) // true
// 大小写判断
fmt.Println(unicode.IsUpper('A')) // true
fmt.Println(unicode.IsLower('a')) // true
fmt.Println(unicode.IsTitle('測')) // false
// 大小写转换(对单个 rune 操作)
fmt.Printf("%c\n", unicode.ToUpper('a')) // A
fmt.Printf("%c\n", unicode.ToLower('A')) // a
fmt.Printf("%c\n", unicode.ToTitle('a')) // A
// 特定脚本判断
fmt.Println(unicode.Is(unicode.Han, '中')) // true — 汉字
fmt.Println(unicode.Is(unicode.Hiragana, 'あ')) // true — 平假名
fmt.Println(unicode.Is(unicode.Katakana, 'ア')) // true — 片假名
fmt.Println(unicode.Is(unicode.Latin, 'A')) // true — 拉丁字母
// 判断标点符号
fmt.Println(unicode.IsPunct(',')) // true
fmt.Println(unicode.IsPunct('。')) // true
fmt.Println(unicode.IsSymbol('©')) // true
}text/template 与 html/template
Go 标准库提供了两个模板引擎:text/template 用于生成纯文本输出,html/template 用于生成安全的 HTML 输出(自动转义)。模板使用 {{ . }} 语法引用数据。
text/template 基础
package main
import (
"os"
"text/template"
)
func main() {
// 定义模板字符串
const tpl = `你好, {{ .Name }}!
你的年龄是 {{ .Age }} 岁。
{{ if .IsVIP }}你是一名 VIP 用户!{{ else }}请升级 VIP。{{ end }}
{{ range .Hobbies }}
- {{ . }}
{{ end }}
`
// 解析模板
t, err := template.New("greeting").Parse(tpl)
if err != nil {
panic(err)
}
// 准备数据
data := struct {
Name string
Age int
IsVIP bool
Hobbies []string
}{
Name: "Alice",
Age: 25,
IsVIP: true,
Hobbies: []string{"编程", "阅读", "游泳"},
}
// 执行模板,输出到标准输出
t.Execute(os.Stdout, data)
}html/template 安全输出
package main
import (
"html/template"
"os"
)
func main() {
const htmlTpl = `
<!DOCTYPE html>
<html>
<head><title>{{ .Title }}</title></head>
<body>
<h1>{{ .Title }}</h1>
<p>{{ .Content }}</p>
{{ range .Items }}
<li>{{ . }}</li>
{{ end }}
</body>
</html>`
t, _ := template.New("page").Parse(htmlTpl)
data := struct {
Title string
Content string
Items []string
}{
Title: "我的页面",
Content: "<script>alert('xss')</script>", // 会被自动转义!
Items: []string{"项目1", "项目2"},
}
t.Execute(os.Stdout, data)
// Content 中的 <script> 会被转义为 <script>,防止 XSS 攻击
}始终使用 html/template 生成 HTML
当输出目标是 HTML 时,必须使用 html/template 而非 text/template。html/template 会自动对 <、>、&、"、' 等字符进行转义,防止 XSS(跨站脚本攻击)。使用 text/template 生成 HTML 存在严重安全隐患。
模板管道与函数
package main
import (
"os"
"strings"
"text/template"
)
func main() {
const tpl = `
大写: {{ .Name | upper }}
小写: {{ .Name | lower }}
字数: {{ .Name | len }}
重复: {{ "ha" | printf "%s%s" }}
{{- range $index, $item := .Items }}
{{ $index }}: {{ $item }}
{{- end }}
`
// 自定义模板函数
funcs := template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
}
t := template.Must(template.New("demo").Funcs(funcs).Parse(tpl))
data := map[string]any{
"Name": "Alice",
"Items": []string{"Go", "Rust", "Python"},
}
t.Execute(os.Stdout, data)
}练习题
练习 1:CSV 解析器
编写一个函数 ParseCSVLine(line string) []string,手动实现 CSV 行解析(不使用 encoding/csv)。要求:
- 正确处理引号包裹的字段,如
"Hello, World" - 正确处理转义的引号
"" - 不使用正则表达式
解题思路:逐字符遍历,跟踪是否在引号内部。遇到逗号且不在引号内时分割字段。
代码:
package main
import "fmt"
func ParseCSVLine(line string) []string {
var fields []string
var current strings.Builder
inQuotes := false
for i := 0; i < len(line); i++ {
ch := line[i]
if inQuotes {
if ch == '"' {
// 检查是否是转义引号 ""
if i+1 < len(line) && line[i+1] == '"' {
current.WriteByte('"')
i++ // 跳过下一个引号
} else {
inQuotes = false // 结束引号
}
} else {
current.WriteByte(ch)
}
} else {
if ch == '"' {
inQuotes = true
} else if ch == ',' {
fields = append(fields, current.String())
current.Reset()
} else {
current.WriteByte(ch)
}
}
}
fields = append(fields, current.String())
return fields
}
func main() {
fmt.Printf("%#v\n", ParseCSVLine(`Alice,25,"New York",true`))
// []string{"Alice", "25", "New York", "true"}
fmt.Printf("%#v\n", ParseCSVLine(`"Hello, World","""Quote""",123`))
// []string{"Hello, World", `"Quote"`, "123"}
fmt.Printf("%#v\n", ParseCSVLine(`simple,no quotes,here`))
// []string{"simple", "no quotes", "here"}
}关键点:状态机思维——用 inQuotes 标记当前是否在引号内,引号内的逗号不作为分隔符。
练习 2:模板驱动的代码生成
使用 text/template 编写一个程序,根据结构体信息自动生成 Go 代码的 CRUD 函数骨架。
解题思路:定义模板字符串,将结构体名和字段信息传入模板,生成代码。
代码:
package main
import (
"fmt"
"os"
"strings"
"text/template"
)
type Field struct {
Name string
Type string
}
type Model struct {
Name string
Fields []Field
}
func main() {
const codeTpl = `// 代码自动生成 — 请勿手动编辑
package models
// {{ .Name }} — 模型结构体
type {{ .Name }} struct {
{{- range $i, $f := .Fields }}
{{ $f.Name }} {{ $f.Type }}
{{- end }}
}
// New{{ .Name }} — 创建新实例
func New{{ .Name }}({{ range $i, $f := .Fields }}{{ if $i }}, {{ end }}{{ $f.Name }} {{ $f.Type }}{{ end }}) *{{ .Name }} {
return &{{ .Name }}{
{{- range $i, $f := .Fields }}
{{ $f.Name }}: {{ $f.Name }},
{{- end }}
}
}
// String — 字符串表示
func (m *{{ .Name }}) String() string {
return "{{ .Name }}{" +
{{- range $i, $f := .Fields }}
{{ if $i }}", " + {{ end }}"{{ $f.Name }}=" + fmt.Sprintf("%v", m.{{ $f.Name }}) +
{{- end }}
"}"
}
`
funcs := template.FuncMap{
"lower": strings.ToLower,
}
t := template.Must(template.New("crud").Funcs(funcs).Parse(codeTpl))
model := Model{
Name: "User",
Fields: []Field{
{"ID", "int"},
{"Name", "string"},
{"Email", "string"},
{"Age", "int"},
},
}
t.Execute(os.Stdout, model)
}输出:
// 代码自动生成 — 请勿手动编辑
package models
// User — 模型结构体
type User struct {
ID int
Name string
Email string
Age int
}
// NewUser — 创建新实例
func NewUser(ID int, Name string, Email string, Age int) *User {
return &User{
ID: ID,
Name: Name,
Email: Email,
Age: Age,
}
}
// ...关键点:模板的 range 可以遍历切片,{{ if $i }} 利用索引判断是否为第一个元素来控制逗号输出。
练习 3:正则表达式提取器
编写一个函数 ExtractMarkdownLinks(text string) []Link,使用正则表达式从 Markdown 文本中提取所有链接。
格式:[显示文本](URL)
解题思路:使用正则表达式的捕获组分别提取显示文本和 URL。
代码:
package main
import (
"fmt"
"regexp"
)
type Link struct {
Text string
URL string
}
func ExtractMarkdownLinks(text string) []Link {
// 正则表达式:[显示文本](URL)
re := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
matches := re.FindAllStringSubmatch(text, -1)
var links []Link
for _, match := range matches {
links = append(links, Link{
Text: match[1],
URL: match[2],
})
}
return links
}
func main() {
text := `这是一个示例文档。
请访问 [Go 官网](https://go.dev) 获取更多信息。
也可以查看 [GitHub](https://github.com) 上的代码仓库。
嵌套的 [Google 搜索](https://google.com/search?q=golang) 链接。`
links := ExtractMarkdownLinks(text)
for _, link := range links {
fmt.Printf("文本: %-15s URL: %s\n", link.Text, link.URL)
}
}输出:
文本: Go 官网 URL: https://go.dev
文本: GitHub URL: https://github.com
文本: Google 搜索 URL: https://google.com/search?q=golang关键点:\[([^\]]+)\]\(([^)]+)\) 中 [^\]]+ 匹配 ] 以外的字符(非贪婪),[^)]+ 匹配 ) 以外的字符。
