缘起 今天开发的时候遇到一个奇怪的问题,一个JSON文件,使用文本编辑器打开复制,并使用strings.NewReader来decode,是正常的,但是通过文件打开同样调用的方法来decode,却是失败的。后面通过打开IDE,发现文件前面有一些空白的内容。是一些bom信息。
关于BOM BOM (Byte Order Mark) 的历史原因和用途主要与字符编码和跨平台兼容性有关:
历史原因 Unicode 出现前: ASCII 只用 1 字节,没有字节序问题 各国有自己的编码标准(GB2312、Shift-JIS等) Unicode 引入后: UTF-16 使用 2 字节表示字符 不同CPU架构的字节序不同: Big Endian (大端序): 高位字节在前 Little Endian (小端序): 低位字节在前 跨平台问题: Intel x86 使用小端序 Motorola 68k 使用大端序 同一文件在不同平台解析可能出错 BOM 的作用 UTF-16 的字节序标记: FE FF: Big Endian FF FE: Little Endian UTF-8 的编码标识: EF BB BF: 表明这是 UTF-8 编码 UTF-8 实际不需要 BOM(字节序无关) Windows 添加 BOM 主要为了兼容性 实际例子 // 字符 "中" 在不同编码下的表示 text := "中" // UTF-8: E4 B8 AD // UTF-16BE: 4E 2D // UTF-16LE: 2D 4E // 示例代码 func showEncoding() { text := "中" utf8Bytes := []byte(text) // UTF-8 utf16beBytes := utf16.Encode([]rune(text)) // UTF-16BE fmt.Printf("UTF-8: % X\n", utf8Bytes) fmt.Printf("UTF-16BE: % X\n", utf16beBytes) } 常见问题场景 文本编辑器: Notepad++ 可选是否添加 BOM VS Code 可以识别和显示 BOM 某些编辑器可能显示 BOM 为特殊字符 Web 开发: PHP 文件开头的 BOM 可能导致 header 错误 HTML 文件的 BOM 可能影响布局 JavaScript 文件的 BOM 可能导致语法错误 配置文件: XML 文件的 BOM 可能影响解析 INI 文件的 BOM 可能导致配置读取错误 JSON 文件的 BOM 可能导致解析失败 建议 UTF-8 编码: 不建议使用 BOM UTF-8 是字节序无关的 已成为事实标准 新项目: 统一使用 UTF-8 无 BOM 明确规定编码要求 添加编码检测机制 老项目维护: 保持现有编码方式 添加编码处理代码 记录文件编码信息 解决方案 帮助函数 // 检测文件是否包含 BOM func HasBOM(filename string) (bool, string, error) { file, err := os.Open(filename) if err != nil { return false, "", err } defer file.Close() reader := bufio.NewReader(file) peek, err := reader.Peek(4) if err != nil && err != io.EOF { return false, "", err } // 检查不同的 BOM boms := map[string][]byte{ "UTF-8": {0xEF, 0xBB, 0xBF}, "UTF-16BE": {0xFE, 0xFF}, "UTF-16LE": {0xFF, 0xFE}, "UTF-32BE": {0x00, 0x00, 0xFE, 0xFF}, "UTF-32LE": {0xFF, 0xFE, 0x00, 0x00}, } for encoding, bom := range boms { if len(peek) >= len(bom) && bytes.Equal(peek[:len(bom)], bom) { return true, encoding, nil } } return false, "", nil } // 添加 UTF-8 BOM func AddUTF8BOM(filename string) error { // 读取原文件内容 content, err := os.ReadFile(filename) if err != nil { return err } // 检查是否已有 BOM if len(content) >= 3 && bytes.Equal(content[:3], []byte{0xEF, 0xBB, 0xBF}) { return nil // 已有 BOM,不需要添加 } // 添加 BOM newContent := append([]byte{0xEF, 0xBB, 0xBF}, content...) return os.WriteFile(filename, newContent, 0644) } // 移除 BOM func RemoveBOM(filename string) error { content, err := os.ReadFile(filename) if err != nil { return err } // 检查并移除不同类型的 BOM boms := map[string][]byte{ "UTF-8": {0xEF, 0xBB, 0xBF}, "UTF-16BE": {0xFE, 0xFF}, "UTF-16LE": {0xFF, 0xFE}, "UTF-32BE": {0x00, 0x00, 0xFE, 0xFF}, "UTF-32LE": {0xFF, 0xFE, 0x00, 0x00}, } for _, bom := range boms { if len(content) >= len(bom) && bytes.Equal(content[:len(bom)], bom) { // 移除 BOM return os.WriteFile(filename, content[len(bom):], 0644) } } return nil // 没有 BOM,不需要处理 } 纯文本文件调用 func main() { // 创建示例文件 content := []byte("Hello, 世界!") tempDir := os.TempDir() filename := filepath.Join(tempDir, "test.txt") // 写入原始内容 if err := os.WriteFile(filename, content, 0644); err != nil { log.Fatalf("Error writing file: %v", err) } defer os.Remove(filename) // 1. 检测原始文件 hasBOM, encoding, err := DetectBOM(filename) if err != nil { log.Fatalf("Error detecting BOM: %v", err) } fmt.Printf("Original file has BOM: %v, Encoding: %s\n", hasBOM, encoding) // 2. 添加 BOM if err := AddBOM(filename); err != nil { log.Fatalf("Error adding BOM: %v", err) } fmt.Println("Added UTF-8 BOM to file") // 3. 再次检测 hasBOM, encoding, err = DetectBOM(filename) if err != nil { log.Fatalf("Error detecting BOM: %v", err) } fmt.Printf("After adding BOM: has BOM: %v, Encoding: %s\n", hasBOM, encoding) // 4. 移除 BOM if err :=RemoveBOM(filename); err != nil { log.Fatalf("Error removing BOM: %v", err) } fmt.Println("Removed BOM from file") // 5. 最后检测 hasBOM, encoding, err = DetectBOM(filename) if err != nil { log.Fatalf("Error detecting BOM: %v", err) } fmt.Printf("After removing BOM: has BOM: %v, Encoding: %s\n", hasBOM, encoding) // 读取并显示最终内容 finalContent, err := os.ReadFile(filename) if err != nil { log.Fatalf("Error reading final content: %v", err) } fmt.Printf("Final content: %s\n", string(finalContent)) } JSON解析 package main import ( "bufio" "bytes" "encoding/json" "fmt" "io" "log" "os" ) // 定义 JSON 结构 type Person struct { Name string `json:"name"` Age int `json:"age"` Hobbies []string `json:"hobbies"` } // BOM 处理函数 func skipBOM(reader *bufio.Reader) error { boms := map[string][]byte{ "UTF-8": {0xEF, 0xBB, 0xBF}, "UTF-16BE": {0xFE, 0xFF}, "UTF-16LE": {0xFF, 0xFE}, } // 预读4个字节 peek, err := reader.Peek(4) if err != nil && err != io.EOF { return fmt.Errorf("reading BOM: %w", err) } // 检查并跳过 BOM for encoding, bom := range boms { if len(peek) >= len(bom) && bytes.Equal(peek[:len(bom)], bom) { _, err = reader.Discard(len(bom)) if err != nil { return fmt.Errorf("skipping BOM: %w", err) } log.Printf("Skipped %s BOM", encoding) return nil } } return nil // 没有 BOM } // 读取并解析 JSON 文件 func ReadJSONFile(filename string) ([]Person, error) { // 打开文件 file, err := os.Open(filename) if err != nil { return nil, fmt.Errorf("opening file: %w", err) } defer file.Close() // 创建带缓冲的读取器 reader := bufio.NewReader(file) // 处理 BOM if err := skipBOM(reader); err != nil { return nil, fmt.Errorf("handling BOM: %w", err) } // 解析 JSON var people []Person decoder := json.NewDecoder(reader) if err := decoder.Decode(&people); err != nil { return nil, fmt.Errorf("decoding JSON: %w", err) } return people, nil } // 创建示例 JSON 文件 func createSampleJSONFile(filename string, withBOM bool) error { // 示例数据 people := []Person{ { Name: "张三", Age: 25, Hobbies: []string{"读书", "游泳"}, }, { Name: "李四", Age: 30, Hobbies: []string{"篮球", "音乐"}, }, } // 转换为 JSON jsonData, err := json.MarshalIndent(people, "", " ") if err != nil { return err } // 如果需要 BOM,添加 UTF-8 BOM if withBOM { jsonData = append([]byte{0xEF, 0xBB, 0xBF}, jsonData...) } // 写入文件 return os.WriteFile(filename, jsonData, 0644) } func main() { // 测试文件名 filename := "test.json" // 1. 创建带 BOM 的 JSON 文件 log.Println("Creating JSON file with BOM...") err := createSampleJSONFile(filename, true) if err != nil { log.Fatalf("Error creating JSON file: %v", err) } defer os.Remove(filename) // 2. 读取并解析 JSON log.Println("Reading JSON file...") people, err := ReadJSONFile(filename) if err != nil { log.Fatalf("Error reading JSON file: %v", err) } // 3. 打印结果 log.Printf("Successfully parsed %d records:", len(people)) for i, person := range people { fmt.Printf("%d. %s (age: %d) likes %v\n", i+1, person.Name, person.Age, person.Hobbies) } // 4. 创建不带 BOM 的文件测试 log.Println("\nCreating JSON file without BOM...") err = createSampleJSONFile(filename, false) if err != nil { log.Fatalf("Error creating JSON file: %v", err) } // 5. 再次读取 log.Println("Reading JSON file without BOM...") people, err = ReadJSONFile(filename) if err != nil { log.Fatalf("Error reading JSON file: %v", err) } // 6. 打印结果 log.Printf("Successfully parsed %d records:", len(people)) for i, person := range people { fmt.Printf("%d. %s (age: %d) likes %v\n", i+1, person.Name, person.Age, person.Hobbies) } }