导航菜单

字符串与文本处理

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.Builder

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
}

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
}

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}
%#vGo 语法表示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
}
// 推荐:包级别预编译
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> 会被转义为 &lt;script&gt;,防止 XSS 攻击
}

模板管道与函数

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

关键点\[([^\]]+)\]\(([^)]+)\)[^\]]+ 匹配 ] 以外的字符(非贪婪),[^)]+ 匹配 ) 以外的字符。

搜索