字符串处理
第19课:字符串处理
🎯 生活类比
想象你是一位图书管理员。每天你要处理大量的文字工作:
- 查找:在书名中是否包含某个关键词 →
strings.Contains - 替换:把旧标签替换成新标签 →
strings.Replace - 拆分:把一串用逗号分隔的标签拆开 →
strings.Split - 拼接:把多个关键词组合成一句搜索语 →
strings.Join - 修剪:去掉书名前后多余的空格 →
strings.Trim
字符串处理就是程序中的"文字工作"——几乎所有程序都需要与文本打交道。
核心概念
Go语言提供了多个标准库来处理字符串:
| 包 | 用途 | 常用函数 |
|---|---|---|
strings |
字符串查找、替换、拆分、拼接等 | Contains, Replace, Split, Join, Trim |
strconv |
字符串与其他类型的转换 | Atoi, Itoa, ParseBool, FormatFloat |
unicode/utf8 |
UTF-8编码相关的操作 | RuneCountInString, ValidString |
strings.Builder |
高效拼接大量字符串 | WriteString, String |
关键要点:
- Go中的字符串是不可变的(immutable),任何修改操作都会生成新字符串
- Go中的字符串底层是UTF-8编码的字节序列
len(str)返回的是字节数,不是字符数rune是Go中表示Unicode码点的类型(本质上是int32)
基本语法与用法
1. strings 包
GO
package main
import (
"fmt"
"strings"
)
func main() {
str := "Hello, Go语言!"
// 查找
fmt.Println(strings.Contains(str, "Go")) // true
fmt.Println(strings.HasPrefix(str, "Hello")) // true
fmt.Println(strings.HasSuffix(str, "!")) // true
fmt.Println(strings.Index(str, "Go")) // 7
// 替换
result := strings.Replace(str, "Go", "Golang", 1)
fmt.Println(result) // "Hello, Golang语言!"
// 全部替换
s := "aabbcc"
fmt.Println(strings.ReplaceAll(s, "a", "x")) // "xxbbcc"
// 拆分与拼接
csv := "apple,banana,cherry"
fruits := strings.Split(csv, ",")
fmt.Println(fruits) // [apple banana cherry]
joined := strings.Join(fruits, " | ")
fmt.Println(joined) // "apple | banana | cherry"
// 修剪
padded := " Hello World "
fmt.Println(strings.TrimSpace(padded)) // "Hello World"
fmt.Println(strings.Trim("##Hello##", "#")) // "Hello"
fmt.Println(strings.TrimLeft("##Hello##", "#")) // "Hello##"
// 大小写转换
fmt.Println(strings.ToUpper("hello")) // "HELLO"
fmt.Println(strings.ToLower("HELLO")) // "hello"
// 重复
fmt.Println(strings.Repeat("Go", 3)) // "GoGoGo"
// 计数
fmt.Println(strings.Count("banana", "an")) // 2
}
💡 提示:
strings.Split 的分隔符为空字符串时,会将字符串拆分为单个字符的切片。
2. strconv 包
GO
package main
import (
"fmt"
"strconv"
)
func main() {
// 字符串 → 整数
num, err := strconv.Atoi("42")
if err != nil {
fmt.Println("转换失败:", err)
}
fmt.Println(num) // 42
// 整数 → 字符串
str := strconv.Itoa(42)
fmt.Println(str) // "42"
// 字符串 → 布尔值
b, err := strconv.ParseBool("true")
fmt.Println(b, err) // true <nil>
// 字符串 → 浮点数
f, err := strconv.ParseFloat("3.14", 64)
fmt.Println(f, err) // 3.14 <nil>
// 浮点数 → 字符串
// 'f'表示普通格式, -1表示最少位数, 64表示float64
s := strconv.FormatFloat(3.14, 'f', -1, 64)
fmt.Println(s) // "3.14"
// 格式化输出(类似C的sprintf)
formatted := strconv.FormatInt(255, 16) // 十六进制
fmt.Println(formatted) // "ff"
}
💡 提示:
strconv.Atoi 等价于 strconv.ParseInt(s, 10, 0),返回的是平台相关的int类型。
3. unicode/utf8 包
GO
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
str := "Go语言编程"
// len() 返回字节数
fmt.Println(len(str)) // 14 (每个中文字符占3字节)
// utf8.RuneCountInString() 返回字符数
fmt.Println(utf8.RuneCountInString(str)) // 7
// 判断是否为有效UTF-8
fmt.Println(utf8.ValidString(str)) // true
fmt.Println(utf8.ValidString("abc")) // true
// 遍历字符串中的每个rune
for i, r := range str {
fmt.Printf("索引:%d 字符:%c Unicode:%U\n", i, r, r)
}
}
💡 提示:用
range 遍历字符串时,Go会自动按rune(Unicode字符)遍历,而不是按字节遍历。
4. strings.Builder(高效拼接)
GO
package main
import (
"fmt"
"strings"
)
func main() {
// ❌ 低效方式:每次拼接都创建新字符串
// result := ""
// for i := 0; i < 1000; i++ {
// result += "a" // 每次都分配新内存
// }
// ✅ 高效方式:使用 strings.Builder
var builder strings.Builder
for i := 0; i < 1000; i++ {
builder.WriteString("a")
}
result := builder.String()
fmt.Println(len(result)) // 1000
// 预分配容量,进一步提升性能
var builder2 strings.Builder
builder2.Grow(1000) // 预分配1000字节
for i := 0; i < 1000; i++ {
builder2.WriteString("b")
}
fmt.Println(builder2.Len()) // 1000
}
💡 提示:
strings.Builder 内部使用 []byte 切片,避免了字符串不可变性带来的频繁内存分配。
示例代码
示例:字符串统计与分析(难度⭐)
GO
package main
import (
"fmt"
"strings"
"unicode"
)
// 统计字符串中的各类字符数量
func analyzeString(s string) (letters, digits, spaces, others int) {
for _, r := range s {
switch {
case unicode.IsLetter(r):
letters++
case unicode.IsDigit(r):
digits++
case unicode.IsSpace(r):
spaces++
default:
others++
}
}
return
}
func main() {
text := "Hello, Go语言! 2024年 Version 2.0"
letters, digits, spaces, others := analyzeString(text)
fmt.Printf("文本: %q\n", text)
fmt.Printf("字母: %d\n", letters)
fmt.Printf("数字: %d\n", digits)
fmt.Printf("空格: %d\n", spaces)
fmt.Printf("其他: %d\n", others)
// 统计单词频率
words := strings.Fields("the go the language the world")
freq := make(map[string]int)
for _, w := range words {
freq[strings.ToLower(w)]++
}
fmt.Println("\n单词频率:", freq)
}
运行结果:
TEXT
文本: "Hello, Go语言! 2024年 Version 2.0"
字母: 17
数字: 6
空格: 5
其他: 3
单词频率: map[go:1 language:1 the:3 world:1]
示例:CSV解析器(难度⭐⭐)
GO
package main
import (
"fmt"
"strings"
)
// 简易CSV行解析器,支持引号包裹的字段
func parseCSVLine(line string) []string {
var fields []string
var current strings.Builder
inQuotes := false
for _, r := range line {
switch {
case r == '"' && !inQuotes:
// 进入引号区域
inQuotes = true
case r == '"' && inQuotes:
// 离开引号区域
inQuotes = false
case r == ',' && !inQuotes:
// 遇到分隔符,保存当前字段
fields = append(fields, current.String())
current.Reset()
default:
// 普通字符
current.WriteRune(r)
}
}
// 保存最后一个字段
fields = append(fields, current.String())
return fields
}
// 清理并格式化字段
func cleanFields(fields []string) []string {
cleaned := make([]string, len(fields))
for i, f := range fields {
cleaned[i] = strings.TrimSpace(f)
}
return cleaned
}
func main() {
// 模拟CSV数据
csvData := []string{
`Alice,28,"Beijing, China"`,
`Bob,35,"New York, USA"`,
`Charlie,42,"London, UK"`,
}
fmt.Println("=== CSV解析结果 ===")
for _, line := range csvData {
fields := parseCSVLine(line)
fields = cleanFields(fields)
fmt.Printf("姓名: %-10s 年龄: %-4s 地点: %s\n",
fields[0], fields[1], fields[2])
}
// 反向操作:将切片拼接为CSV行
record := []string{"David", "30", "Shanghai, China"}
csvLine := strings.Join(record, ",")
fmt.Println("\n生成的CSV行:", csvLine)
}
运行结果:
TEXT
=== CSV解析结果 ===
姓名: Alice 年龄: 28 地点: Beijing, China
姓名: Bob 年龄: 35 地点: New York, USA
姓名: Charlie 年龄: 42 地点: London, UK
生成的CSV行: David,30,Shanghai, China
示例:模板引擎(难度⭐⭐⭐)
GO
package main
import (
"fmt"
"strconv"
"strings"
)
// 简易模板引擎:将 {{key}} 替换为对应值
func renderTemplate(template string, data map[string]string) string {
var result strings.Builder
result.Grow(len(template) * 2) // 预估容量
i := 0
for i < len(template) {
// 查找 "{{"
if i+1 < len(template) && template[i] == '{' && template[i+1] == '{' {
// 查找对应的 "}}"
end := strings.Index(template[i+2:], "}}")
if end != -1 {
key := strings.TrimSpace(template[i+2 : i+2+end])
if value, ok := data[key]; ok {
result.WriteString(value)
} else {
// 键不存在,保留原样
result.WriteString("{{" + key + "}}")
}
i += end + 4 // 跳过 "}}"
continue
}
}
result.WriteByte(template[i])
i++
}
return result.String()
}
// 格式化表格输出
func formatTable(headers []string, rows [][]string) string {
// 计算每列最大宽度
colWidths := make([]int, len(headers))
for i, h := range headers {
colWidths[i] = len(h)
}
for _, row := range rows {
for i, cell := range row {
if i < len(colWidths) && len(cell) > colWidths[i] {
colWidths[i] = len(cell)
}
}
}
var b strings.Builder
// 写入表头
for i, h := range headers {
b.WriteString(fmt.Sprintf("%-*s | ", colWidths[i], h))
}
b.WriteString("\n")
// 写入分隔线
for i := range headers {
b.WriteString(strings.Repeat("-", colWidths[i]) + "-+-")
}
b.WriteString("\n")
// 写入数据行
for _, row := range rows {
for i, cell := range row {
if i < len(colWidths) {
b.WriteString(fmt.Sprintf("%-*s | ", colWidths[i], cell))
}
}
b.WriteString("\n")
}
return b.String()
}
// 将字符串转换为不同进制表示
func toBases(numStr string) (map[string]string, error) {
num, err := strconv.ParseInt(numStr, 10, 64)
if err != nil {
return nil, err
}
return map[string]string{
"decimal": strconv.FormatInt(num, 10),
"binary": strconv.FormatInt(num, 2),
"octal": strconv.FormatInt(num, 8),
"hexadecimal": strconv.FormatInt(num, 16),
}, nil
}
func main() {
// 1. 模板渲染
fmt.Println("=== 模板渲染 ===")
template := "Hello, {{name}}! Welcome to {{city}}. You have {{count}} new messages."
data := map[string]string{
"name": "张三",
"city": "北京",
"count": "5",
}
fmt.Println(renderTemplate(template, data))
// 2. 表格格式化
fmt.Println("\n=== 表格格式化 ===")
headers := []string{"姓名", "年龄", "城市"}
rows := [][]string{
{"Alice", "28", "Beijing"},
{"Bob", "35", "New York"},
{"Charlie", "42", "London"},
}
fmt.Print(formatTable(headers, rows))
// 3. 进制转换
fmt.Println("\n=== 进制转换 ===")
bases, _ := toBases("255")
for name, value := range bases {
fmt.Printf("%-12s: %s\n", name, value)
}
}
运行结果:
TEXT
=== 模板渲染 ===
Hello, 张三! Welcome to 北京. You have 5 new messages.
=== 表格格式化 ===
姓名 | 年龄 | 城市 |
--------+------+---------+-
Alice | 28 | Beijing |
Bob | 35 | New York|
Charlie | 42 | London |
=== 进制转换 ===
decimal : 255
binary : 11111111
octal : 377
hexadecimal : ff
实际应用场景
场景1:日志分析器
GO
package main
import (
"fmt"
"strconv"
"strings"
"time"
)
// 日志条目结构
type LogEntry struct {
Timestamp string
Level string
Message string
Source string
}
// 解析日志行
// 格式: [2024-01-15 10:30:00] [ERROR] Database connection failed (db-service)
func parseLogLine(line string) (*LogEntry, error) {
entry := &LogEntry{}
// 提取时间戳
if start := strings.Index(line, "["); start != -1 {
if end := strings.Index(line, "]"); end != -1 {
entry.Timestamp = line[start+1 : end]
line = line[end+1:]
}
}
// 提取日志级别
line = strings.TrimSpace(line)
if start := strings.Index(line, "["); start != -1 {
if end := strings.Index(line, "]"); end != -1 {
entry.Level = line[start+1 : end]
line = line[end+1:]
}
}
// 提取消息和来源
line = strings.TrimSpace(line)
if parenStart := strings.LastIndex(line, "("); parenStart != -1 {
if parenEnd := strings.LastIndex(line, ")"); parenEnd != -1 {
entry.Source = line[parenStart+1 : parenEnd]
entry.Message = strings.TrimSpace(line[:parenStart])
}
} else {
entry.Message = line
}
return entry, nil
}
// 统计日志级别
func analyzeLogs(entries []LogEntry) map[string]int {
stats := make(map[string]int)
for _, e := range entries {
stats[strings.ToUpper(e.Level)]++
}
return stats
}
// 过滤包含关键词的日志
func filterLogs(entries []LogEntry, keyword string) []LogEntry {
var filtered []LogEntry
keyword = strings.ToLower(keyword)
for _, e := range entries {
if strings.Contains(strings.ToLower(e.Message), keyword) {
filtered = append(filtered, e)
}
}
return filtered
}
func main() {
// 模拟日志数据
logLines := []string{
"[2024-01-15 10:30:00] [INFO] Application started (main-service)",
"[2024-01-15 10:30:05] [INFO] Connected to database (db-service)",
"[2024-01-15 10:31:00] [WARN] High memory usage detected (monitor)",
"[2024-01-15 10:32:00] [ERROR] Database connection timeout (db-service)",
"[2024-01-15 10:32:01] [ERROR] Retry failed, switching to backup (db-service)",
"[2024-01-15 10:33:00] [INFO] Backup database connected (db-service)",
"[2024-01-15 10:35:00] [DEBUG] Cache cleared (cache-service)",
}
// 解析所有日志
var entries []LogEntry
for _, line := range logLines {
entry, err := parseLogLine(line)
if err == nil {
entries = append(entries, *entry)
}
}
// 统计日志级别
fmt.Println("=== 日志级别统计 ===")
stats := analyzeLogs(entries)
for level, count := range stats {
fmt.Printf(" %s: %d 条\n", level, count)
}
// 过滤错误日志
fmt.Println("\n=== 错误日志 ===")
for _, e := range entries {
if strings.ToUpper(e.Level) == "ERROR" {
fmt.Printf(" %s | %s | %s\n", e.Timestamp, e.Message, e.Source)
}
}
// 搜索关键词
fmt.Println("\n=== 包含 'database' 的日志 ===")
filtered := filterLogs(entries, "database")
for _, e := range filtered {
fmt.Printf(" [%s] %s\n", e.Level, e.Message)
}
}
运行结果:
TEXT
=== 日志级别统计 ===
INFO: 3 条
WARN: 1 条
ERROR: 2 条
DEBUG: 1 条
=== 错误日志 ===
2024-01-15 10:32:00 | Database connection timeout | db-service
2024-01-15 10:32:01 | Retry failed, switching to backup | db-service
=== 包含 'database' 的日志 ===
[INFO] Connected to database
[ERROR] Database connection timeout
[INFO] Backup database connected
场景2:用户输入验证与清理
GO
package main
import (
"fmt"
"regexp"
"strconv"
"strings"
"unicode"
)
// 清理用户输入的用户名
func sanitizeUsername(name string) (string, error) {
// 去除首尾空格
name = strings.TrimSpace(name)
// 检查长度
if len(name) < 3 {
return "", fmt.Errorf("用户名太短(最少3个字符)")
}
if len(name) > 20 {
return "", fmt.Errorf("用户名太长(最多20个字符)")
}
// 只允许字母、数字、下划线
var cleaned strings.Builder
for _, r := range name {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' {
cleaned.WriteRune(r)
}
}
result := cleaned.String()
if len(result) < 3 {
return "", fmt.Errorf("有效字符太少")
}
return strings.ToLower(result), nil
}
// 验证并解析手机号(中国大陆)
func parsePhone(phone string) (string, error) {
// 去除所有空格和连字符
phone = strings.ReplaceAll(phone, " ", "")
phone = strings.ReplaceAll(phone, "-", "")
// 检查是否以+86开头
if strings.HasPrefix(phone, "+86") {
phone = phone[3:]
} else if strings.HasPrefix(phone, "86") {
phone = phone[2:]
}
// 验证长度
if len(phone) != 11 {
return "", fmt.Errorf("手机号长度不正确: %d位", len(phone))
}
// 验证是否全是数字
for _, r := range phone {
if !unicode.IsDigit(r) {
return "", fmt.Errorf("手机号包含非数字字符: %c", r)
}
}
// 验证是否以1开头
if !strings.HasPrefix(phone, "1") {
return "", fmt.Errorf("手机号必须以1开头")
}
return phone, nil
}
// 解析带单位的大小字符串
func parseSize(sizeStr string) (int64, error) {
sizeStr = strings.TrimSpace(strings.ToUpper(sizeStr))
// 提取数字部分和单位部分
var numPart strings.Builder
var unitPart strings.Builder
for _, r := range sizeStr {
if unicode.IsDigit(r) || r == '.' {
numPart.WriteRune(r)
} else if unicode.IsLetter(r) {
unitPart.WriteRune(r)
}
}
num, err := strconv.ParseFloat(numPart.String(), 64)
if err != nil {
return 0, fmt.Errorf("无效的数字: %s", numPart.String())
}
// 根据单位转换为字节
unit := unitPart.String()
multipliers := map[string]int64{
"B": 1,
"KB": 1024,
"MB": 1024 * 1024,
"GB": 1024 * 1024 * 1024,
"TB": 1024 * 1024 * 1024 * 1024,
}
multiplier, ok := multipliers[unit]
if !ok {
return 0, fmt.Errorf("未知的单位: %s", unit)
}
return int64(num * float64(multiplier)), nil
}
func main() {
// 用户名清理测试
fmt.Println("=== 用户名验证 ===")
usernames := []string{" Alice_123 ", "ab", "A!@#B", "GoDeveloper2024"}
for _, u := range usernames {
result, err := sanitizeUsername(u)
if err != nil {
fmt.Printf(" %q → 错误: %v\n", u, err)
} else {
fmt.Printf(" %q → %q\n", u, result)
}
}
// 手机号解析测试
fmt.Println("\n=== 手机号解析 ===")
phones := []string{"138 0013 8000", "+86-138-0013-8000", "12345", "23800138000"}
for _, p := range phones {
result, err := parsePhone(p)
if err != nil {
fmt.Printf(" %q → 错误: %v\n", p, err)
} else {
fmt.Printf(" %q → %s\n", p, result)
}
}
// 文件大小解析测试
fmt.Println("\n=== 文件大小解析 ===")
sizes := []string{"1.5GB", "512MB", "1024KB", "100B", "2TB"}
for _, s := range sizes {
bytes, err := parseSize(s)
if err != nil {
fmt.Printf(" %s → 错误: %v\n", s, err)
} else {
fmt.Printf(" %s → %d 字节\n", s, bytes)
}
}
}
运行结果:
TEXT
=== 用户名验证 ===
" Alice_123 " → "alice_123"
"ab" → 错误: 用户名太短(最少3个字符)
"A!@#B" → 错误: 有效字符太少
"GoDeveloper2024" → "godeveloper2024"
=== 手机号解析 ===
"138 0013 8000" → 13800138000
"+86-138-0013-8000" → 13800138000
"12345" → 错误: 手机号长度不正确: 5位
"23800138000" → 错误: 手机号必须以1开头
=== 文件大小解析 ===
1.5GB → 1610612736 字节
512MB → 536870912 字节
1024KB → 1048576 字节
100B → 100 字节
2TB → 2199023255552 字节
❓ 常见问题
Q1:为什么 len("Go语言") 返回 8 而不是 4?
GO
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "Go语言"
// len() 返回字节数,不是字符数
fmt.Println("len():", len(s)) // 8
// 中文字符在UTF-8中占3字节
// G(1) + o(1) + 语(3) + 言(3) = 8
// 正确获取字符数的方法
fmt.Println("RuneCountInString():", utf8.RuneCountInString(s)) // 4
// 或者使用 range 遍历计数
count := 0
for range s {
count++
}
fmt.Println("range计数:", count) // 4
}
要点:处理中文等多字节字符时,务必使用 utf8.RuneCountInString() 或 range 来获取真正的字符数。
Q2:字符串拼接用 + 还是 strings.Builder?
GO
package main
import (
"fmt"
"strings"
)
func main() {
// 少量拼接:用 + 即可(编译器会优化)
s := "Hello" + " " + "World"
fmt.Println(s)
// 大量拼接:必须用 strings.Builder
var builder strings.Builder
for i := 0; i < 10000; i++ {
builder.WriteString("a")
}
fmt.Println("长度:", builder.Len())
// 预分配可以进一步提升性能
var builder2 strings.Builder
builder2.Grow(10000) // 预分配10000字节
for i := 0; i < 10000; i++ {
builder2.WriteString("b")
}
fmt.Println("长度:", builder2.Len())
}
经验法则:
| 场景 | 推荐方式 |
|---|---|
| 2-3个字符串拼接 | + 或 fmt.Sprintf |
| 循环中拼接(次数已知) | strings.Builder + Grow() |
| 循环中拼接(次数未知) | strings.Builder |
Q3:如何判断字符串是否只包含特定字符?
GO
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
s := "Hello123"
// 检查是否只包含字母和数字
isAlphanumeric := true
for _, r := range s {
if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
isAlphanumeric = false
break
}
}
fmt.Println("仅字母数字:", isAlphanumeric)
// 检查是否只包含ASCII字母
isASCII := true
for _, r := range s {
if r > 127 {
isASCII = false
break
}
}
fmt.Println("仅ASCII:", isASCII)
// 检查是否包含特定字符集
allowed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
containsOnlyAllowed := true
for _, r := range s {
if !strings.ContainsRune(allowed, r) {
containsOnlyAllowed = false
break
}
}
fmt.Println("在允许范围内:", containsOnlyAllowed)
}
Q4:strings.Contains 和正则表达式如何选择?
GO
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
text := "我的邮箱是test@example.com,电话是13800138000"
// 简单查找 → 用 strings 包(更快)
fmt.Println(strings.Contains(text, "example.com")) // true
// 模式匹配 → 用正则表达式
emailRegex := regexp.MustCompile(`[\w.]+@[\w.]+\.\w+`)
fmt.Println("邮箱:", emailRegex.FindString(text))
phoneRegex := regexp.MustCompile(`1[3-9]\d{9}`)
fmt.Println("电话:", phoneRegex.FindString(text))
}
选择建议:
- 固定字符串查找:用
strings包,性能更好 - 模式匹配(如邮箱、电话格式):用
regexp包 - 避免在循环中重复编译正则表达式,应提前编译好
📖 小节
| 主题 | 关键内容 |
|---|---|
| strings 包 | Contains, HasPrefix, HasSuffix, Index, Replace, Split, Join, Trim, ToUpper, ToLower, Count, Repeat, Fields |
| strconv 包 | Atoi, Itoa, ParseBool, ParseFloat, FormatInt, FormatFloat |
| unicode/utf8 | RuneCountInString, ValidString, unicode.IsLetter/IsDigit/IsSpace |
| strings.Builder | WriteString, WriteRune, WriteByte, Grow, String, Len |
| 核心原则 | 字符串不可变、len返回字节数、range按rune遍历、大量拼接用Builder |
📝 作业
练习1:基础 - 字符串反转
编写一个函数 reverseString(s string) string,反转一个字符串。要求正确处理中文字符。
提示:不能简单地将字符串转为 []byte 反转,因为中文字符占多个字节。
GO
// 期望结果
reverseString("Hello") // "olleH"
reverseString("Go语言") // "言语oG"
练习2:进阶 - 驼峰与下划线互转
编写两个函数:
camelToSnake(s string) string:将驼峰命名转为下划线命名snakeToCamel(s string) string:将下划线命名转为驼峰命名
提示:利用 unicode.IsUpper 判断大写字母位置。
GO
// 期望结果
camelToSnake("helloWorld") // "hello_world"
camelToSnake("HTTPResponse") // "http_response"
snakeToCamel("hello_world") // "helloWorld"
snakeToCamel("http_response") // "httpResponse"
练习3:挑战 - 简易Markdown标题提取器
编写一个函数 extractHeadings(md string) []string,提取Markdown文档中的所有标题。
提示:标题以 # 开头,# 的数量表示标题级别。
GO
// 输入
md := `# 一级标题
这是正文
## 二级标题
### 三级标题
## 另一个二级`
// 期望输出
// ["# 一级标题", "## 二级标题", "### 三级标题", "## 另一个二级"]
下一课
恭喜你完成了字符串处理的学习!在下一课中,我们将学习 文件I/O操作——如何读写文件、处理目录、以及使用缓冲I/O提高性能。



