《Go语言圣经》笔记(1-4章)

第一章、入门

  • 第一章的命令行部分最后学!先学后面

1、hello world

  • Go不需要加分号
  • “{” 符号必须和关键字func在同一行,不能独自成行。并且在 x+y 这个表达式中,换行符可以在+操作符的后面,但是不能在+操作符的前面
  • 为了在一个包下的多个文件能同时编写main方法,可以在无用的文件前面加
//go:build ignore
// +build ignore

2、 命令行参数

  • os包提供一些函数和变量。命令行参数以os包中Args名字的变量供程序访问,在os包外面,使用os.Args这个名字。变量os.Args是一个字符串slice。
  • slice可以理解为一个动态容量的顺序数组s。
    • 通过s[i]来访问单个元素
    • 通过s[m:n]来访问一段连续子区间
      • s[m:n]表示一个从第m个到第n-1个元素的slice
      • os.Args[1 : len(os.Args)]
      • 如果m或n缺失,默认分别是0或len(s)。故上式可以简写为os.Args[1:]。
    • 数组长度用len(s)表示
  • 注释以//开头,写在一个包的声明之前
  • j = i++是不合法的,并且仅支持后缀,所以–i也是不合法的
  • for是Go里的唯一循环语句
for initialization; condition; post{
    //...
}

//传统的while循环
for condition{
    //...
}

//传统的无限循环
for{
    //...
}
  • 循环的索引变量i在for循环开始处声明。:= 符号用于短变量声明
func main() {
	var s, sep string
	for i := 1; i < len(os.Args); i++ {
		s += sep + os.Args[i]
		sep = " "
	}
	fmt.Println(s)
}
  • Go不允许存在无用的临时变量,解决办法是可以使用空标识符:“_”(即下划线)
func main() {
	s, sep := "", ""
	for _, arg := range os.Args[1:] {
		s += sep + arg
		sep = ""
	}
	fmt.Println(s)
	fmt.Print(2)
}
  • range用法补充
/*
	Go 语言中 range 关键字用于
		·for循环中迭代数组(array)、切片(slice)、链表(channel)或集合(map)的元素;
		·在数组和切片中它返回元素的索引值,
		·在集合中返回 key-value 对的 key 值。
*/
package main

import "fmt"

func main() {
	//使用range去求一个slice的和。类似于使用数组
	nums := []int{2, 3, 4}
	sum := 0
	for _, num := range nums {
		sum += num
	}
	fmt.Println("sum:", sum)
	//在数组上使用range将传入index和值两个变量
	for i, num := range nums {
		if num == 3 {
			fmt.Println("index:", i)
		}
	}
	//range也可以用在map的键值对上
	kvs := map[string]string{"a": "apple", "b": "banana"}
	for k, v := range kvs {
		fmt.Println("%s -> %s\n", k, v)
	}
	//range也可以用来枚举Unicode字符串。
	//第一个参数是字符的索引,第二个是字符(Unicode的值)本身
	for i, c := range "go" {
		fmt.Println(i, c)
	}
}

  • 以下几种声明字符串变量的方式是等价的:
s := ""
var s string
var s = ""
var s string = ""
  • 一个包下只能有一个main()方法,可以在不需要编译的文件前面加上 // +build ignore
  • 如果连接涉及的数据量很大, 这种方式代价高昂。 一种简单且高效的解决方案是使用 strings 包的 Join 函数:
func main() {
	fmt.Println(strings.Join(os.Args[1:], " "))
}
  • 最后, 如果不关心输出格式, 只想看看输出值, 或许只是为了调试, 可以用 Println 为我们格式化输出
fmt.Println(os.Args[1:])
  • 这条语句的输出结果跟 strings.Join 得到的结果很像, 只是被放到了一对方括号里。 切片都会被打印成这种格式。

3、找出重复行

dup1
//dup1输出标准输入中出现次数大于1的行,前面是次数
func main() {	
	//内置的函数make可以用来新建map
	counts := make(map[string]int)
	//map 存储一个键/值对集合,结构为map[key]value
	//key支持能进行相等(==)比较的任意类型;
	//值可以是任意类型
	//这个例子中,键的类型是字符串,值是int
	input := bufio.NewScanner(os.Stdin)
	//bufio包创建一个标准输入流
	for input.Scan() {
		//等价于:counts[input.Text()]++
		line := input.Text()
		if line == "bye" {
			break //输入bye时结束输入
		}
		counts[line]++
	}
	//注意:忽略input.Err()中可能的错误
	for line, n := range counts { //使用range遍历输入
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, line)
			//f代表格式化输出format
			//用来格式化的函数都会在末尾以f字母结尾(译 注:f后对应format或fmt缩写),
			//比如log.Printf,fmt.Errorf,同时还有一系列对应以ln结尾的函数(译注:ln后对应line			缩写),这些函数默认以%v来格式化他们的参数,并且会在输出结束后在 后自动加上一个换行符。

		}
	}
}

运行示例
在这里插入图片描述

  • Printf函数有超过10个这样的转义字符,Go程序员称为verb
  • 制表符 \t 换行符 \n
    在这里插入图片描述
dup2
  • 许多程序既可以像dup一样从标准输入进行读取,也可以从具体的文件读取。这个版本的dup程序可以从标准输入或一个文件列表进行读取,使用os.Open函数来逐个打开
//dup2 打印输入中多次出现的行的个数和文本
package main
import (
	"bufio"
	"fmt"
	"os"
)
func main() {
	counts := make(map[string]int)
	files := os.Args[1:] //获取参数中的文件名
	if len(files) == 0 {
		countLines(os.Stdin, counts)
	} else {
		for _, arg := range files {
			f, err := os.Open(arg)
			if err != nil {
				fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
				continue //如果产生错误,则终止本次循环,然后继续
			}
			countLines(f, counts)
			f.Close()
		}
	}
	for line, n := range counts {
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, line)
		}
	}
}

func countLines(f *os.File, counts map[string]int) {
	input := bufio.NewScanner(f)
	for input.Scan() {
		counts[input.Text()]++
	}
	//注意:忽略input.Err()中可能的错误
}

运行示例
在这里插入图片描述

  • 这个版本的dup使用“流式”模式读取输入,然后按需拆分为行,这样原理上程序可以处理海量的输入。
dup3
  • 这个版本的dup将一次读取整个输入到大块内存,一次性地分割所有行,然后处理这些行。
  • ReadFile函数(来自io/ioutil包):读取整个命名文件的内容
  • strings.Split函数:将一个字符串分割为一个由子串组成的slice(其中Split是strings.Join的反操作)
  • dup3的简化之处:
    • 仅读取指定的文件,而非标准输入,因为ReadFile需要一个文件名作为参数
    • 将统计行数的工作放回main函数,因为它当前仅在一处用到
package main
import (
	"fmt"
	"io/ioutil"
	"os"
	"strings"
)
func main() {
	counts := make(map[string]int)
	for _, filename := range os.Args[1:] {
		data, err := ioutil.ReadFile(filename)
		//ReadFile函数返回一个可以转化为字符串的字节slice
		//这样它可以被strings.Split分割
		if err != nil {
			fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
			continue
		}
		for _, line := range strings.Split(string(data), "\n") {
			counts[line]++
		}
	}
	for line, n := range counts {
		if n > 1 {
			fmt.Println("%d\t%s\n", n, line)
		}
	}
}

练习
  • 修改dup2,出现重复的行时打印文件名称
package main
import (
	"bufio"
	"fmt"
	"os"
)
type LnFile struct {
	Count     int
	FileNames []string
}
func main() {
	counts := make(map[string]*LnFile)
	files := os.Args[1:]
	if len(files) == 0 {
		countLine(os.Stdin, counts)
	} else {
		for _, arg := range files {
			f, err := os.Open(arg)
			if err != nil {
				fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
				continue
			}
			countLine(f, counts)
			f.Close()
		}
	}
	for line, n := range counts {
		if n.Count > 1 {
			fmt.Printf("%d\t%v\t%s\n", n.Count, n.FileNames, line)
		}
	}
}

func countLine(f *os.File, counts map[string]*LnFile) {
	input := bufio.NewScanner(f)
	for input.Scan() {
		key := input.Text()
		_, ok := counts[key]
		if ok {
			counts[key].Count++
			counts[key].FileNames = append(counts[key].FileNames, f.Name())
		} else {
			counts[key] = new(LnFile)
			counts[key].Count = 1
			counts[key].FileNames = append(counts[key].FileNames, f.Name())
		}
	}
}

运行示例
在这里插入图片描述

4、GIF动画

//lissajous 产生随机利萨茹图形的GIF动画
package main
import (
	"bytes"
	"image"
	"image/color"
	"image/gif"
	"io"
	"io/ioutil"
	"math"
	"math/rand"
	"time"
)
var palette = []color.Color{color.White, color.Black}
const (
	whiteIndex = 0 // 画板中的第一种颜色
	blackIndex = 1 // 画板中的下一种颜色
)
func main() {
	//rand.Seed(time.Now().UTC().UnixNano())
	//if len(os.Args) > 1 && os.Args[1] == "web" {
	//	handler := func(w http.ResponseWriter, r *http.Request) {
	//		lissajous(w)
	//	}
	//	http.HandleFunc("/", handler)
	//	log.Fatal(http.ListenAndServe("localhost:8000", nil))
	//	return
	//}
	//lissajous(os.Stdout)
	
	//如果是rand.Int,此时获取的随机数,都是重复的一些随机数据
	//下面这种写法使用了当前时间播种伪随机数生成器,可以保证每次随机都是随机的
	rand.Seed(time.Now().UTC().UnixNano())
	//改为直接输出字节文件
	buf := &bytes.Buffer{}
	lissajous(buf)
	if err := ioutil.WriteFile("output.gif", buf.Bytes(), 0666); err != nil {
		panic(err)
	}
}
func lissajous(out io.Writer) {
	const ( //const用于给常量命名
		cycles  = 5     // 完整的x振荡器变化的个数
		res     = 0.001 // 角度分辨率
		size    = 100   // 图象画布包含 [-size..+size]
		nframes = 64    // 动画中的帧数
		delay   = 8     // 以10ms为单位的帧间延迟
	)
	freq := rand.Float64() * 3.0 // y振荡器的相对频率
	anim := gif.GIF{LoopCount: nframes}
	phase := 0.0 // phase difference 相位差
	for i := 0; i < nframes; i++ {
		rect := image.Rect(0, 0, 2*size+1, 2*size+1)
		img := image.NewPaletted(rect, palette)
		for t := 0.0; t < cycles*2*math.Pi; t += res {
			x := math.Sin(t)
			y := math.Sin(t*freq + phase)
			img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
				blackIndex)
		}
		phase += 0.1
		//append的作用:在原切片末尾添加元素
		anim.Delay = append(anim.Delay, delay)
		anim.Image = append(anim.Image, img)
	}
	gif.EncodeAll(out, &anim) // 注意:忽略编码错误
}

运行示例
在这里插入图片描述

练习
  • 练习1.5:修改前面的Lissajous程序里的调色板,由黑色改为绿色。我们可以用 color.RGBA{0xRR, 0xGG, 0xBB, 0xff}来得到 #RRGGBB 的色值,三个十六进制的字符串分别代表红、绿、蓝像素
var palette = []color.Color{color.RGBA{0, 0, 0, 0xFF}, 
color.RGBA{0, 0xFF, 0, 0xFF}}

运行示例
在这里插入图片描述

  • 练习1.6:修改Lissajous程序,修改其调色板来生成更丰富的颜色,然后修改SetColorIndex的第三个参数,看看显示结果吧
package main

import (
	"bytes"
	"image"
	"image/color"
	"image/gif"
	"io"
	"io/ioutil"
	"math"
	"math/rand"
	"time"
)

const nColors = 10

func main() {
	seed := time.Now()
	//Unix将t作为Unix时间返回,即自UTC时间1970年1月1日起经过的秒数。结果不依赖于与t相关联的位置。
	//类unix操作系统通常将时间记录为32位的秒数,但是由于这里的方法返回一个64位的值,它在过去或未来的数十亿年里都是有效的。
	rand.Seed(seed.Unix())

	var palette []color.Color //palette调色板
	for i := 0; i < nColors; i++ {
		r := uint8(rand.Uint32() % 256)
		g := uint8(rand.Uint32() % 256)
		b := uint8(rand.Uint32() % 256)
		palette = append(palette, color.RGBA{r, g, b, 0xff})
	}

	buf := &bytes.Buffer{}
	lissajous1(buf, palette)
	if err := ioutil.WriteFile("output2.gif", buf.Bytes(), 0666); err != nil {
		panic(err)
	}
}

func lissajous1(out io.Writer, palette []color.Color) {
	const (
		cycles  = 5
		res     = 0.001
		size    = 100
		nframes = 64
		delay   = 8
	)
	freq := rand.Float64() * 3.0
	anim := gif.GIF{LoopCount: nframes}
	phase := 0.0
	for i := 0; i < nframes; i++ {
		rect := image.Rect(0, 0, 2*size+1, 2*size+1)
		img := image.NewPaletted(rect, palette)
		nColor := uint8(i % len(palette))
		for t := 0.0; t < cycles*2*math.Pi; t += res {
			x := math.Sin(t)
			y := math.Sin(t*freq + phase)
			img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5), nColor)
		}
		phase += 0.1
		anim.Delay = append(anim.Delay, delay)
		anim.Image = append(anim.Image, img)
	}
	gif.EncodeAll(out, &anim)
}

运行示例
在这里插入图片描述

5、获取一个URL

  • 程序 fetch 展示从互联网获取信息的最小需求,它获取每个指定URL的内容,然后不加解析的输出。
  • http.Get 函数产生一个 HTTP 请求
    • 如果没有出错,返回结果存在响应结构 resp 里面,其中 resp 的 Body 域包含服务器端响应的一个可读取数据流
    • 利用 ioutil.ReadAll 可以读取 Body 字段中的全部内容
    • Body.Close 用于关闭 Body 流来避免资源泄露,并使用Printf将响应输出到标准输出
  • os.Exit 函数终止进程,并且返回一个 status 状态错误码,其值为1

第二章 程序结构

2.1、命名

  • 区分大小写
  • 开头可以是字母或下划线
  • 关键字
    在这里插入图片描述
    在这里插入图片描述
  • 如果一个实体在包中、函数外声明
    • 以大写字母开头,可导出
    • 小写字母和下划线开头的,不可导出
  • 推荐风格:
    • 短小+驼峰
    • 像ASCII和HTML这样的首字母缩写词通常使用相同的大小写

2.2、声明

  • 主要有4个类型:变量(var)、常量(const)、类型(type)和函数(func)

2.3、变量

  • var 声明的通用形式:

var name type = expression
var 变量名 类型 = 表达式

  • 类型和表达式最多省略一个
    • 如果表达式省略,其初始值对应于类型的零值:数字是0,布尔值是false,字符串是 “”,接口和引用类型(slice、指针、map、通道、函数)是nil
    • 如果类型省略,它的类型由初始化表达式决定
    • 忽略类型允许声明多个不同类型的变量
var i, j, k int					// int, int, int
var b, f, s = true, 2.3, "four"	// bool, float64, string
  • 一组变量可以通过调用返回多个值的函数进行初始化
//os.Open("name")
//注意使用的是“/”,而不是“\”
//os.Open 返回一个文件和一个错误
var f, err = os.Open("E:/space/workspace/src/Project/output.gif")
2.3.1、短变量声明
  • 作用:用于声明和初始化局部变量
  • 形式:

name := expression
name 的类型由 expression 的类型决定

  • var声明通常用于和初始化表达式类型不一致的局部变量,或者用于后面才对变量赋值以及初始值无关紧要的情况。
i := 100		// 一个 int 类型的变量
var boiling float64 = 100	//一个 float64 类型的变量

var name []string
var err error
var p Point
  • 与 var 声明一样,多个变量可以以短变量声明的方式声明和初始化:
i, j := 0, 1
  • 注意,:= 表示声明,而 = 表示赋值
  • 区分多变量的声明和多重赋值。多重赋值指的是将右边的值赋给左边的对应变量:
i, j = j, i		// 交换 i 和 j 的值
  • 注意:短变量声明不需要声明所有在左边的变量,如下:
in, err := os.Open(infile)
//...
out, err := os.Create(outfile)	//可以通过
  • 但最少声明一个新变量,否则代码编译将无法通过:
f, err := os.Open(infile)
//...
f, err := os.Create(outfile)	//编译错误:没有新的变量
2.3.2、指针
  • 如果一个变量声明为 var x int,表达式 &x(x 的地址)可以获取一个指向整型变量的指针,它类型为整型指针(*int)。*p 表示 p 指向的变量即 x。
x := 1
p := &x				// p 是整型指针,指向 x
fmt.Println(*p)		// "1"
*p = 2				// 等于 x = 2
fmt.Println(x)		// 结果 "2"
  • 指针类型的零值是 nil。测试 p != nil,结果是 true 说明 p 指向一个变量。
  • 指针是可比较的,两个指针当且仅当指向同一个变量或者两者都是 nil 的情况下才相等
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil)	// "true false false"
  • 函数返回局部变量的地址是非常安全的。如下面的代码中,调用 f 产生的局部变量 v 一直存在,指针 p 也依然引用它:
var p = f()
func f() *int {	//返回值类型为 *int
	v := 1
	return &v
}
//每次调用f都会返回一个不同的值:
fmt.Println(f() == f())	// "false"
  • 传递一个指针参数给函数,能够让函数更新指针所指的变量的值
func incr(p *int) int{
	*p++	// 递增 p 所指向的值;p 自身保持不变
	return *p
}
v := 1
incr(&v)				// 副作用:v 现在等于 2
fmt.Println(incr(&v))	// "3"(v 现在是 3)
  • 输出命令行参数
// echo4 输出其命令行参数
package main
import (
	"flag"
	"fmt"
	"strings"
)
//flag.Bool 函数创建一个新的布尔类型标志变量
//有三个参数:标志的名字("n"),变量的默认值(false),
//以及当用户提供非法标识、非法参数抑或 -h 或 -help 参数时输出的消息
//变量 sep 和 n 是指向标志变量的指针,它们必须通过 *sep 和 *n 来访问
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")

func main() {
	//在使用标识前,必须调用 flag.Parse 来更新标识变量的默认值。
	flag.Parse()
	fmt.Print(strings.Join(flag.Args(), *sep))
	if !*n {
		fmt.Println()
	}
}
  • windows需要使用 .\name.exe 才能运行,如果是在系统的命令行,则不需要加".\"
    在这里插入图片描述
    在这里插入图片描述
2.3.3 、new函数
  • 表达式new(T)

    • 创建一个T类型的匿名变量
    • 该匿名变量的初始值为T类型的零值
    • 返回类型为 *T 的地址
  • 对比指针变量声明和用new创建变量的区别

    • 指针变量声明后的初始值是nil
    • 使用new则会给对象赋值,值为对应类型的零值
	p := new(int)
	var p2 *int
	fmt.Println(p)  //某个内存地址:0xc0000160a8
	fmt.Println(*p) //0
	fmt.Println(p2) //<nil>
	fmt.Println(*p2)//panic: runtime error: invalid memory address or nil pointer dereference
  • 使用 new 创建的变量和取其地址的普通局部变量没什么区别,只是不需要引入(和声明)一个虚拟的名字,通过 new(T) 就能直接使用。如下面两种方式是等价的
func newInt() *int {	//方式一
	return new(int)
}

func newInt() *int {	//方式二
	var dummy int 
	return &dummy
}
  • 每次调用 new(T) 都会返回一个具有唯一地址的不同变量:
  • 除非两个变量的类型不携带任何类型且是零值,如 struct{} 或 [0]int
p := new(int)
q := new(int)
fmt.Println(p == q)		//"false"
2.3.4 、变量的生命周期
  • 逃逸现象:局部变量(如下面的x)在函数(f)返回后还可以从全局变量(global)访问
var global *int
func f() {
	var x int
	x = 1
	global = &x
}
  • 逃逸的变量需要额外分配内存,会影响性能优化
2.4、赋值
  • 通常函数使用额外的返回值来指示一些错误情况,例如
    • 通过 os.Open 返回的 error 类型

    f, err = os.Open(“foo.txt”)

    • 一个通常叫做 ok 的 bool 类型变量
    • 有三个操作符也会有类似的行为:如果map查询、类型断言或通道接收动作出现在两个结果的赋值语句中,都会产生一个额外的布尔型结果

    v, ok = m[key] // map 查询
    v, ok = x.(T) // 类型断言
    v, ok = < - ch // 通道接收

    • 如变量声明,可以将不需要的值赋给空标识符:

    _, err = io.Copy(dst, src) // 丢弃字节个数
    _, ok = x.(T) // 检查类型但丢弃结果

2.5、类型
  • 不同命名类型的值不能直接比较,如下面的 t2 和 t3
package main

import "fmt"

type a1 int
type a2 int

func main(){
	//t1 := 1
	var t2 a1
	var t3 a2
	fmt.Println(t2 == 0)		// true
	fmt.Println(t3 >= 0)		// true
	//fmt.Println(t2 == t1)		// invalid operation: t2 == t1 (mismatched types a1 and int)
	//fmt.Println(t2 == t3)		// invalid operation: t2 == t3 (mismatched types a1 and a2)
	fmt.Println(t2 == a1(t3))	// true
}
2.6、包和文件

导入包

  • 导入包的路径要从根路径开始
  • 通过大小写的导出规则,实现外部的可见性
    • 大写开头外部可见
    • 小写开头不可见
  • 导入包却没有使用将被当作一个编译错误

包的初始化

  • 首先解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化
  • 如果包由多个 .go 文件组成,初始化按照编译器收到文件的顺序进行
  • 可以使用Init函数做初始化工作,它们会按照声明顺序执行

PopCount 函数

  • 快查表版
package main

import "fmt"

// pc[i] 是 i 的种群统计
// 统计一个数字中,值为1的位的个数
var pc [256]byte

// init()将256种byte的取值对应的1的位数提前存在pc[i]中
func init() {
	for i, _ := range pc {
		// byte(i&1) 判断最低位是否为1,然后递归调用统计1的个数
		pc[i] = pc[i/2] + byte(i&1)
	}
}

func PopCount(x uint64) int {
	return int(pc[byte(x>>(0*8))] +
		pc[byte(x>>(1*8))] +
		pc[byte(x>>(2*8))] +
		pc[byte(x>>(3*8))] +
		pc[byte(x>>(4*8))] +
		pc[byte(x>>(5*8))] +
		pc[byte(x>>(6*8))] +
		pc[byte(x>>(7*8))])
}
  • 练习 2.3: 重写 PopCoun t函数,用一个循环代替单一的表达式。
func PopCountByLoop(x uint64) int {
	n := 0
	for i := byte(0); i < 8; i++ {
		n += int(pc[byte(x>>(i*8))])
	}
	return n
}
  • 练习 2.4: 用移位算法重写PopCount函数,每次测试最右边的1bit,然后统计总数。

func PopCountByBitShift(x uint64) int {
    n := 0
    for i := uint(0); i < 64; i++ {
        if (x>>i)&1 != 0 {
            n++
        }
    }
    return n
}
  • 练习 2.5: 表达式x&(x-1)用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数
func PopCountByBitClear(x uint64) int {
    n := 0
    for x != 0 {
        x = x & (x - 1)
        n++
    }
    return n
}
2.7、作用域
  • 声明的作用域是声明在程序文本中出现的区域,它是一个编译时属性
  • 变量的生命周期是变量在程序执行期间能被程序的其他部分所引用的起止时间,它是一个运行时属性
  • 语法块是由花括弧所包含的一系列语句。语法块内部声明的名字无法被外部访问,也就是说语法快决定了内部声明名字的作用域
  • 当编译器遇到一个名字的引用时,将从最内层的封闭词法块到全局块寻找其声明。
    • 如果没有找到,它会报 “undeclared name” 错误
    • 如果在内层和外层块都存在这个声明,内层的将先被找到。这时,内层声明将覆盖外部声明,使它不可访问,如下:
func f() {}
var g = "g"
func main() {
	f := "f"
	fmt.Println(f)	//"f";局部变量f覆盖了包级函数 f
	fmt.Println(g)	//"g";包级变量
	fmt.Println(h)	//编译错误:undeclared name;未定义 h
}
  • if 语句的条件部分声明的变量,作用域仅在 if 语句中,所以 f 不能被接下来的语句访问,错误案例如下:
	if f, err := os.Open(fname); err != nil{ // 编译错误:未使用f
		return err
	}
	f.Stat()	// 编译错误:未定义f
	f.Close()	// 编译错误:未定义f
  • 所以通常需要在条件判断之前声明 f,使其在 if 语句后面可以访问
	f, err := os.Open(fname)
	if err != nil{
		return err
	}
	f.Stat()
	f.Close()
  • 如果希望避免在外部块中声明 f 和 err,方法是将 Stat 和 Close 的调用放到 else 块中
if f, err := os.Open(fname); err != nil{
		return err
	} else {
		// f 与 err 在这里可见
		f.Stat()
		f.Close()
	}

第三章 基本数据

3.1、整数

  • int 和 uint 都对应特定平台上的运算效率最高的值
  • rune 类型是 int32 类型的同义词,常用于指明一个值是 Unicode 码点(code point)
  • byte 类型是 uint8 类型的同义词,强调一个值是原始数据,而非量值
  • 无符号整数 uintptr,其大小不明确但足以完整存放指针
  • 有符号整数以补码表示,保留最高位作为符号位
  • 下列循环中用到的内置的 len 函数,返回的是有符号整数:
    • 假若 len 返回的结果是无符号整数,就会导致严重错误,因为 i 随之也成为 uint 类型,条件 i >= 0 将恒成立
    • 且第 3 轮迭代后,有 i==0,语句 i-- 将使得 i 变成 uint 型的最大值(例如可能为 264-1),而非 -1 。导致 medals[i] 试图越界访问元素,超出 slice 范围,引发运行失败或宕机。
medals := []string{"gold", "silver", "bronze"}
	for i := len(medals) - 1; i >= 0; i-- {
		fmt.Println(medals[i])	// "gold", "silver", "bronze"
	}

3.2、浮点数

  • 特殊值(math 包提供)
    • 正无穷大(+Inf):表示超出最大许可值的数
    • 负无穷大(- Inf):表示除以零的商
    • NaN(Not a Number):表示数学上无意义的运算结果(如 0/0 或 Sqrt(-1))

练习3.4:构建一个 Web 服务器,计算并生成曲面,同时将 SVG 数据写入客户端。服务器必须如下设置 Content-Type 报头
w.Header().Set(“Content-Type”, “image/svg+xml”)

//浏览器url后面添加eggbox/saddle 会出现对应的图案,默认是雪堆状
package main

import (
"fmt"
"io"
"log"
"math"
"net/http"
)

const (
	width, height = 600, 320            //画布大小
	cells         = 100                 //单元格的个数
	xyrange       = 30.0                //坐标轴的范围(-xyrnage..+xyrange)
	xyscale       = width / 2 / xyrange //x或y轴上每个单位长度的像素
	zscale        = height * 0.5        //z轴上每个单位长度的像素
	angle         = math.Pi / 6         //x、y轴的角度(=30°)
)

var sin30, cos30 = math.Sin(angle), math.Cos(angle) // sin(30°),cos(30°)
type zFunc func(x, y float64) float64

func main() {
	http.HandleFunc("/", handler)
	http.HandleFunc("/eggbox", eggboxs)
	http.HandleFunc("/saddle", saddles)
	log.Fatal(http.ListenAndServe("localhost:1234", nil))

}

func handler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "image/svg+xml")
	surface(w, "f")
}
func eggboxs(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "image/svg+xml")
	surface(w, "eggbox")
}
func saddles(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "image/svg+xml")
	surface(w, "saddle")
}

func surface(w io.Writer, fName string) {
	var fn zFunc
	switch fName {
	case "saddle":
		fn = saddle
	case "eggbox":
		fn = eggbox
	default:
		fn = f
	}
	z_min, z_max := min_max(fn)
	fmt.Fprintf(w, "<svg xmlns='http://www.w3.org/2000/svg' "+
		"style='fill: white; stroke-width:0.7' "+
		"width='%d' height='%d'>", width, height)
	for i := 0; i < cells; i++ {
		for j := 0; j < cells; j ++ {
			ax, ay := corner(i+1, j, fn)
			bx, by := corner(i, j, fn)
			cx, cy := corner(i, j+1, fn)
			dx, dy := corner(i+1, j+1, fn)
			if math.IsNaN(ax) || math.IsNaN(ay) || math.IsNaN(bx) || math.IsNaN(by) || math.IsNaN(cx) || math.IsNaN(cy) || math.IsNaN(dx) || math.IsNaN(dy) {
				continue
			} else {
				fmt.Fprintf(w, "<polygon style='stroke: %s;' points='%g,%g %g,%g %g,%g %g,%g'/>\n",
					color(i, j, z_min, z_max), ax, ay, bx, by, cx, cy, dx, dy)
			}
		}
	}
	fmt.Fprintln(w, "</svg>")
}

// minmax返回给定x和y的最小值/最大值并假设为方域的z的最小值和最大值。
func min_max(f zFunc) (min, max float64) {
	min = math.NaN()
	max = math.NaN()
	for i := 0; i < cells; i++ {
		for j := 0; j < cells; j++ {
			for xoff := 0; xoff <= 1; xoff++ {
				for yoff := 0; yoff <= 1; yoff++ {
					x := xyrange * (float64(i+xoff)/cells - 0.5)
					y := xyrange * (float64(j+yoff)/cells - 0.5)
					z := f(x, y)
					if math.IsNaN(min) || z < min {
						min = z
					}
					if math.IsNaN(max) || z > max {
						max = z
					}
				}
			}
		}
	}
	return min, max
}

func color(i, j int, zmin, zmax float64) string {
	min := math.NaN()
	max := math.NaN()
	for xoff := 0; xoff <= 1; xoff++ {
		for yoff := 0; yoff <= 1; yoff++ {
			x := xyrange * (float64(i+xoff)/cells - 0.5)
			y := xyrange * (float64(j+yoff)/cells - 0.5)
			z := f(x, y)
			if math.IsNaN(min) || z < min {
				min = z
			}
			if math.IsNaN(max) || z > max {
				max = z
			}
		}
	}

	color := ""
	if math.Abs(max) > math.Abs(min) {
		red := math.Exp(math.Abs(max)) / math.Exp(math.Abs(zmax)) * 255
		if red > 255 {
			red = 255
		}
		color = fmt.Sprintf("#%02x0000", int(red))
	} else {
		blue := math.Exp(math.Abs(min)) / math.Exp(math.Abs(zmin)) * 255
		if blue > 255 {
			blue = 255
		}
		color = fmt.Sprintf("#0000%02x", int(blue))
	}
	return color
}

func corner(i, j int, f zFunc) (float64, float64) {
	//求出网格单元(i,j)的顶点坐标(x,y)
	x := xyrange * (float64(i)/cells - 0.5)
	y := xyrange * (float64(j)/cells - 0.5)

	//计算曲面高度z
	z := f(x, y)
	//将(x, y, z)等角投射到二维SVG绘图平面上,坐标是(sx, sy)
	sx := width/2 + (x-y)*cos30*xyscale
	sy := height/2 + (x+y)*sin30*xyscale - z*zscale
	return sx, sy
}

func f(x, y float64) float64 {
	r := math.Hypot(x, y) //到(0,0)的距离
	return math.Sin(r) / r
}

func eggbox(x, y float64) float64 { //鸡蛋盒
	r := 0.2 * (math.Cos(x) + math.Cos(y))
	return r
}

func saddle(x, y float64) float64 { //马鞍
	a := 25.0
	b := 17.0
	a2 := a * a
	b2 := b * b
	r := y*y/a2 - x*x/b2
	return r
}
  • 运行结果:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3.3、复数

  • 几个内置函数:
    • 1.complex:构建复数
    • 2.real:返回复数的实部
    • 3.imag:返回复数的虚部

3.4、布尔值

  • 逻辑运算(&&,|| 等)中,可能会有短路行为(如 s[0] == ‘’ 作用于空字符串):如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不被求值

3.5、字符串

  • 因为字符串不可改变,所以字符串内部的数据不允许修改:
s[0] = 'L'	// 编译错误:s[0] 无法赋值
  • 不可变意味着两个字符串能安全地共用同一段底层内存,使得复制任何长度字符串的开销都低廉。
  • 类似地,字符串 s 及其子串(如 s[7:] 可以安全地共用数据)
  • 这两种情况下都没有分配新内存。如下图:
    在这里插入图片描述
3.5.1、字符串字面量
转义字符

在这里插入图片描述

  • 一个原生的字符串面值是用一对反引号 `` 来标识的,此时转义序列不起作用
3.5.2、Unicode
  • Unicode 囊括了世界上所有文书体系的全部字符,uinicode 码点在 Go 中称为:文字符号,即 rune 类型(int32)
3.5.3、UTF-8
  • 当 []rune 转换作用于 UTF-8 编码的字符串时,返回其 Unicode 码点序列:
// 日本片假名"程序"
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"
  • 如果把文字符号类型的 slice 转换成一个字符串,会输出各个文字符号的 UTF-8 编码的拼接结果:
fmt.Println(String(r))	// "プログラム"
  • 若将一个整数值转换成字符串,其值按文字符号类型解读,并产生对应的 UTF-8 码
fmt.Println(String(65))	// "A",而不是"65"
fmt.Println(String(r))	// "京"
  • 如果文字符号值非法,将被专门的替换字符取代(‘\uFFFD’,显示为:�)
fmt.Println(String(1234567))	// "�"
3.5.4、字符串和字节 slice
  • 标准库对字符串处理的包:
    • strings
    • bytes 包有许多函数用于操作字节 slice([]byte 类型)。用于字符串不可变,因此按增量方式构建字符串会导致多次内存分配和复制,这种情况下,使用 bytes.Buffer 类型会更高效
    • strconv 包:提供了布尔型,整型数,浮点数和对应字符串的相互转换,还有双引号转义相关的转换
    • unicode 包:备有判别文字符号值特性的函数,如:IsDigit,IsLetter,IsUpper,IsLower。每个函数以单个文字符号值(rune类型)作为参数,并返回一个布尔值。
  • 字符串可以和字节 slice 相互转换:
s := "abc"
b := []byte(s)
s2 := string(b)
  • []byte(s) 转换操作会分配新的字节数组,拷贝填入 s 含有的字节,并生成一个 slice 引用,指向整个数组。
  • bytes 包为高效处理字节 slice 提供了 Buffer 类型,下例中 bytes.Buffer 变量无须初始化,零值本就有效
package main

import (
	"bytes"
	"fmt"
)

// intsToString 与 fmt.Sprint(values)类似,但插入了逗号
func intsToString(values []int) string {
	var buf bytes.Buffer
	buf.WriteByte('[')
	for i, v := range values {
		if i > 0 {
			buf.WriteString(", ")
		}
		fmt.Fprintf(&buf, "%d", v)
	}
	buf.WriteByte(']')
	return buf.String()
}

func main() {
	fmt.Println(intsToString([]int{1, 2, 3}))	// "[1, 2, 3]"
}
  • 若要在 bytes.Buffer 变量后面:
    • 添加文字符号的 UTF-8 编码,使用 WriteRune 方法
    • 追加 ASCII 字符,如 ‘[’ 和 ‘]’,则使用 WriteByte 亦可

练习 3.10: 编写一个非递归版本的comma函数,使用 bytes.Buffer 代替字符串链接操作。

package main

import (
	"bytes"
)

func comma(s string) string {
	var buffer bytes.Buffer
	l := len(s)
	for i := 0; i < len(s); i++ {
		buffer.WriteString(string(s[i]))

		if (i+1)%3 == l%3 {	// 取余3可以得到第一个插入逗号的位置,后面依次+3即可
			buffer.WriteString(",")
		}
	}
	s = buffer.String()
	return s[:len(s)-1]	// 末尾会多一个逗号,去掉
}

练习 3.12: 编写一个函数,判断两个字符串是否是是相互打乱的,也就是说它们有着相同的字符,但是对应不同的顺序。

package main

func isReverse(a, b string) bool{
	// 长度不一样直接返回 false
	if len(a) != len(b) {
		return false
	}
	// 用于记录每个字符串出现的次数
	m := make(map[rune]int)
	n := make(map[rune]int)
	// 以字符串Unicode码作为map的Key
	for _, v := range a {
		m[v]++
	}
	for _, v := range b {
		n[v]++
	}
	// 判断相同下标值是否相同
	for i, v := range m {
		if n[i] != v {
			return false
		}
	}
	return true
}
3.5.5、字符串和数字的相互转换
  • 方法一:使用 fmt.Sprintf
  • 方法二:用函数 strconv.Itoa(…) “integer to ASCII”
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x))	// "123 123"
  • FomatInt和FormatUint可以转换成不同进制格式,也可以使用fmt.Printf格式化
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
s := fmt.Sprintf("x=%b", x) // "x=1111011"
  • strconv 包内的 Atoi 函数或 ParseInt 函数用于解释表示整数的字符串,而 ParseUint 用于无符号整数:
x, err := strconv.Atoi("123")		// x 是整型
y, err := strconv.ParseInt("123", 10, 64)	//十进制,最长为64位

3.6、常量

  • 若同时声明一组常量,除了第一项之外,其他项在等号右侧的表达式都可以省略,意味着复用前一项的表达式及其类型
const (
	a = 1
	b
	c = 2
	d
)
3.6.1、常量生成器 iota
  • 在第一个声明的常量所在的行,iota将会被置为0,然后每一个有常量声明的行加1
  • 常常用于声明枚举型的常量,如下:
type Weekday int
const (
	Sunday Weekday = iota	// 0
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)
  • Go 语言没有指数运算符
3.6.2、无类型常量
  • 许多常量并不从属于某一具体类型,这些值数字精度更高。
  • 借助推迟确定从属类型,无类型常量不仅能暂时维持更高的精度,还能写进更多表达而无需转换类型
  • 例如:浮点型常量 math.Pi 可用于任何需要浮点值或复数的地方:
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

第四章 复合数据类型

4.1、数组

  • …省略号表示数组的长度是根据初始化值的个数决定的
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
  • 数组的长度是数组类型的一个组成部分,[3]int和[4]int不是相同的数组类型
  • 类型相同的数组可以互相比较
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [2]int{1, 3}
fmt.Println(a == b, a == c, b == c) // "true false false"
d := [3]int{1, 2}
fmt.Println(a == d) // 编译错误,无法比较 [2]int == [3]int
  • 调用一个函数时,若数组是参数,在函数内修改的只是参数的副本,而不是参数本身。若要在函数内部修改数组,需要使用指针。
  • 将一个数组 [32]byte 的元素清零:
func zero(ptr *[32]byte) {
	for i := range ptr {
		prt[i] = 0
	}
}
  • 数组字面量 [32]byte{} 可以生成一个拥有32个字节元素的数组,默认的值都是0,故可利用这点写另一个方法
func zero(ptr *[32]byte){
	*ptr = [32]byte{}
}

4.2、slice

  • 如果 slice 的引用超过了被引用对象的容量,即 cap(s),会导致程序宕机
  • 如果 slice 的引用超出了被引用对象的长度,即 len(s),那么最终 slice 会比原 slice 长
  • 示例
	summer := months[6:9]
	fmt.Println(summer)	 // ["June" "July" "August"]
	
	fmt.Println(summer[:20])	// 宕机:超出了被引用对象的边界
	endlessSummer := summer[:5]	// 在 slice 容量范围内扩展了 slice
	fmt.Println(endlessSummer)	// ["June" "July" "August" "September" "October"]
  • 字符串的切片操作还是字符串,实际上都是字符串而不是切片,也就是常量,可以使用len但是不能用cap
  • 反转函数
package main
import "fmt"
// 就地反转一个整型 slice 中的元素
func reverse(s []int) {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
}

// 将一个 slice 左移 n 个元素的方法是连续调用 reverse 函数三次
// 第一次反转前 n 个元素,第二次反转剩下的元素,最后整个反转
func shift(s []int, n int)  {
	reverse(s[:n])
	reverse(s[n:])
	reverse(s)
}

func main() {
	a := [...]int{0, 1, 2, 3, 4, 5}
	reverse(a[:])
	fmt.Println(a)	// [5 4 3 2 1 0]
	
	// 向左移动两个元素
	shift(a[:], 2)
	fmt.Println(a)	// [3 2 1 0 5 4]
}
  • slice 无法做比较,因此不能用 == 来测试两个 slice 是否有相同的元素
  • 可以使用 bytes.Equal 来比较两个字节 slice([]byte),但是其他类型的比较函数必须自己写,以 string 类型为例。
func equal(x, y []string) bool {
	if len(x) != len(y) {
		return false
	}
	for i := range x {
		if x[i] != y[i] {
			return false
		}
	}
	return true
}
  • 检查一个 slice 是否为空,应该使用 len(s) == 0,而不是 s == nil,因为 s != nil 的情况下,slice 也有可能为空
var s []int		// len(s) == 0, s == nil
s = nil			// len(s) == 0, s == nil
s = []int(nil)	// len(s) == 0, s == nil
s = []int{}		// len(s) == 0, s == nil
4.2.1、append 函数
  • 当原来的 slice 空间不足时,函数会分配一个长度加倍的新数组,并将数据都复制到新数组中,最后再追加新元素 i。
  • 可知,我们并不清楚一次 append 调用会不会导致一次新的内存分配,所以我们不能假设原始的 slice 和调用 append 后的结果 slice 指向同一个底层数组。
4.2.2、slice 内存技巧
  • 从 slice 的中间移除一个元素,并保留剩余元素的顺序
func remove(slice []int, i int) []int {
	copy(slice[i:], slice[i+1:])
	return slice[:len(slice)-1]
}
func main() {
	s := []int{5, 6, 7, 8, 9}
	fmt.Println(remove(s, 2))	// "[5 6 8 9]"
}

练习 4.4: 编写一个 rotate 函数,通过一次循环完成旋转。

  • 解题思路:
    • 重新创建一个数组,新数组下标为原数组下标加上偏移值
    • 如果超出最大长度则从最左边开始
func rotate(s []int, r int) []int {
	lens := len(s)
	arr := make([]int, lens)
	for k := range s {
		index := r + k
		if index >= lens {
			index -= lens
		}
		arr[k] = s[index]
	}
	return arr
}

练习 4.5: 写一个函数在原地完成消除[]string 中相邻重复的字符串的操作。

  • 解题思路:
    • 遇到相同的先前移一位
    • 取 len(s)-1 的部分,继续检测
func Remove(str []string) []string {
	for i := 0; i < len(str)-1; i++ {
		if str[i] == str[i+1] {
			copy(str[i:], str[i+1:])
			str = str[:len(str)-1]
			i--
		}
	}
	return str
}

4.3、map

  • 如果需要排序,需要显式使用sorts包对key的值进行排序,相当于创建一个和key相同类型的slice
import "sort"
var names []string
for name := range ages {
	names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
	fmt.Printf("%s\t%d\n", name, ages[name])
}
  • 程序 dedup 读取一系列的行,并且只输出每个不同行一次
func main() {
	seen := make(map[string]bool)	// 字符串集合
	input := bufio.NewScanner(os.Stdin)
	for input.Scan() {
		line := input.Text()
		if !seen[line] {
			seen[line] = true
			fmt.Println(line)
		}
	}
	if err := input.Err(); err != nil {
		fmt.Fprintf(os.Stderr, "dedup:%v\n", err)
		os.Exit(1)
	}
}
4.4.3、结构体嵌套和匿名成员
  • 此时程序看起来清晰,但是访问 wheel 的成员比较麻烦
type Point struct {
	X, Y int
}
type Circle struct {
	Center Point
	Radius int
}
type Wheel struct {
	Circle Circle
	Spokes int
}

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20
  • Go 允许我们定义不带名称的结构体成员,只需要指定类型即可;这种结构体成员称作匿名成员。这个结构体成员的类型必须是一个命名类型或者指向命名类型的指针。
type Point struct {
	X, Y int
}
type Circle struct {
	Point
	Radius int
}
type Wheel struct {
	Circle
	Spokes int
}

var w Wheel
w.X = 8 // 等价于 w.Circle.Point.X = 8
w.Y = 8 // 等价于 w.Circle.Point.Y = 8
w.Radius = 5 // 等价于 w.Circle.Radius = 5
w.Spokes = 20
  • 有嵌入成员的结构体,在初始化时,必须显式写明
w = Wheel{Circle{Point{8, 8}, 5}, 20}
w = Wheel{
Circle: Circle{
	Point:
		Point{X: 8, Y: 8},
		Radius: 5,
	},
	Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}
  • 因为 “匿名成员” 拥有隐式的名字,所以不能在一个结构体里面定义两个相同类型的匿名成员,否则会引起冲突。
  • 在 Go 中,组合是面向对象编程方式的核心。

4.5、JSON

  • JavaScript 对象表示法(JSON)是一种发送和接收结构化信息的标准(由包encoding/json支持)
  • JSON 是 JavaScript 值的 Unicode 编码
  • JSON 的数组是一个有序的元素序列,每个元素之间用逗号分隔,两边使用方括号括起来。
    • JSON 的数组用于编码 Go 里面的数组和 slice。
  • JSON 的对象是一个从字符串到值的映射,写出 name:value 对的序列,每个元素之间用逗号分隔,两边用花括号。
    • JSON 的对象用于编码 Go 里面的 map(键为字符串类型)和结构体
  • 例子:
boolean true
number -273.15
string "She said \"Hello, BF\""
array ["gold", "silver", "bronze"]
object {"year": 1980,
		"event": "archery",
		"medals": ["gold", "silver", "bronze"]}
  • encoding/json的基本操作:
    • 1.Marshal:把 Go 的数据结构转换为 JSON
    • 2.Unmarshal:将 JSON 字符串解码为 Go 数据结构
    • 3.MarshalIndent:输出整齐格式化的结果
    • 4.Decoder
    • 5.Encoder
  • 某收集电影信息的程序的结构体
type Movie struct {
	Title string
	Year int `json:"released"`
	Color bool `json:"color,omitempty"`
	Actors []string
}

var movies = []Movie{
	{Title: "电影1号", Year: 1999, Color: false,
		Actors: []string{"1号演员甲", "1号演员乙"}},
	{Title: "电影2号", Year: 2000, Color: true,
		Actors: []string{"2号演员丙"}},
	{Title: "电影3号", Year: 2001, Color: true,
		Actors: []string{"3号演员丁", "3号演员戊"}},
}

marshal

  • Marshal 生成了一个字节 slice,包含一个不含多余空白字符的很长的字符串
[{"Title":"电影1号","released":1999,"Actors":["1号演员甲","1号演员乙"]},{"Title":"电影2号","released":2000,"color":true,"Actors":["2号演员丙"]},{"Title":"电影3号","released":2001,"color":true,"Actors":["3号演员丁","3号演员戊"]}]

格式化输出 MarshalIndent

  • 这个函数有两个参数,一个是定义每行输出的前缀字符串,另一个是定义缩进的字符串
func main() {
	data, err := json.MarshalIndent(movies, "", "   ")
	if err != nil {
		log.Fatalf("JSON marshaling failed: %s", err)
	}
	fmt.Printf("%s\n", data)
}
  • 输出结果:
[
   {
      "Title": "电影1号",
      "released": 1999,
      "Actors": [
         "1号演员甲",
         "1号演员乙"
      ]
   },
   {
      "Title": "电影2号",
      "released": 2000,
      "color": true,
      "Actors": [
         "2号演员丙"
      ]
   },
   {
      "Title": "电影3号",
      "released": 2001,
      "color": true,
      "Actors": [
         "3号演员丁",
         "3号演员戊"
      ]
   }
]
  • 可见,上面的结构体成员 Year 对应地转换为 released ,另外 Color 转换为 color,这就是通过成员标签定义(field tag)实现的
  • 成员标签定义一般由遗传由空格分开的标签键值对 key:"value" 组成
  • Color 的标签还有一个额外的选项:omitempty,它表示如果这个成员的值是零值或者为空,则不输出这个成员到 JSON 中

unmarshal

	var titles []struct{ Title string }
	if err := json.Unmarshal(data, &titles); err != nil {
		log.Fatalf("JSON unmarshaling failed: %s", err)
	}
	fmt.Println(titles)		// [{电影1号} {电影2号} {电影3号}]
  • 很多的 Web 服务都提供 JSON 接口,通过发送 HTTP 请求来获取想要得到的 JSON 信息
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值