Golang 从入门到夺门而逃 (一)

Go语言(golang)的学习笔记及一些基本知识。

第一章 Go语言

1.1 go doc

Go文档可分为两种执行,go doc fmt.Printfgodoc -http=:8001(godoc -http :8001)

1.2 编译Go

//aSourceFile.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello, world!")
}

go bulid aSourceFile.go

1.3 执行Go

go run aSourceFile.go运行完删除可执行文件。

1.4 Go语言规则

  1. 编译时Go包必须被使用。(可以添加_绕过)
  2. 花括号{不能用在行首,原因是编译时会自动在行末添加分号。

1.5 使用外部Go包

例如import "github.com/mactsouk/go/tree/master/simpleGitHub",不能直接go run,需要go get -v github.com/mactsouk/go/tree/master/simpleGitHub;清理需要go clean -i -v -x github.com/mactsouk/go/tree/master/simpleGitHub && rm -rf ~/go/src/github.com/mactsouk/go/tree/master/simpleGitHub

1.6 变量声明与赋值

短赋值语句arg := value与隐式var arg = value声明基本相同;var常用作全局变量,变量类型后置声明例如var i,j int

1.7 log记录

package main

import (
    "fmt"
    "log"
    "os"
)

var LOGFILE = "myLog.log"

func main() {
    f, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        fmt.Printf("Error opening file: %v\n", err)
        return
    }
    defer f.Close()

    iLog := log.New(f, "iLog ", log.LstdFlags)
    iLog.Println("Hello Log!")
    iLog.Println("Hello Log!!")
    iLog.Println("Hello Log!!!")
}

第二章 内部机制

2.1 垃圾收集

使用GODEBUG=gctrace=1 go run gColl.go获得垃圾收集处理期间的详细信息;使用runtime.GC()手动垃圾收集初始化,该操作会阻塞调用者,进而阻塞整个程序。相关代码存在于src/runtime/mgc.go。Go垃圾收集器是一个实时垃圾收集器,并与Go程序的其他协程并发运行,且仅对低延迟问题进行优化。

runtime.GC()
_ = arr[0]    // 防止过早收集

2.2 映射

以散列表形式存储,包含指针映射myMap := make(map[int]*int);不包含指针映射myMap := make(map[int]int);划分映射myMap := make([]map[int]int,200)。更多可以参考3.5。

2.3 不安全代码

需要引用unsafe包。

memoryAddress := uintptr(unsafe.Pointer(pointer)) + unsafe.Sizeof(array[0])

2.4 调用C代码

注释写代码;编译需要调用gcc,Windows环境需要在PATH变量中引用MinGW

2.4.1 同文件

package main

//#include <stdio.h>
//void callC() {
//    printf("Calling C code!\n");
//}
import "C"

func main() {
   println("code in go >>>")
   C.callC()
   println("<<< code in go")
}

2.4.2 独立文件

需要提前编译gcc -c *.c && ar -rs callC.a *.o生成链接库

// callC.h
#ifndef CALLC_H
#define CALLC_H
void cHello();

#endif

// callC.c
#include<stdio.h>
#include"callC.h"

void cHello() {
    printf("Hello from C\n");
}

// main.go
package main

/*
#cgo CFLAGS: -I${SRCDIR}/cFileDictionary
#cgo LDFLAGS: ${SRCDIR}/cFileDictionary/callC.a
#include <stdlib.h>
#include <callC.h>
*/
import "C"

func main() {
    C.cHello()
}

2.5 C调用Go

包名必须为main。需要在导出函数前添加//export funcName(注意//与export之间无空格)

// usedByC.go
package main

import "C"

//export PrintByGo
func PrintByGo() {
    println("A Go function!")
}

//export Add
func Add(a, b int) int {
    return a * b
}

func main(){
}
go build -O usedByC.o -buildmode=c-shared usedByC.go

不需要对usedByC.h做任何改动。

// willUseGo.c
#include<stdio.h>
#include<usedByC.h>

int main(int argc, char **argv)
{
    GoInt x = 1;
    GoInt y = 2;
    PrintByGo();
    GoInt p = Add(x, y);
    printf("%d\n", (int)p);
    return 0;
}

2.5 defer 关键字

推迟某个函数的执行,直到周边函数返回,简单而言,就是所在函数全部执行完后再执行defer的语句。一个比较常见的例子是:在文件打开操作附近,用defer写上关闭处理。当存在多个defer时遵循LIFO。

package main

func d1() {
    for i := 3; i > 0; i-- {
        defer print(i)
    }
}

func d2() {
    for i := 3; i > 0; i-- {
        defer func() {
            print(i)
        }()
    }
}

func d3() {
    for i := 3; i > 0; i-- {
        defer func(n int) {
            print(n)
        }(i)
    }
}

func main() {
    d1()
    println()
    d2()
    println()
    d3()
    println()
}

// output
// 123
// 000
// 123

2.6 panic()和recover()函数

panic终止当前流并启用异常处理,recover恢复panic打断。

package main

func a() {
    println("Inside a()")
    defer func() {
        if c := recover(); c != nil {
            println("recover Inside a()")
        }
    }()
    println("about call b()")
    b()
    println("b() exited") // will not execute
    println("exit a()")   // will not execute
}

func b() {
    println("Inside b()")
    panic("Panic in b()")
    println("exit b()") // will not execute
}
func main() {
    a()
    println("exit main()")
}
// Output:
//    Inside a()
//    about call b()
//    Inside b()
//    recover Inside a()
//    exit main()       

2.7 strace dtrace dtruss

几个比较好用的Unix调试程序。

2.8 Go环境

使用runtime包,可以用来限制、提示使用的go版本。

    println(runtime.Compiler)         // "gc"
    println(runtime.GOARCH)          // "amd64"
    println(runtime.GOOS)              // "windows"
    println(runtime.Version())      // "go1.18.3"
    println(runtime.NumCPU())        // 8
    println(runtime.NumGoroutine())    // 1

查看所有环境变量列表和值可以在shell中使用go env这个命令

2.9 Go汇编器

$ GOOS=windows GOARCH=amd64 go tool compile -S goSource.go
$ GOOS=windows GOARCH=amd64 go build -gcfilags -S goSource.go

命令等价,更多信息可以参考A Quick Guide to Go’s Assembler

2.10 节点树

Go节点被定义为一个struct,其中包含了大量属性。可以使用下面的代码生成。

$ go run nodeTree.go
$ go tool compile -W nodeTree.go

go tool 6g -W nodetree.go该命令可能已经过时。

-W 参数用于生成调试解析树

l(8)表明当前节点定义位于第8行代码中。

2.11 go build细节

添加-x参数可以输出编译细节。

2.12 生成WebAssembly代码

WebAssembly (Wasm) 是一种标定于虚拟机的机器模型和可执行格式,旨在实现速度和文件尺寸方面的高效性。

Wasm主要包含两种格式,纯文本.wat,二进制文件.wasm。Rust、C、C++也可以生成WebAssembly。

2.12.1 示例

  1. 编译
// toWasm.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Creating WebAssembly code from Go!")
}
$ GOOS=js GOARCH=wasm go build -o main.wasm toWasm.go
  1. 使用

    main.wasm复制到web服务器目录中。

    可以使用$(go env GOROOT)/misc/wasm下文件作为参考,替换wasm_exec.htmltest.wasmmain.wasm。点击run后相关结果会在控制台输出。

    1. 需要在Webserver中访问wasm_exec.html,否则需要添加–allow-file-access-from-files或–args –disable-web-security参数启动Chrome。
    2. 修改chrome://flags/#enable-webassembly-baseline可以Wasm优化运行时间。
    3. 如果有Node.js环境,也可以是执行下面这些命令运行。
    $ export PATH="$PATH:$(go env GOROOT)/misc/wasm"
    $ GOOS=js GOARCH=wasm go run .
    

2.13 小结与建议

  1. 如果Go函数中存在错误,则可以记录该错误或返回该错误;如果缺少足够的理由,则不应同时执行这两项操作。
  2. Go语言中的接口定义了行为,而非数据和数据结构。
  3. 尽量使用io.Readerio.Writer接口,是代码更具扩展性。
  4. 仅在必要时向函数传递指向某个变量的指针,其余时间传递变量值即可。
  5. 错误变量是error类型,而非string
  6. 非必要,不在生产环境使用测试代码。如果不了解某个特性,首次使用前对其进进行测试。

第三章 基本数据类型

3.1 数字数值类型

  1. 整数

    int8, int16, int32, int64及对应的uint;int与uint为当前平台最有效的数据类型。会随计算机结构体系大小而变化。

  2. 浮点数

    float32, float64大约6、15位精度。

  3. 复数

    complex64,complex128;含有两个float32(float64)分别表示实部和虚部。

    c1 := 12 + 1i
    c2 := complex(5, 7)
    

    注意,不能声明为c := 12 + 2 * i,该语句为加法与乘法运算,如果i没有定义还会报错。

3.2 循环

Go语言不支持while关键字,需要使用for循环代替。

for i := 0; i < 100; i++ {
} // for
for {
} // while
for ok := true; ok; ok = anExpression {
} // do...while(anExpression)

anArray := [5]int{0, 1, -1, 2, -2}
for i, value := range anArray {
    fmt.Println(i, value)
} // keyword range

3.3 数组

索引开始为0,len(anArray)计算数组长度。

anArray := [2][3]int{{0, 1, -1}, {2, -2}}
for i, value := range anArray {
    for j, v := range value {
        fmt.Println(i, j, v, anArray[i][j])
    }
}

Go语言中数组大小无法修改,即Go数组为非动态数组,故Go语言中较少使用。

3.4 切片

切片在内部采用数组方式实现,切片通过引用方式被传递至参数中,实际传递的是切片变量的内存地址,在函数内部对切片进行的修改在函数退出后并不会丢失。Go语言不会生成切片的副本。遍历方式与数组类似。

aSliceLiteral := []int{1, 2, 3}
integer := make([]int, 20) // 自动填充20个nil,对于整数来说是0

与数组的明显区别在于[]中不指定元素数量;且可以通过aSliceLiteral = append(aSliceLiteral, 666)添加元素。s := make([]byte, 5)为字节切片。s := make([][]int, 5)为多维切片,多维切片make后内部为空(nil[[][][][][]]),切片类型的0为nil。当一直使用多维切片时应是否需要重新设计解决方案。

3.4.1 切片重组

可以通过对数组或切片进行[:]运算进行切片,范围左闭右开。注意,切片重组产生的子片段并非副本,因此,修改子切片会同步对原片段进行操作。

package main

import "fmt"

func main() {
    anArray := []int{1, 2, 3, 4, 5, 6, 7, 8}
    bnArray := anArray[3:6]
    anArray[4] = 9
    for i, value := range bnArray {
        fmt.Println(i, value)
    }
}
// Output
//0 4
//1 9  // not 5
//2 6

3.4.2 自动扩展

切片包含两个属性长度len()和容量cap(),当空间耗尽后,Go语言会自动翻倍当前切片容量。

3.4.3 copy(dst, src)函数

以最小数量复制切片。数组复制时需要使用[:]进行转换。

3.4.4 sort.Slice()切片排序

3.4.5 切片中附加数组

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3}
    s2 := [3]int{4, 5, 6}
    ref := s2[:]
    s1 = append(s1, ref...)
    s1 = append(s1, s1...)
    s1[2] = 9
    s1[4] = 8
    fmt.Println("Output: \t", s1)
}
// Output:          [1 2 9 4 8 6 1 2 3 4 5 6]

3.5 映射

Go语言中映射和其他语言的哈希表相等价。映射可以使用任意数据类型作为索引,即映射键(key)。映射键必须支持==运算。

iMap := make(map[string]int)
iMap["a"] = 1
iMap["b"] = 2
/* * * * */
iMap := map[string]int{
    "a" : 1
    "b" : 2
}

println(iMap["c"] == "") // Output true

遍历采用for key, value := range iMap;删除采用delete(iMap, "a"),delete 可以删除不存在的键,并不会生成任何报错,另外,如果获取不存在的键时,会返回0值。此外,不能将nil赋予尝试使用的映射。

3.6 常量

const定义,例如const A = 1(注意不是:=

const s1 = 123
const s2 float64 = 123

var v1 float32 = s1 * 12
var v2 float32 = s2 * 12 // Error!

3.6.1 常量生成器iota

iota用于声明一个相关值序列,即递增生成。

const (
    One int = iota
    Zero
)
fmt.Println(Zero, One)  // output  1 0


const (
    Zero int = 3 << iota
    One
    Two
    _
    Three
)
fmt.Println(Zero, One, Two, Three)
// output 3 6 12 48 (3*(2**0) + 3*(2**1) + 3*(2**2) + 3*(2**4))

_忽略值。

3.7 指针

小心使用指针。*解引用,&取地址。

func getPointer(n *int){
    *n = *n * *n    // only update value
}
func returnPointer(n int) *int{
    v := n * n
    return &v
}

Go语言中的字符串表示值类型,而非C语言中指针。指针常用用作数据共享或区分0值与非设定值。

3.8 时间和日期

需要使用time包。

3.8.1 时间日期格式

Go语言有个神奇的日期2006-01-02T15:04:05Z07:00,这是Go语言的时间格式,仔细观察发现,这个日期包含了1~7这些数字(06年1月2日下午3点4分5秒西7区北美山地标准时间),相较于其他语言%Y-%m-%d %H:%M:%Syyyy-MM-dd HH:mm:ss这样的表达式,显然Golang更易理解与记忆。

Unit Golang Layout Examples Note
Year 06 21, 81, 01
Year 2006 2021, 1981, 0001
Month January January, February, December
Month Jan Jan, Feb, Dec
Month 1 1, 2, 12
Month 01 01, 02, 12
Day Monday Monday, Wednesday, Sunday
Day Mon Mon, Wed, Sun
Day 2 1, 2, 11, 31
Day 02 01, 02, 11, 31 zero padded day of the month
Day _2 ⎵1, ⎵2, 11, 31 space padded day of the month
Day 002 001, 002, 011, 031, 145, 365, 366 zero padded day of the year
Day __2 ⎵⎵1, ⎵⎵2, ⎵11, ⎵31, 365, 366 space padded day of the year
Part of day PM AM, PM
Part of day pm am, pm
Hour 24h 15 00, 01, 12, 23
Hour 12h 3 1, 2, 12
Hour 12h 03 01, 02, 12
Minute 4 0, 4 ,10, 35
Minute 04 00, 04 ,10, 35
Second 5 0, 5, 25
Second 05 00, 05, 25
10-1 to 10-9 s .0 .000000000 .1, .199000000 Trailing zeros included
10-1 to 10-9 s .9 .999999999 .1, .199 Trailing zeros omitted
Time zone MST UTC, MST, CET
Time zone Z07 Z, +08, -05 Z is for UTC
Time zone Z0700 Z, +0800, -0500 Z is for UTC
Time zone Z070000 Z, +080000, -050000 Z is for UTC
Time zone Z07:00 Z, +08:00, -05:00 Z is for UTC
Time zone Z07:00:00 Z, +08:00:00, -05:00:00 Z is for UTC
Time zone -07 +00, +08, -05
Time zone -0700 +0000, +0800, -0500
Time zone -070000 +000000, +080000, -050000
Time zone -07:00 +00:00, +08:00, -05:00
Time zone -07:00:00 +00:00:00, +08:00:00, -05:00:00

在Golang 1.17+之后的版本中,可以使用,代替.作为小数秒的分隔符(输出只会显示.),例如,显示当前秒数可为05.0000,这与05,0000等效。更多细节可以在time/format.go查看。

RFC 3339格式:“{年}-{月}-{日}T{时}:{分}:{秒}.{毫秒}{时区}”;
其中时间部分的毫秒是可选的;在时区信息中,Z 是零时区 Zulu 的缩写,可以被 +08:00 或 -08:00 等替换。例如
2006-01-02T15:04:05.00Z
2006-01-02T08:04:05.00-07:00
代表相同时间。

3.8.2 常用变量与函数

3.8.2.1 一些时间常量
time.Nanosecond     // 纳秒
time.Microsecod     // 微秒
time.Millisecond    // 毫秒
time.Second         // 秒
time.Minute         // 分钟
time.Hour           // 小时
3.8.2.1 常用函数
time.Now().Unix()  //时间戳(1970.01.01到此刻所经历的秒数)

time.Sub()  //计算两个时间的差值。 time.Now().Sub(time.Now())

time0 := time.Now()
loc, _ := time.LoadLocation("Asia/Shanghai")  //加载时区,忽略报错
time0.In(loc)  // 时区转换

myTime := "12:10"
fullTime, err := time.Parse("15:04", myTime)  //时间或日期解析,不存在的信息值为0,15:04这些字段可以参考3.8.1的表格。
// 对于文本时间处理可以使用regexp.MustCompile()进行正则匹配然后根据匹配内容写出time.Parse()即可

3.9 用时间进行性能分析

3.9.1 度量Go语言中的命令和函数的执行时间

package main

import (
    "fmt"
    "time"
)

func main() {
    start := time.Now()
    time.Sleep(time.Second)
    duration := time.Since(start)
    fmt.Println(duration)

    start = time.Now()
    for i := 0; i < 2000000000; i++ {
        _ = i
    }
    duration = time.Since(start)
    fmt.Println(duration)

    sum := 0
    start = time.Now()
    for i := 0; i < 2000000000; i++ {
        sum += i
    }
    duration = time.Since(start)
    fmt.Println(duration)
    //fmt.Println(duration, sum)
}

// Output
// 1.0022136s
// 549.4892ms
// 535.5952ms

一个比较有意思的事情,执行求和速度要比不做处理效率要高,通过分析汇编发现sum += i这条语句跟空语句是一样的,因为后面并没有引用sum的值,甚至_ = i在汇编过后还多了条nop空语句;但是相差10多毫秒,看起来与nop无关。继续分析,调换_ = isum += i顺序,发现第二次执行优于第一次,猜测该现象与更底层的机制有关(缓存、内存分配等)。

; go tool compile -S source.go

; _ = i
0x0086 00134 (source.go:15) CALL    time.Now(SB)
0x008b 00139 (source.go:15) XORL    DX, DX
0x008d 00141 (source.go:16) JMP 146
0x008f 00143 (source.go:16) INCQ    DX
0x0092 00146 (source.go:16) CMPQ    DX, $2000000000
0x0099 00153 (source.go:16) JLT 143
0x009b 00155 (source.go:16) NOP
0x00a0 00160 (source.go:19) CALL    time.Since(SB)

; sum += i
0x00e5 00229 (source.go:22) CALL    time.Now(SB)
0x00ea 00234 (source.go:22) XORL    DX, DX
0x00ec 00236 (source.go:23) JMP 241
0x00ee 00238 (source.go:23) INCQ    DX
0x00f1 00241 (source.go:23) CMPQ    DX, $2000000000
0x00f8 00248 (source.go:23) JLT 238
0x00fa 00250 (source.go:26) CALL    time.Since(SB)
// gobench
package test

import (
    "fmt"
    "testing"
    "time"
)

func BenchmarkSum(b *testing.B) {
    sum := 0
    start := time.Now()
    for i := 0; i < 2000000000; i++ {
        sum += i
    }
    duration := time.Since(start)
    fmt.Println(duration)
}

func BenchmarkEmpty(b *testing.B) {
    start := time.Now()
    for i := 0; i < 2000000000; i++ {
        _ = i
    }
    duration := time.Since(start)
    fmt.Println(duration)
}

// Output
// goos: windows
// goarch: amd64
// pkg: go1/test
// BenchmarkSum
// 1.1164016s
// BenchmarkSum-8       1       1117387900 ns/op
// BenchmarkEmpty
// 1.0992783s
// BenchmarkEmpty-8     1       1100245100 ns/op
// PASS

3.9.2 度量Golang垃圾收集器的操作

参考2.1,简单修改即可,统计runtime.GC()前后的时间做差即可。

package main

import (
    "fmt"
    "runtime"
    "time"
)

type data struct {
    i, j int
}

func main() {
    var N = 40000000
    var structure []data
    for i := 0; i < N; i++ {
        value := int(i)
        structure = append(structure, data{value, value})
    }
    start := time.Now()
    runtime.GC()
    duration := time.Since(start)
    fmt.Println(duration)
    _ = structure[0]
}

// Output: 962.5µs

第四章 组合类型的使用

虽然Golang中标准类型方便、快速和灵活,但很有可能无法包含需要支持的各种数据类型。对此,Golang提供了结构这一概念,并可通过开发人员自定义相关类型。除此之外,Golang还办含量自身的方式以支持元组,以使函数可返回多个值,并且无需像C语言那样将其整合至结构中。

4.1 结构

4.1.1 结构的声明与定义

type aStructure struct {    //声明
    persson string    //字段通常小写开始
    height int
    weight int
}

var s1 aStructure    // 定义
p1 := aStructure{"p1", 12, -2}    // 定义
p2 := aStructure{weight: 12, height: -2}    // 定义不必声明全部全部字段

一般来讲,Golang中的类型,特别是结构,通常被定义在main()函数的外部,进而面向全局作用域并可用于整个Go包,除非希望某种类型仅在当前作用域有效,并不希望被其他地方使用。

结构数组中赋予另一个结构时,结构将被复制进入结构数组,因为原始结构的数值将不会对当前数组的对象产生影响。

结构类型定义中字段的设置顺序对于所定义结构的类型标识来说十分重要,在Golang中,如果字段不具备相同的顺序,那么包含相同字段的两个结构并不会被视为等同。

4.1.2 指向结构的指针

结构可以整合任意多个值,并将此类值视为单一实体。

package main

import "fmt"

type myStructure struct {
    Name    string
    Surname string
    Height  int32
}

func createStructure(n, s string, h int32) *myStructure {
    if h > 300 {
        h = 0
    }
    return &myStructure{n, s, h}
}

func retStructure(n, s string, h int32) myStructure {
    if h > 300 {
        h = 0
    }
    return myStructure{n, s, h}
}

func main() {
    s1 := createStructure("AAA", "BBB", 333)
    s2 := retStructure("AAA", "BBB", 333)
    fmt.Println((*s1).Name)
    fmt.Println(s2.Name)
}

对于C/C++背景的开发人员来说,Go函数返回局部变量的内存地址完全合法,且不会丢失。

go的方法可以直接返回局部变量的指针,这主要依赖go是有runtime的语言,编译器在发现有变量可以逃逸出去的时候会在堆上分配变量而不是栈上,这样就可以返回该变量的指针了,且会使该地址的引用+1,当生命空间结束时,gc会去回收。

上面这段代码中createStructure()retStructure()均可正常工作,使用凭个人喜好即可。值得一提的是,如果采用返回一个指向结构的指针方法,则在使用时需要解引用。

4.1.3 关键字new

Golang支持new关键字,它可以分配新的对象,new返回所分配对象的内存地址,即new返回指针。

pS := new(aStructure)    // 创建一个新的aStructure变量

sP :+ new([]aStructure) // 创建一个指向nil的切片

new和make的区别主要在于,make创建的变量已被适当初始化,所分配的内存空间不包含0值。除此之外,make适用于映射、通道和切片,且不会返回一个内存地址。这意味着make不会返回指针。

make 关键字的主要作用是创建 slice、map 和 Channel 等内置的数据结构,而 new 的主要作用是为类型申请一片内存空间,并返回指向这片内存的指针。

4.2 元组

严格来说,元组是一个包含多个部分的有序表。关于元组,最为重要的一点是,Go语言不支持元组类型,也就是说,元组能用,但从未发布官方支持。

package main

import "fmt"

func retThree(x int) (int, int, int) {
    return 2 * x, x * x, -x
}

func main() {
    n1, n2, n3 := retThree(20)
    n1, n2 = n2, n1
    n1, n2, n3 = n1*2, n1*n1, -n2
    fmt.Println(n1, n2, n3)
}

此外,可以采用_忽略一个或全部返回值(如果声明了未使用的变量,则可能导致编译错误)。

4.3 正则表达式和模式匹配

正则表达式使用regexp包。当在代码中使用正则表达式时,应将正则表达式的定义视为相关代码中最重要的部分,因为代码的功能取决于正则表达式。

4.3.1 文件读取

filename := "test.txt"
column := 2
f, err := os.Open(filename)
if err != nil {
    fmt.Printf("error opening file %s\n", err)
}
defer f.Close()
r := bufio.NewReader(f)
for {
    line, err := r.ReadString('\n')
    if err == io.EOF {
        break
    } else if err != nil {
        fmt.Printf("error reading file%s\n", err)
    }
    data := strings.Fields(line)
    if len(data) >= column {
        fmt.Println(data[column-1])
    }
}

永远不要对数据予以百分之百的信任,特别是数据源子非技术性用户,需要始终验证希望获取的数据是否存在。

4.3.2 正则表达式示例——IPv4匹配

关于正则表达式,建议另找教程学习。

func findIP(input string) string {
    partIP := "(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"
    grammar := partIP + "\\." + partIP + "\\." + partIP + "\\." + partIP
    matchMe := regexp.MustCompile(grammar)
    return matchMe.FindString(input)
}

func main() {
    ipv4 := findIP("1.1.1.1")
    trial := net.ParseIP(ipv4)
    if trial.To4() != nil {
        fmt.Println(ipv4)
    }
}

4.4 字符串

严格来讲,Go语言中的字符串是一个组合类型。Go语言默认支持UTF-8,这意味着,无需加载专用包即可输出Unicode字符。Go字符串位只读字节切片,可加载任意的字节类型,并可以包含任意长度。利用len()可以计算字符串变量或字符串字面值的长度(不一定是字符串长度,而是Unicode的长度)。对于Unicode而言,在fmt.Printf()%q可以安全转义输出,%+q输出ASCII;%s输出字符串;%#U输出一个Unicode编码及字符,在包含Unicode的字符串中 range将逐一处理Unicode字符。

fmt.Println(len("字符串"))    // output 9

for x, y := range "0a字符串b1" {
    fmt.Printf("%c %#U starts at byte position %d\n", y, y, x)
}
// Output
// 0 U+0030 '0' starts at byte position 0
// a U+0061 'a' starts at byte position 1
// 字 U+5B57 '字' starts at byte position 2
// 符 U+7B26 '符' starts at byte position 5
// 串 U+4E32 '串' starts at byte position 8
// b U+0062 'b' starts at byte position 11
// 1 U+0031 '1' starts at byte position 12

4.4.1 rune

rune是一个int32值,因而也是一种Go类型,用于表示Unicode代码点。这里,代码点或代码位置是一个数字值,常用于表示单个Unicode字符。此外代码点还包含其他含义,如提供格式信息。

可以将一个字符串视作一个rune集合。rune字面值是一个单引号中的字符。

rune与byte的差别是一个是uint32一个是uint8,即一个rune可以表示一个汉字。

fmt.Println([]byte("字符串"))    // output [229 173 151 231 172 166 228 184 178]
fmt.Println([]byte("abc")) // output [97 98 99]
fmt.Printf("%s\n", 'a') // output %!s(int32=97)
fmt.Printf("%c\n", 'a') // output a
fmt.Printf("%q\n", "\xe5\xad\x97") //output "字"
fmt.Printf("%s\n", "\xe5\xad\x97") //output 字

4.4.2 unicode包

与ctype比较类似?用于单个字符的处理

const sL = "\x99\x00ab\x50\x00\x23\x50\x29\x9c\xe5\xad\x97"
for i := 0; i < len(sL); i++ {
    if unicode.IsPrint(rune(sL[i])) {
        fmt.Printf("%c\n", sL[i])
    } else {
        fmt.Println("Not printable!")
    }
}
//output
//Not printable!
//Not printable!
//a
//b
//P
//Not printable!
//#
//P
//)
//Not printable!
//å
//Not printable!
//Not printable!

// 示例:判断汉字
func main() {
    for _, r := range "Hello 世界!" {
        // 判断字符是否为汉字
        if unicode.Is(unicode.Scripts["Han"], r) {  //或者In都是可以的
            fmt.Printf("%c", r) // 世界
        }
    }
}

4.4.3 strings包

用作字符串处理。

package main
import (
    "fmt"
    "strings"
)
func main(){
    str := "This is an example of  string"
    fmt.Printf("Does the string\" %s\" have prefix %s ?\n",str,"Th")
    fmt.Printf("%t\n",strings.HasPrefix(str,"Th"))
    fmt.Printf("Does the string \"%s\" have prefix %s?\n",str,"string")
    fmt.Printf("%t\n",strings.HasSuffix(str,"string"))
    fmt.Printf("Does the string \"%s \" have prefix %s?\n",str,"example")
    fmt.Printf("%t\n",strings.Contains(str,"example"))
    fmt.Printf("%t\n",strings.HasPrefix(str,"example"))

    fmt.Println(strings.ContainsAny("team","i"))
    fmt.Println(strings.ContainsAny("failure","a&o"))
    fmt.Println(strings.ContainsAny("foo",""))
    fmt.Println(strings.ContainsAny("",""))
    fmt.Println(strings.Contains("team","i"))
    fmt.Println(strings.Contains("failure","a&o"))
    fmt.Println(strings.Contains("foo",""))
    fmt.Println(strings.Contains("",""))
}
// output
// Does the string" This is an example of  string" have prefix Th ?
// true
// Does the string "This is an example of  string" have prefix string?
// true
// Does the string "This is an example of  string " have prefix example?
// true
// false
// false
// true
// false
// false
// false
// false
// true
// true

HasPrefix前缀,HasSuffix后缀,中间Contains。与strings.Contains(str,chars)不同的是,strings.ContainsAny(str,chars)能够匹配更广泛的内容,它可以匹配Unicode字符。

此外strings包还包括Compare(),Fields(),Replace(),Split()等常用函数,更多可以查看相关文档。

4.5 switch语句

Go默认在每个case后补上break,匹配到第一个成立时不在执行其他语句,当匹配其他情形时,会执行default,一般default位于switch语句组后(可以省略default语句)。switch十分灵活,分支也可以包含相应的条件,亦可以包含硬编码。

a := 0
switch a {    // 不会报错,执行后无事发生
case 0:
case 1:
case 2:
    print("yes")
default:
    print("no")
}

switch a {    // 执行后输出yes
case 0, 1, 2:
    print("yes")
}

switch { // 执行后输出yes
case a > 0:
    print("yes")
case a < 0:
    print("no")
}

switch a { // 执行后输出yesyesyes
case 0:
    print("yes")
    fallthrough
case 1:
    print("yes")
    fallthrough
case 2:
    print("yes")
}

switch {
case false:
    fmt.Println("The integer was <= 4")
    fallthrough
case true:
    fmt.Println("The integer was <= 5")
    fallthrough
case false:
    fmt.Println("The integer was <= 6")
    fallthrough
case true:
    fmt.Println("The integer was <= 7")
    fallthrough
case false:
    fmt.Println("The integer was <= 8")
default:
    fmt.Println("default case")
}
// output
//The integer was <= 5
//The integer was <= 6
//The integer was <= 7
//The integer was <= 8

fallthrough强制执行后面的一个case代码,不论后面case的值是否为真。

4.6 math/big包

通过两个Go程序学习使用math/big标准包,实现高精度计算。

4.6.1 计算高精度Pi值

计算采用Bellard’s formula算法,算法实现如下
$$
\begin{align}
\pi = \frac1{2^6} \sum_{n=0}^\infty \frac{(-1)^n}{2^{10n}} \, \left(-\frac{2^5}{4n+1} \right. & {} – \frac1{4n+3} + \frac{2^8}{10n+1} – \frac{2^6}{10n+3} \left. {} – \frac{2^2}{10n+5} – \frac{2^2}{10n+7} + \frac1{10n+9} \right)
\end{align}
$$
相关程序如下

// calculatePi.go

package main

import (
    "fmt"
    "math"
    "math/big"
    "os"
    "strconv"
)

var precision uint = 0    // Pi精度 

func Pi(accuracy uint) *big.Float {
    k := 0
    pi := new(big.Float).SetPrec(precision).SetFloat64(0) 
    k1k2k3 := new(big.Float).SetPrec(precision).SetFloat64(0)
    k4k5k6 := new(big.Float).SetPrec(precision).SetFloat64(0)
    temp := new(big.Float).SetPrec(precision).SetFloat64(0)
    minusOne := new(big.Float).SetPrec(precision).SetFloat64(-1)
    total := new(big.Float).SetPrec(precision).SetFloat64(0)

    two2Six := math.Pow(2, 6)
    two2SixBig := new(big.Float).SetPrec(precision).SetFloat64(two2Six)

    // Bellard算法
    for {
        if k > int(accuracy) {
            break
        }
        t1 := float64(float64(1) / float64(10*k+9))
        k1 := new(big.Float).SetPrec(precision).SetFloat64(t1)
        t2 := float64(float64(64) / float64(10*k+3))
        k2 := new(big.Float).SetPrec(precision).SetFloat64(t2)
        t3 := float64(float64(32) / float64(4*k+1))
        k3 := new(big.Float).SetPrec(precision).SetFloat64(t3)
        k1k2k3.Sub(k1, k2)
        k1k2k3.Sub(k1k2k3, k3)

        t4 := float64(float64(4) / float64(10*k+5))
        k4 := new(big.Float).SetPrec(precision).SetFloat64(t4)
        t5 := float64(float64(4) / float64(10*k+7))
        k5 := new(big.Float).SetPrec(precision).SetFloat64(t5)
        t6 := float64(float64(1) / float64(4*k+3))
        k6 := new(big.Float).SetPrec(precision).SetFloat64(t6)
        k4k5k6.Add(k4, k5)
        k4k5k6.Add(k4k5k6, k6)
        k4k5k6 = k4k5k6.Mul(k4k5k6, minusOne)
        temp.Add(k1k2k3, k4k5k6)

        k7temp := new(big.Int).Exp(big.NewInt(-1), big.NewInt(int64(k)), nil)
        k8temp := new(big.Int).Exp(big.NewInt(1024), big.NewInt(int64(k)), nil)

        k7 := new(big.Float).SetPrec(precision).SetFloat64(0)
        k7.SetInt(k7temp)
        k8 := new(big.Float).SetPrec(precision).SetFloat64(0)
        k8.SetInt(k8temp)

        t9 := float64(256) / float64(10*k+1)
        k9 := new(big.Float).SetPrec(precision).SetFloat64(t9)
        k9.Add(k9, temp)
        total.Mul(k9, k7)
        total.Quo(total, k8)
        pi.Add(pi, total)

        k = k + 1
    }
    pi.Quo(pi, two2SixBig)
    return pi
}

func main() {
    arguments := os.Args
    if len(arguments) == 1 {
        fmt.Println("Please provide one numeric argument!")
        os.Exit(1)
    }

    temp, _ := strconv.ParseUint(arguments[1], 10, 32)
    precision = uint(temp) * 3    // 精度控制*3暂不理解含义

    PI := Pi(precision)
    fmt.Println(PI)
}

贝拉算法简介:

Fabrice Bellard在圆周率算法方面也有着惊人的成就,1997年FabriceBellard提出最快圆周率算法公式。在计算圆周率的过程中,Fabrice Bellard使用改良后的查德诺夫斯基方程算法来进行圆周率的计算,并使用贝利-波温-劳夫算法来验证计算的结果。为了纪念他对圆周率算法所作出的杰出贡献,Fabrice Bellard所使用的改良型算法被命名为Fabrice Bellard算法,这种算法是目前所有圆周率算法中最快的一种,这个计算N位PI的公式比传统的BBQ算法要快47%。

2009年的最后一天,Fabr ice Bellard宣布另一重大突破:他用桌面电脑打破了由超级计算机保持的圆周率运算记录。这是一个壮举, 他将PI计算到了小数点后2.7万亿位!更令人惊讶的是, 他使用的不过是价格不到2000欧元的个人PC,仅用了116天,就计算出了PI的小数点后第2.7万亿位,超过了排名世界第47位的T2K Open超级计算机于2009年8月17日创造的世界纪录。新纪录比原纪录多出1200亿位,然而,他使用的这台桌面电脑的配置仅为:2.93GHz Core i7 CPU,6GB内存,7.5TB硬盘!

不过这次为了加快计算完成的速度保住排名第一的位置,Fabrice Bel lard使用了9台联网的电脑来对数据进行验证, 若使用一台电脑来验证计算结果的话, 则需要额外增加13天的计算时间。

Fabrice Bellard在圆周率方面的辉煌成就, 使他创造多次圆周率单一位计算的世界纪录(计算10的整次幂位),也曾因此而登上《科学美国人》法文版。

4.6.2 计算高精度平方根

package main

import (
    "fmt"
    "math"
    "math/big"
)

func main() {
    // 我们将在尾数中以 200 位精度进行计算。
    const prec = 200

    // 使用牛顿法计算 2 的平方根。我们从
    // 对 sqrt(2) 的初始估计,然后迭代:
    //     x_{n+1} = 1/2 * ( x_n + (2.0 / x_n) )

    // 由于牛顿法将每个正确数字的数量加倍
    // 迭代,我们至少需要 log_2(prec) 个步骤。
    steps := int(math.Log2(prec))

    // 初始化计算所需的值。
    two := new(big.Float).SetPrec(prec).SetInt64(2)
    half := new(big.Float).SetPrec(prec).SetFloat64(0.5)

    // 使用 1 作为初始估计。
    x := new(big.Float).SetPrec(prec).SetInt64(1)

    // 我们使用 t 作为临时变量。无需设置其精度
    // 因为 big.Float 值未设置 (== 0) 精度自动假设
    // 作为结果使用时参数的最大精度(接收器)
    // 一个 big.Float 操作。
    t := new(big.Float)

    // Iterate.
    for i := 0; i <= steps; i++ {
        t.Quo(two, x)  // t = 2.0 / x_n
        t.Add(x, t)    // t = x_n + (2.0 / x_n)
        x.Mul(half, t) // x_{n+1} = 0.5 * t
    }

    // 我们可以使用通常的 fmt.Printf 动词,因为 big.Float 实现了 fmt.Formatter
    fmt.Printf("sqrt(2) = %.50f\n", x)

    // 打印 2 和 x*x 之间的误差。
    t.Mul(x, x) // t = x*x
    fmt.Printf("error = %e\n", t.Sub(two, t))
}

4.7 键-值存储

简化版本的键-值存储,主要满足下面4项基本任务:

  1. 添加新元素 ADD
  2. 根据某个键从键-值存储中删除某个现有元素 DELETE
  3. 在存储中查找特定的值 LOOKUP
  4. 修改现有键的值 CHANGE

本质就是crud,这里使用原生的Go map来实现K-V存储,因为内置的数据结构往往执行效率更高。map变量被声明为全局变量,其k为string类型,v为myElement类型,myElement是自定义的结构体。

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)

type myElement struct {
    Name    string
    SurName string
    Id      string
}

var DATA = make(map[string]myElement)



func ADD(k string, n myElement) bool {
    if k == "" {
        return false
    }
    if LOOKUP(k) == nil {
        DATA[k] = n
        return true
    }
    return false
}

func DELETE(k string) bool {
    if LOOKUP(k) != nil {
        delete(DATA, k)
        return true
    }
    return false
}

//这部分代码实现了ADD和DELETE两个功能,用户在执行ADD命令时,如果没有携带足够的参数,我们要保证该操作不会失败,意味着myElement中对应的字段为空字符串。然而如果你要添加的key已经存在了,就会报错(K-V存储不允许重复key出现)而不是修改对应的值。

func LOOKUP(k string) *myElement {
    _, ok := DATA[k]
    if ok {
        n := DATA[k]
        return &n
    } else {
        return nil
    }
}

func CHANGE(k string, n myElement) bool {
    DATA[k] = n
    return true
}

func PRINT() {
    for k, v := range DATA {
        fmt.Printf("key: %s value: %v", k, v)
    }
}

// 该代码段实现了LOOKUP与CHANGE功能。如果你要修改的key不存储,程序会自动将其存储。PRINT命令能够打印出目前所有K-V存储的全部内容。

func main() {
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        text := scanner.Text()
        text = strings.TrimSpace(text)
        tokens := strings.Fields(text)

        switch len(tokens) {
        case 0:
            continue
        case 1:
            tokens = append(tokens, "")    // 补齐ADD操作所需数量
            tokens = append(tokens, "")
            tokens = append(tokens, "")
            tokens = append(tokens, "")
        case 2:
            tokens = append(tokens, "")
            tokens = append(tokens, "")
            tokens = append(tokens, "")
        case 3:
            tokens = append(tokens, "")
            tokens = append(tokens, "")
        case 4:
            tokens = append(tokens, "")
        }
        switch tokens[0] {
        case "PRINT":
            PRINT()
        case "STOP":
            return
        case "DELETE":
            if !DELETE(tokens[1]) {
                fmt.Println("Delete operations failed")
            }
        case "ADD":
            n := myElement{tokens[2], tokens[3], tokens[4]}
            if !ADD(tokens[1], n) {
                fmt.Println("Add operation failed")
            }
        case "LOOKUP":
            n := LOOKUP(tokens[1])
            if n != nil {
                fmt.Printf("%v\n", n)
            }

        case "CHANGE":
            n := myElement{tokens[2], tokens[3], tokens[4]}
            if !CHANGE(tokens[1], n) {
                fmt.Println("Update operation failed")
            }

        default:
            fmt.Println("Unknown command - please try again!")
        }
    }
}

可以通过增加goroutines和channel来改善这个程序。当然,在单用户应用使用goroutines和channel是没有任何实际意义的,但是如果你是通过TCP/IP来操作K-V存储,那么使用goroutines和channel会帮助你实现多连接、服务多用户功能。

4.8 JSON文件操作

JSON是一种非常流行的,基于文本的格式,它被设计用来在JavaScript系统之间传递信息。当然,JSON也可以用来为应用创建配置文件,以结构化的格式存储信息。

标准库包encoding/json提供了Encode()Decode()两个函数,可以使用这两个函数在JSON文本和Go对象之间相互转换。另外,这个包来提供了Marshal()Unmarshal()两个函数,这两个函数和Encode()Decode()的工作方式是相似,也确实是基于这两个函数的。这两对函数之间最主要的区别是Encode()Decode()处理单个对象,而后者可以处理多个对象或者字节流。

4.8.1 JSON读取

readMe.json文件

{
  "Name":"Mihalis",
  "Surname":"Tsoukalos",
  "Tel":[
    {"Mobile":true,"Number":"1234-567"},
    {"Mobile":true,"Number":"1234-abcd"},
    {"Mobile":false,"Number":"abcc-567"}
  ]
}

readJSON.go文件

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Record struct {
    Name    string
    Surname string
    Tel     []Telephone
}
type Telephone struct {
    Mobile bool
    Number string
}

func loadFromJSON(filename string, key interface{}) error {
    in, err := os.Open(filename)
    if err != nil {
        return err
    }
    decodeJSON := json.NewDecoder(in)
    err = decodeJSON.Decode(key)
    if err != nil {
        return err
    }
    in.Close()
    return nil
}

func main() {
    filename := "readMe.json"
    var myRecord Record
    err := loadFromJSON(filename, &myRecord)
    if err == nil {
        fmt.Println(myRecord)
    } else {
        fmt.Println(err)
    }
}

其中func loadFromJSON(filename string, key interface{})这个函数用第二个参数给定的数据结构来解码JSON文件中的数据。关于接口interface{}后面可能会提及,亦可查看how-to-use-interfaces-in-go先行了解。

4.8.2 保存JSON

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Record struct {
    Name    string
    Surname string
    Tel     []Telephone
}
type Telephone struct {
    Mobile bool
    Number string
}

func saveToJSON(filename *os.File, key interface{}) {
    encodeJSON := json.NewEncoder(filename)
    err := encodeJSON.Encode(key)
    if err != nil {
        fmt.Println(err)
        return
    }
}

func main() {
    myRecord := Record{
        Name:    "Mihalis",
        Surname: "Tsoukalos",
        Tel: []Telephone{Telephone{Mobile: true, Number: "1234-567"},
            Telephone{Mobile: true, Number: "1234-abcd"},
            Telephone{Mobile: false, Number: "abcc-567"},
        },
    }
    saveToJSON(os.Stdout, myRecord)
}

这个程序会在标准输出(os.Stdout)中写入JSON数据,即直接写在屏幕上。

4.8.3 Marshal()和Unmarshal()与JSON数据解析

通过查看Encode()与Decode()源码可以知道,其核心功能依托于Marshal()和Unmarshal()。

这里读写无结构的JSON数据。这里有很重要的一点需要记住:无结构的JSON数据是和之前的处理不一样,并不是放到结构体中,而是放到Go map映射中的。

noStr.json

{
  "Name": "John",
  "Surname": "Doe",
  "Age": 25,
  "Parents": [
    "Jim",
    "Mary"
  ],
  "Tel":[
    {"Mobile":true,"Number":"1234-567"},
    {"Mobile":true,"Number":"1234-abcd"},
    {"Mobile":false,"Number":"abcc-567"}
  ]
}

parsingJSON.go

package main

import (
   "encoding/json"
   "fmt"
   "os"
)

func main() {
   //arguments := os.Args
   //if len(arguments) == 1 {
   // fmt.Println("Please provide a filename!")
   // return
   //}
   //filename := arguments[1]
   filename := "noStr.json"
   fileData, err := os.ReadFile(filename)    // 一次性读取文件
   if err != nil {
      fmt.Println(err)
      return
   }
   var parsedData map[string]interface{}
   json.Unmarshal([]byte(fileData), &parsedData)
   for key, value := range parsedData {
      fmt.Println("key:", key, "value:", value)
   }
}

// Output
// key: Age value: 25
// key: Parents value: [Jim Mary]
// key: Tel value: [map[Mobile:true Number:1234-567] map[Mobile:true Number:1234-abcd] map[Mobile:false Number:abcc-567]]
// key: Name value: John
//key: Surname value: Doe

通过output可以看到,结果随机顺序输出。

4.9 XML文件操作

XML是一种与HTML类似的标记语言。

4.9.1 读取固定格式的XML

与JSON读取相似。

data.xml

<?xml version="1.0" encoding="UTF-8"?>
<Record>
    <Name>Dimitris</Name>
    <Surname>Tsoukalos</Surname>
    <Tel>
        <Mobile>true</Mobile>
        <Number>1234-567</Number>
    </Tel>
    <Tel>
        <Mobile>true</Mobile>
        <Number>1234-abcd</Number>
    </Tel>
    <Tel>
        <Mobile>false</Mobile>
        <Number>abcc-567</Number>
    </Tel>
</Record>

readXML.go

package main

import (
    "encoding/xml"
    "fmt"
    "os"
)

type Record struct {
    Name    string
    Surname string
    Tel     []Telephone
}
type Telephone struct {
    Mobile bool
    Number string
}

func loadFromXML(filename string, key interface{}) error {
    in, err := os.Open(filename)
    if err != nil {
        return err
    }
    decodeXML := xml.NewDecoder(in)
    err = decodeXML.Decode(key)
    if err != nil {
        return err
    }
    in.Close()
    return nil
}
func main() {
    //arguments := os.Args
    //if len(arguments) == 1 {
    //  fmt.Println("Please provide a filename!")
    //  return
    //}
    //filename := arguments[1]
    filename := "data.xml"
    var myRecord Record
    err := loadFromXML(filename, &myRecord)
    if err == nil {
        fmt.Println("XML:", myRecord)
    } else {
        fmt.Println(err)
    }
}

4.9.2 JSON转XML

这一部分包括固定格式的XML解析与保存及与Json的相互转换。

package main

import (
    "encoding/json"
    "encoding/xml"
    "fmt"
    "os"
)

type Record struct {
    Name    string
    Surname string
    Tel     []Telephone
}
type Telephone struct {
    Mobile bool
    Number string
}

func loadFromJSON(filename string, key interface{}) error {
    in, err := os.Open(filename)
    if err != nil {
        return err
    }
    decodeJSON := json.NewDecoder(in)
    err = decodeJSON.Decode(key)
    if err != nil {
        return err
    }
    in.Close()
    return nil
}

func main() {
    //arguments := os.Args
    //if len(arguments) == 1 {
    //  fmt.Println("Please provide a filename!")
    //  return
    //}
    //filename := arguments[1]
    filename := "readMe.json"
    var myRecord Record
    err := loadFromJSON(filename, &myRecord)
    if err == nil {
        fmt.Println("JSON:", myRecord)
    } else {
        fmt.Println(err)
    }

    myRecord.Name = "Dimitris"  //改变一个数据

    xmlData, _ := xml.MarshalIndent(myRecord, "", " ") //转换数据 类似于Json的Marshal()
    xmlData = []byte(xml.Header + string(xmlData)) // 添加文件头
    fmt.Println("\nxmlData:", string(xmlData))
    data := &Record{}
    err = xml.Unmarshal(xmlData, data) // 解析XML
    if nil != err {
        fmt.Println("Unmarshalling from XML", err)
        return
    }
    result, err := json.Marshal(data) // 保存为Json
    if nil != err {
        fmt.Println("Error marshalling to JSON", err)
        return
    }
    _ = json.Unmarshal([]byte(result), &myRecord) //解析保存的Json
    fmt.Println("\nJSON:", myRecord)
}

4.9.3 自定义XML

这里演示如何调整和自定义生成后的XML输出结果,处于简单考虑,这里以硬编码实现。

package main

import (
    "encoding/xml"
    "fmt"
    "os"
)

func main() {
    type myStr struct {
        myStr1, myStr2 string
    }
    type XmlBody struct {
        XMLName  xml.Name `xml:"myName"`
        Id       int      `xml:"id,attr"`             //字段名 这里对应id
        Initials string   `xml:"myInitials>initials"` //initials嵌入myInitials
        Id2      int      `xml:"id2,omitempty"`       //"omitempty" 忽略空值字段 空值包括 0、false、nil指针或接口,以及0长度的数组、切片、映射或字符串
        myStr
        Comment string `xml:",comment"` //带有`",comment"`表示为一个注释且对应输出结果中的格式。
    }
    r := &XmlBody{
        Id:       7,
        Initials: "init",
        Id2:      0,
    }
    r.Comment = "Test +DevOps"
    r.myStr = myStr{"str1", "str2"}
    output, err := xml.MarshalIndent(r, " ", " ")
    if err != nil {
        fmt.Println("Error:", err)
    }
    output = []byte(xml.Header + string(output))
    _, err = os.Stdout.Write(output)
    if err != nil {
        return
    }
    _, err = os.Stdout.Write([]byte("\n"))
    if err != nil {
        return
    }
}

// Output 
// <?xml version="1.0" encoding="UTF-8"?>
// <myName id="7">
//  <myInitials>
//   <initials>init</initials>
//  </myInitials>
//  <!--Test +DevOps-->
// </myName>

4.10 YAML格式

YAML(YAML Ain’t Markup Language)不是一种标记语言,是一种简洁易懂的文件格式,比如其巧妙避开各种封闭符号,如:引号、各种括号等,这些符号在嵌套结构中会变得复杂而难以辨认。

Golang标准库并支持YAML,如果需要使用可以使用go-yaml/yaml: YAML support for the Go languagespf13/viper: Go configuration with fangs

参考文献

[1] 《精通Go语言(第二版)》米哈里斯·托卡洛斯

[2] Mastering_Go_ZH_CN

[3] Mastering_Go_Second_Edition_ZH_CN