【学习笔记】Golang语法学习笔记

一、入门

go是编译型的语言,代码风格类似于C语言,其最大特点是支持并发编程,go文件后缀名为.go

在命令行通过go run helloworld.go来运行,或先通过go build helloworld.go编译,然后./helloworld执行,在windows下编译生成的是.exe文件。go mod tidy命令拉取缺少的模块,移除不用的模块

go语言代码的第一行使用package声明包,名为main的包比较特殊,它定义的是一个独立的可执行程序,而不是库。在 main 里的 main 函数也很特殊,它是整个程序执行时的入口。使用import导入使用的包,import必须跟在package声明之后。缺少或多了不需要的包,编译都不会通过。

go语言不需要用分号结尾,除非多个语句或声明出现在同一行。实际上在特定符号后面的换行符会被转换为分号,所以在什么地方换行会影响对go代码的解析,例如{必须和关键字func在同一行,不能独立成行。

字符串必须是双引号,单行注释是//,多行注释是/**/

package main

import (
	"fmt"
	"os"
)

func main() {
	var s, sep string
	sep = " "
	// os.Args是通过命令行运行代码时传入的各个参数,第0个参数默认是本代码的路径
	for i := 0; i < len(os.Args); i++ {
		s = s + sep + os.Args[i]
	}
	fmt.Println(s)
}

i++i--是语句而非表达式,故j = i++是非法的,且只支持后缀,++i也是非法的。

go语言中只有for循环,for循环有以下几种形式

for initialization; condition; post{
	// 语句
}

// 相当于C语言中的while循环
for condition{
    // 语句
}

// 死循环
for{
    // 语句
}

// rang产生一对值:索引和索引处的元素值
for _,arg := range os.Args[1:]{
    // 语句
}

go语言中不允许存在无用的变量,即声明了但是没用到的变量,所以索引值用不到时不能用普通的变量名代替,而是需要使用空标识符_

定义变量的几种方式,建议使用第一、二种

s := ""
var s string
var s = ""
var a, b string = "aaa", "bbb"

string包中string.Join(os.Args[1:], " ")可以使用空格将os.Args中的元素连接起来

格式化字符:

符号含义
%d十进制整数
%x, %o, %b十六进制,八进制,二进制整数
%f, %g, %e浮点数
%t布尔值
%c字符
%s字符串
%q带双引号的字符串或带单引号的字符
%v变量的自然形式
%T变量的类型
%%字面上的百分号标志

例:找出重复行

package main

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

func main() {
	// make可以创建新的map,map[string]int表示一个键为string类型,值为int类型的map
	counts := make(map[string]int)
	// bufio中的Scanner可以读入输入,以换行分割,以Ctrl+D结束
	input := bufio.NewScanner(os.Stdin)
	for input.Scan() {
		counts[input.Text()]++
	}
	for i, n := range counts {
		if n > 1 {
			// 格式化输出
			fmt.Printf("%d\t%s\n", n, i)
		}
	}
}

例:从stdin或指定文件读取并找出重复行

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)
			// err为nil时表示成功打开
			if err != nil {
				fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
				continue
			}
			countLines(f, counts)
			f.Close()
		}
	}
	for i, n := range counts {
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, i)
		}
	}
}

// 函数和其他包级别的实体可以任意次序声明
func countLines(f *os.File, counts map[string]int) {
	input:=bufio.NewScanner(f)
	for input.Scan() {
		// 更改子函数中的counts值会影响到main函数中counts的值
		counts[input.Text()]++
	}
}

输出从url获取的内容

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
)

func main() {
	for _, url := range os.Args[1:] {
        // 产生一个http请求
		resp, err := http.Get(url)
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
        // 读取响应的响应体
		b, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
			os.Exit(1)
		}
		fmt.Printf("%s\n", b)
	}
}

并发获取多个url

package main

import (
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

func main() {
	start := time.Now()
	// 创建一个字符串通道,通道是允许某一例程向另一例程传递指定类型的值的通信机制
	ch := make(chan string)
	for _, url := range os.Args[1:] {
		// main函数在一个goroutine中执行,go语句创建额外的goroutine,即一个并发执行的函数
		go fetch(url, ch)
	}
	for range os.Args[1:] {
		// <-ch是接收发送的值
		fmt.Println(<-ch)
	}
	fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
	start := time.Now()
	resp, err := http.Get(url)
	if err != nil {
		// 在通道ch上发送一个值
		ch <- fmt.Sprint(err)
		return
	}

	// io.Copy获取响应内容,并通过ioutil.Discard输出流进行丢弃
	nbytes, err := io.Copy(ioutil.Discard, resp.Body)
	resp.Body.Close()
	if err != nil {
		ch <- fmt.Sprintf("while reading %s: %v\n", url, err)
		return
	}
	secs := time.Since(start).Seconds()
	ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}

迷你web服务器

package main

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

func main()  {
	// 回声请求调用处理程序
	http.HandleFunc("/",handler)
	log.Fatal(http.ListenAndServe("localhost:8000",nil))
}

func handler(w http.ResponseWriter,r *http.Request)  {
	// 回显请求URL r的路径部分
	fmt.Fprintf(w,"URL.Path = %q\n",r.URL.Path)
}

迷你回声和计数服务器

package main

import (
	"fmt"
	"log"
	"net/http"
	"sync"
)

var mu sync.Mutex
var count int

func main() {
	http.HandleFunc("/", handler)
	http.HandleFunc("/count", counter)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
	// 加锁
	mu.Lock()
	count++
	mu.Unlock()
	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}

func counter(w http.ResponseWriter, r *http.Request) {
	mu.Lock()
	fmt.Fprintf(w, "Count %d\n", count)
	mu.Unlock()
}

使用&操作符获取一个变量的地址,使用*操作符获取指针引用的变量的值。

二、程序结构

1. 名称

go中有25个关键字:

breakdefalutfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

内建常量:

true false iota nil

内建类型:

int int8 int16 int32 int64

uint uint8 uint16 uint32 uint64 uintptr

float32 float64 complex128 complex64

bool byte rune string error

内建函数:

make len cap new append copy close delete

complex real imag

panic recover

go语言中区分大小写,如果实体(方法或变量)名称以大写字母开头,则它对包外是可见和可访问的,如fmt中的Printf。包名总是由小写字母组成。变量名通常采用驼峰命名

2. 声明

声明的作用是给一个程序实体命名,并设定其部分或全部属性

实体有4个主要的声明:变量(var)、常量(const)、类型(type)和函数(func)

3. 变量

变量声明的方式为:var 变量名 数据类型 = 表达式,类型和表达式可以省略一个,但不可全省略。如果省略类型,则类型由表达式决定;如果表达式省略,则其初始值对应类型的零值,接口和引用类型的零值是nil。

var a int
var b int = 100
var c = 100

包级别的初始化在main开始之前进行

声明多个变量

var a, b int = 100, 200
var c, d = 100, "abcd"
var (
	e int = 100
    f bool = true
)
g, h := 100, "abcd"

(1) 短变量声明

格式为:变量名 := 表达式

在局部变量中主要使用短变量声明

短变量声明至少声明一个新变量,否则会编译错误,如

f, err := os.Open(infile)
// 编译错误,因没有新的变量
f, err := os.Create(outfile)

:= 是声明并赋值,并且系统自动推断类型,不需要var关键字,而=必须先使用var声明

(2) 指针

指向整数变量的指针的数据类型是*int

x := 1
p := &x
// *p是指针指向的变量值
*p = 2
*p++ // *p指向的变量值加1
package main

import "fmt"

func swap(a, b *int) {
	*a, *b = *b, *a
}

func main() {
	a := 10
	b := 20
	swap(&a, &b)
	fmt.Println("a = ", a, " b = ", b)

	// 一级指针
	var p *int
	p = &a

	// 二级指针
	var pp **int
	pp = &p
	fmt.Println(&p)
	fmt.Println(pp)
}

(3) new函数和make函数

表达式new(T)创建一的T类型的匿名变量,初始化为T类型的零值,并返回其地址。

p := new(int) // *int(指针)类型的p
fmt.Println(*p)
*p = 2

表达式make(T)创建一个T类型的匿名变量,初始化为T类型的零值,并返回该变量。与new的不同之处在于,new返回的是变量地址,而make返回的是变量本身。
make的形式必须是make(Type, len, cap),且Type的值只能是slicemapchannel三者之一。

make([]int, 2)

make([]int, 2, 4)

make(map[string]string)

make(chan, int)

make(chan, int, 1)

(4) 赋值

x = 1
*p = true
person.name = "bob" // 结构体成员
connt[x] = count[x] * scale // connt[x]为数组或slice或map的元素
// 多重赋值
x, y = y, x
i, j, k = 1, 2, 3
medals := []string{"gold", "silver", "bronze"}

(5) 类型声明

type声明定义一个新的名称类型,它和某个已有类型使用同样的底层类型。其格式为:type 类型名 底层类型

package tempconv

import "fmt"

type Celsius float64
type Fahrenheit float64

const (
	AbsoluteZeroC Celsius = -273.15
	FreeezingC    Celsius = 0
	BoilingC      Celsius = 100
)

func CToF(c Celsius) Fahrenheit {
    // 强制类型转换
	return Fahrenheit(c*9/5 + 32)
}

func FToC(f Fahrenheit) Celsius {
	return Celsius((f - 32) * 5 / 9)
}

其中Celsius和Fahrenheit虽然底层类型都是float,但是它们不能进行比较和合并,Celsius()和Fahrenheit()表示强制类型转换。如果两个类型具有相同的底层类型,或都是指向相同底层类型的指针类型,则二者是可以相互转换的。

(6) 包和文件

通过标出的标识符是否以大写字母开头来管理标识符是否对外可见。

包的初始化按照在程序中导入的顺序来进行,依赖顺序优先,每次初始化一个包。

三、基本数据

1. 整数

rune类型是int32类型的同义词,byte类型是uint8类型的同义词

取模余数正负号总是与被除数一致

运算符优先级从高到低如下:

*  /  %  <<  >>  &  &^
+  -  |  ^
==  !=  <  <=  >  >=
&&
||

其中&^表示位清空(AND NOT),在z = x &^ y中,若y的某位是1,则z的对应位为0,反之为x的对应位。

2. 浮点数

float32和float64

3. 复数

复数有complex64和complex128两种,可以使用real()和imag()分别提取复数的实部和虚部

x := 1 + 2i

4. 布尔值

一个布尔类型的值只有两种:true和false

5. 字符串

字符串是不可变的字节序列,但可以做字符串拼接。

s := "hello,world"
t := "!!!"
fmt.Println(len(s))
fmt.Println(s[0],s[7])
s += t
s[0] = '0' // 报错,不可更改

bytes、strings、strconv、unicode四个标准包对字符串操作特别重要

  • strings包提供了搜索、替换、比较、修整、切分和连接的函数

  • bytes包里也类似,用于操作字节slice

  • strconv提供布尔值、整数、浮点数与字符串类型之间的相互转换函数

  • unicode包有判别文字符号值特性的函数,如IsDigit、IsLetter、IsUpper、IsLower等

6. 常量

const p = 3.14
const(
    e = 2.71828
    pi = 3.1415926
)

// 其中b和d会复用前一项的表达式及其类型
const(
  	a = 1
    b
    c = 2
    d
)

// 常量生成器itoa,从0开始取值,逐项加1
type Weekday int
const(
  	Sunday Weeday = itoa
    Monday
    Tuestday
    Wednesday
    Thursday
    Friday
    Saturday
)

四、复合数据结构

1. 数组

长度固定,且拥有多个相同数据类型的元素。

// 默认初始化为零值
var a [3]int
var b [3]int = [3]int{1,2,3}
length := len(a) // 内置函数len()可以获取数组大小
// 遍历数组中的元素
for _, v := range b{
    fmt.Printf("%d\n",v)
}

// 用...表示数组长度由初始化元素个数决定,此时变量类型为数组,而非切片
c := [...]int{1,2,3}
fmt.Printf("%T\n",c)

// 如果一个数组的元素是可比较的,则数组是可比较的,如果数组长度不同不可比较
fmt.Println(b == c)
package main

import "fmt"

type Currency int

const (
	ZERO Currency = iota
	ONE
	TWO
	THREE
	FOUR
)

func main() {
	// 索引:值
	num := [...]int{ZERO: 0, ONE: 1, TWO: 2, THREE: 3, FOUR: 4}
	fmt.Println(TWO, num[TWO])

	// 定义了100个元素的数组,其中下标为99的元素为-1,其他默认为0
	a := [...]int{99: -1}
	fmt.Println(a)
}

在调用函数时,传入的参数会创建一个副本,函数内对数组的任何修改都仅影响副本,此时可以使用引用传递(传递数组的地址)来解决问题

package main

import "fmt"

// 传入的数组必须是长度为4的
func printArray(myArray [4]int) {
	for index, value := range myArray {
		fmt.Println("index = ", index, ", value = ", value)
	}
	// 只是值拷贝,不会影响原数组的值
	myArray[0] = 111
}

func main() {
	// 固定长度的数组
	var myArray1 [10]int
	myArray2 := [10]int{1, 2, 3, 4}
	myArray3 := [4]int{1, 2, 3, 4}

	for i := 0; i < len(myArray1); i++ {
		fmt.Println(myArray1[i])
	}

	for index, value := range myArray2 {
		fmt.Println("index = ", index, ", value = ", value)
	}

	fmt.Printf("myArray1 types = %T\n", myArray1)
	fmt.Printf("myArray2 types = %T\n", myArray2)
	fmt.Printf("myArray3 types = %T\n", myArray3)

	printArray(myArray3)
	fmt.Println("myArray3[0] = ", myArray3[0])
}

(1) 常量生成器iota

const (
    // iota只能用在const中
    // 第一行的iota为默认值0,每行的iota值会累加1
	BEIJING = 10 * iota // 0
    SHANGHAI            // 10
    SHENZHEN            // 20
)

const (
	a, b = iota + 1, iota + 2 // iota = 0, a = 1, b = 2
    c, d                      // iota = 1, a = 2, b = 3
    e, f                      // iota = 2, a = 3, b = 4
    
    g, h = iota * 2, iota * 3 // iota = 3, a = 6, b = 9
    i, k                      // iota = 4, a = 8, b = 12
)

2. slice

slice表示拥有相同类型元素的可变长度的序列,slice类型写作[]T,其中Tslice中元素类型

slice是一种轻量级的数据结构,可以用来访问数组的元素,这个数组成为slice的底层数组。

slice有是三个属性:指针、长度和容量。指针指向数组的第一个可从slice中访问的元素,但并不一定是第一个元素;长度是slice中的元素个数,不能超过容量。内置的len()cap()可以返回slice的长度和容量。

slice的切片操作s[i,j]用来创建一个新的slice。切片截取的时候使用的是同一个底层切片,改变一个的值,另一个也会变,可以使用copy()函数进行深拷贝。

a := []int{1,2,3,4,5}
// a[i:j],其中i,j都可以不写,不写时i默认为0,j默认为len(a)-1
b := a[1:4] // 前闭后开

因为slice包含了指向数组的指针,所以将slice传递给函数的时候,可以在函数内部修改底层数组的元素

package main

import "fmt"

func printArray(myArray []int) {
    // 引用传递
    // _ 表示匿名的变量,可以不用
    for _, value := range myArray {
        fmt.Println("value = ", value)
    }
    // 对动态数组修改会改变数组原来的值
    myArray[0] = 100
}

func main() {
    // 动态数组,切片,slice
    myArray := []int{1, 2, 3, 4}
    
    printArray(myArray)
    fmt.Println("myArray[0] = ", myArray[0])   
}

slice和数组的初始化的不同在于slice没有在[]中指明长度,所以这样创建的是指向数组的slice

和数组不同的是slice无法作比较,slice唯一允许的比较操作就是和nil作比较,若为nil则表示没有对应的底层数组

可以用make([]T, len, cap)来创建一个指定元素类型、长度和容量的slicecap参数可省略,此时caplen相等。当切片的长度不为0时,前len个元素都为默认值。

(2) 切片的四种定义方式

package main

import "fmt"

func main() {
    // 声明slice1是一个切片,并初始化,长度是3
    // slice1 := []int{1, 2, 3}
    
    // 声明slice1是一个切片,但没有给slice分配空间
    // var slice1 []int
    // 开辟3个空间,默认值是0
    // slice1 = make([]int, 3)
    
    
    // 声明slice1是一个切片,同时给slice分配3个空间,初始化值是0
    // var slice1 []int = make([]int, 3)
    
    // 声明slice1是一个切片,同时给slice分配空间,3个空间,初始化值是0
    slice1 := make([]int, 3)
    
    slice1[0] = 100
    fmt.Printf("len = %d, slice = %v\n", len(slice1), slice1)       
    
    if slice1 == nil {
        fmt.Println("slice1是一个空切片")
    }
}

(3) 切片容量的追加

package main

import "fmt"

func main() {
    var numbers = make([]int, 3, 5)    
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
    
    // 向numbers切片追加一个元素1, number长度为4,容量为5
    numbers = append(numbers, 1)
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
    
    // 向numbers切片追加一个元素2, number长度为5,容量为5
    numbers = append(numbers, 2)
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
    
    // 向numbers切片追加一个元素3, number长度为6,容量为10,变为之前的两倍
    numbers = append(numbers, 3)
    fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
}

3. map

map是散列表的引用,散列表是键值对元素的无序集合,其格式为map[K]V,键的类型必须是可以用==进行比较的

// 第一种声明方式,使用前需要用make给map分配空间
var ages map[string]int
ages = make(map[string]int, 3)
ages["alice"] = 31
ages["charlie"] = 34
ages["bob"] = 15

// 第二种声明方式
ages := make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34
ages["bob"] = 15

// 第三种声明方式
ages2 := map[string]int{
    "alice": 31,
    "charlie": 34,
    // 此处的逗号必须有
    "bob" : 15,
}

// 空map
empty := map[string]int{}

// 移除map中的元素,即使键不存在也可以
delete(ages,"alice")
ages["charlile"]++

// map元素不是一个变量,不可以获取其地址,以下是错误的
_ = &ages["bob"]

// 遍历map,键,值
for name, age := ages{
    fmt.Printf("%s\t%d\n",name,age)
}

// 其中ok是一个布尔值,表示该元素是否存在
age, ok := ages["bob"]

map不可比较,只允许和nil比较,只能通过遍历每个元素进行比较,且需要先判断元素是否存在再比较

4. 结构体

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。但是S类型的结构体可以包含*S指针类型的成员

package main

import (
	"fmt"
	"time"
)

type Employee struct {
	// 结构体成员变量名称是首字母大写的则表示变量是可导出的
	ID            int
	Name, Address string
	DoB           time.Time
	Position      string
	Salary        int
	ManagerID     int
}

func main() {
	var dilbert Employee
	// 通过点操作符访问结构体成员变量
	dilbert.Salary -= 500
	var person *Employee = &dilbert
	// 点操作符也可以用在结构体指针上
	person.Position = "shanghai"
	// 注意要加括号
	(*person).Position = "Beijing"
	fmt.Println(person)
}
package main

import "fmt"

type tree struct {
	value       int
	left, right *tree
}

type Point struct {
	X, Y int
}

func main() {
	// 结构体字面值赋值
	// var _ = Point{1, 2}
	// var _ = Point{X: 1, Y: 2}
	// 用一种简单的方式创建、初始化一个结构体变量并获取其地址
	pp := &Point{1, 2}
	// 等价于
	//pp := new(Point)
	*pp = Point{1, 2}
	fmt.Println(pp)
}

如果结构体中所有成员都是可比较的,则结构体是可比较的,当结构体中各个字段的值都相等时,两个结构体相等。

结构体的赋值:

package main

import "fmt"

type Point struct {
	X, Y int
}

type Circle struct {
	// 如果只声明一个成员对应的数据类型而不指定成员的名字,则该成员被称为匿名成员
	Point
	Radius int
}

func main() {
	// 得益于匿名嵌入的特性,可以直接访问叶子属性而不需要给出完整的路径
	// 如果不是匿名变量,则需要写出完整路径
	var c Circle
	c.X = 8
	c.Y = 8
	c.Radius = 5

	// 以下结构体字面值写法是错误的
	// c = Circle{8, 8, 5}
	// c = Circle{X: 8, Y: 8, Radius: 5}

	// 以下是正确的
	c = Circle{Point{8, 8}, 5}
	c = Circle{
		Point:  Point{X: 8, Y: 8},
		Radius: 5,
	}
	fmt.Println(c)
}

出于效率的考虑,大型结构体通常使用结构体指针的方式直接传递给函数或从函数中返回

package main

import "fmt"

// 声明一种新的数据类型 myint,是int的一个别名
type myint int

// 定义一个结构体
type Book struct {
	title string
	auth string
}

func changeBook1(book Book) {
	// 传递一个book的副本
	book.auth = "666"
}

func changeBook2(book *Book) {
	// 指针传递
	book.auth = "777"
}

func main() {
	/*
	   var a myint = 10
	   fmt.Println("a = ", a)
	   fmt.Printf("type of a = %T\n", a)
	*/
	var book1 Book
	book1.title = "Golang"
	book1.auth = "zhang3"
	fmt.Printf("%v\n", book1)

	changeBook1(book1)
	fmt.Printf("%v\n", book1)

	changeBook2(&book1)
	fmt.Printf("%v\n", book1)
}

5. JSON

package main

import (
	"encoding/json"
	"fmt"
)

type Person struct {
	Name string
	Age  int
	Pet  []string
}

func main() {
	person := Person{"zuzhiang", 25, []string{"cat", "dog", "pig"}}
	fmt.Println("person:\n", person, "\n")
	out, err := json.MarshalIndent(&person, "", "\t")
	if err != nil {
		fmt.Println("error")
	} else {
		fmt.Printf("json:\n%s\n", out)
	}
}

以上代码是将结构体转为json字符串并格式化输出。注意json解析的时候只会对可导出(首字母大写)的字段进行解析。

package main

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

type Movie struct {
	Title string
	Year  int `json:"released"`
	// 单引号内是结构体的成员TAG,通常是一系列以空格分割的键值对
	// 其中color就表示转换成JSON后Color被替换为color
	// omitempty表示成员为空或零值时不生成JSON对象
	Color  bool `json:"color,omitempty"`
	Actors []string
}

var movies = []Movie{
	{Title: "Casablanca", Year: 1942, Color: false,
		Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
	{Title: "Cool Hand Luke", Year: 1967, Color: true,
		Actors: []string{"Paul Newman"}},
	{Title: "Bullitt", Year: 1968, Color: true,
		Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
}

func main() {
	// 将结构体slice转为JSON的过程称为编组(marshaling)
	// data, err := json.Marshal(movies)
	// 第二、三个参数分别是每一行输出的前缀和每一层级的缩进,可省略
	data, err := json.MarshalIndent(movies, "", "    ")
	if err != nil {
		log.Fatalf("JSON marshaling failed: %s", err)
	}
	fmt.Printf("%s\n", data)

	// 将JSON转为结构体
	data = []byte(`[{"Title":"Casablanca"},{"Title":"Cool Hand Luke"},{"Title":"Bullitt"}]`)
	var titles []struct{ Title string }
	// slice将被只含有Title信息值填充,其它JSON成员将被忽略
	if err := json.Unmarshal(data, &titles); err != nil {
		log.Fatalf("JSON unmarshaling failed: %s", err)
	}
	fmt.Println(titles)
}

在编码时,默认使用Go语言结构体的成员名字作为JSON的对象。只有可导出的结构体成员才会被编码,所以结构体成员首字母要大写。

(1) 结构体标签

package main

import (
	"fmt"
	"reflect"
)

type resume struct {
    // 不同项之间以空格分割,冒号后不能有空格
	Name string `info:"name" doc:"我的名字"`
	Sex string `info:"sex"`
}

func findTag(str interface{}) {
	t := reflect.TypeOf(str).Elem()

	for i := 0; i< t.NumField(); i++ {
		taginfo := t.Field(i).Tag.Get("info")
		tagdoc := t.Field(i).Tag.Get("doc")
		fmt.Println("info: ", taginfo, " doc: ", tagdoc)
	}
}

func main() {
	var re resume
	findTag(&re)
}

(2) 结构体标签在json中的使用

package main

import (
	"encoding/json"
	"fmt"
)

type Movie struct {
	Title string `json:"title"`
	Year int `json:"year"`
	Price int `json:"rmb"`
	Actors []string `json:"actors"`
}

func main() {
	movie := Movie{"喜剧之王", 2000, 10, []string{"周星驰", "张柏芝"}}

	// 编码的过程 结构体 -> json
	jsonStr, err := json.Marshal(movie)
	if err != nil {
		fmt.Println("json marshal error", err)
		return
	}
	fmt.Printf("jsonStr = %s\n", jsonStr)

	// 解码的过程 json -> 结构体
	myMovie := Movie{}
	err = json.Unmarshal(jsonStr, myMovie)
	if err != nil {
		fmt.Println("json unmarshl error", err)
		return
	}
}

6. 文本和HTML模板

一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的{{action}}对象

text/template和html/template等模板包提供了一个将变量值填充到一个文本或HTML格式的模板的机制

// 这个模板先打印匹配到的issue总数,然后打印每个issue的编号、创建用户、标题还有存在的时间
const templ = `{{.TotalCount}} issues:
{{range .Items}}----------------------------------------
Number: {{.Number}}
User: {{.User.Login}}
Title: {{.Title | printf "%.64s"}}
Age: {{.CreatedAt | daysAgo}} days
{{end}}`

// 创建并返回一个模板
report, err := template.New("report").
    // 将自定义函数注册到模板中
	Funcs(template.FuncMap{"daysAgo": daysAgo}).
    // 解析模板
	Parse(templ)
if err != nil {
	log.Fatal(err)
}

模板中没有双花括号的是普通字符串,按字面值打印。双花括号内容被称为action,可以打印结构体成员、调用函数、实现循环或条件判断等。

对于每个action,都有一个当前值的概念,即点操作符.,当前值.最初被初始化为调用模板时的参数

模板中{{range .Items}}{{end}}对应一个循环action,循环每次迭代的当前值对应当前的Items元素的值

在一个action中, | 操作符表示将前一个表达式的结果作为后一个函数的输入,类似于UNIX中管道的概念

Age对应的行中,daysAgo是一个自定义函数

// HTML模板
package main

import "html/template"

func main() {
	var issueList = template.Must(template.New("issuelist").Parse(`
	<h1>{{.TotalCount}} issues</h1>
	<table>
	<tr style='text-align: left'>
		<th>#</th>
		<th>State</th>
		<th>User</th>
		<th>Title</th>
	</tr>
	{{range .Items}}
	<tr>
		<td><a href='{{.HTMLURL}}'>{{.Number}}</a></td>
		<td>{{.State}}</td>
		<td><a href='{{.User.HTMLURL}}'>{{.User.Login}}</a></td>
		<td><a href='{{.HTMLURL}}'>{{.Title}}</a></td>
	</tr>
	{{end}}
	</table>
	`))
}
go build gopl.io/ch4/issueshtml
./issueshtml repo:golang/go commenter:gopherbot json encoder >issues.html

五、函数

1. 函数声明

函数声明包括函数名、形参列表、返回值列表(可省略)以及函数体,如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的

func name(parameter-list) (result-list) {
	body
}

func add(x int, y int) int {return x + y}
func sub(x, y int) (z int) { z = x - y; return}
func first(x int, _ int) int { return x }
func zero(int, int) int { return 0 }

调用函数时必须按照声明顺序为所有参数提供实参。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用被修改。

2. 多返回值

如果一个函数将所有要返回值的变量名正好和返回参数列表的变量名一致,那么该函数的return语句可以省略操作数。这称之为bare return。

package main

import "fmt"

func foo1(a string, b int) (int, string) {
	fmt.Println("a = ",a)
	fmt.Println("b = ",b)

	return 666, "zuzhiang"
}

func foo2(a string, b int) (r1 int, r2 string) {
	fmt.Println("a = ",a)
	fmt.Println("b = ",b)

	r1 = 666
	r2 = "zuzhiang"
	return r1, r2
}

func foo3(a string, b int) (r1, r2 int) {
	fmt.Println("a = ",a)
	fmt.Println("b = ",b)

	r1 = 666
	r2 = 777
    // bare return
	return
	// 也可以把以上三行改为 return 666, 777
}

func main() {
	ret1, ret2 := foo1("abc", 12)
	fmt.Println("ret1 = ", ret1, "ret2 = ", ret2)

	//ret1, ret2 := foo2("abc", 12)
	//fmt.Println("ret1 = ", ret1, "ret2 = ", ret2)
	//
	//ret1, ret2 := foo3("abc", 12)
	//fmt.Println("ret1 = ", ret1, "ret2 = ", ret2)
}

3. 错误

Go使用控制流机制(如if和return)处理异常

(1) 错误处理策略

错误处理策略有常见的五种:

// 1. 传播错误。意味着函数中某个子程序失败
resp, err := http.Get(url)
if err != nil{
	return nil, err
}

// 2. 重新尝试失败的操作,但需要设置重试次数。适用于错误的发生是偶然性的,或由不可预知的问题导致的情况
func WaitForServer(url string) error {
    const timeout = 1 * time.Minute
    deadline := time.Now().Add(timeout)
    for tries := 0; time.Now().Before(deadline); tries++ {
        _, err := http.Head(url)
        if err == nil {
            return nil // success
        }
        log.Printf("server not responding (%s);retrying…", err)
        time.Sleep(time.Second << uint(tries)) // exponential back-off
    }
    return fmt.Errorf("server %s failed to respond after %s", url, timeout)
}

// 3. 输出错误信息并结束程序。适用于错误发生后,程序无法继续运行的情况
if err := WaitForServer(url); err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n", err)
    os.Exit(1)
}

// 4. 只输出错误信息,不终止程序
if err := Ping(); err != nil {
	log.Printf("ping failed: %v; networking disabled",err)
}

// 5. 忽略掉错误
os.RemoveAll(dir) // 虽然可能会失败,但忽略即可

log中的所有函数,都默认会在错误信息之前输出时间信息

(2) 文件结尾错误(EOF)

io包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF

in := bufio.NewReader(os.Stdin)
for {
    r, _, err := in.ReadRune()
    if err == io.EOF {
    	break // finished reading
    }
    if err != nil {
    	return fmt.Errorf("read failed:%v", err)
    }
    // ...use r…
}

4. 函数值

函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。Go使用闭包(closures)技术实现函数值,Go程序员也把函数值叫做闭包。

func square(n int) int { return n * n }
f := square
fmt.Println(f(3)) // "9"

函数类型的零值是nil,表示没有函数体。函数值之间是不可比较的,也不能用函数值作为map的key,函数值只能与nil比较。

package main

import (
	"fmt"
	"strings"
)

func add1(r rune) rune { return r + 1 }

func main() {
    // 将字符串中的每个字符+1
    // strings.Map的第一个参数是一个函数,它用来规定怎么对单个字符进行处理,第二个参数是字符串
	fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"
	fmt.Println(strings.Map(add1, "VMS"))      // "WNT"
	fmt.Println(strings.Map(add1, "Admix"))    // "Benjy"
}

5. 匿名函数

匿名函数就是func关键字后没有函数名的函数

package main

import (
	"fmt"
	"strings"
)

func main() {
	fmt.Println(strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")) // "IBM.:111"
	fmt.Println(strings.Map(func(r rune) rune { return r + 1 }, "VMS"))      // "WNT"
	fmt.Println(strings.Map(func(r rune) rune { return r + 1 }, "Admix"))    // "Benjy"
}
package main

import "fmt"

// squares返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares() func() int {
	var x int
	return func() int {
		x++
		return x * x
	}
}

func main() {
	f := squares()
	fmt.Println(f()) // "1"
	fmt.Println(f()) // "4"
	fmt.Println(f()) // "9"
	fmt.Println(f()) // "16"

	fc := squares()
	fmt.Println(fc()) // "1"
	fmt.Println(fc()) // "4"
	fmt.Println(fc()) // "9"
	fmt.Println(fc()) // "16"
    
    fmt.Println(squares()()) // "1"
	fmt.Println(squares()()) // "1"
}

函数squares返回另一个类型为 func() int 的函数。对squares的一次调用会生成一个局部变量 x 并返回一个匿名函数。每次调用时匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用squares时,会生成第二个x变量,并返回一个新的匿名函数。

6. 可变参数

参数数量可变的函数称为为可变参数函数。在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号...,这表示该函数会接收任意数量的该类型参数。

package main

import "fmt"

// ...前后加不加空格都可
func sum(vals...int) int {
	total := 0
	for _, val := range vals {
		total += val
	}
	return total
}

func main() {
	fmt.Println(sum(3))          // "3"
	fmt.Println(sum(1, 2, 3, 4)) // "10"
    values := []int{1, 2, 3, 4}
	// 如果实参是切片类型,则只需在最后一个参数后加上省略符即可
	fmt.Println(sum(values...)) // "10"
}

7. deferred函数

package main

import (
	"log"
	"time"
)

func bigSlowOperation() {
	// 最后加括号是因为trace函数返回的是一个匿名函数,加括号表示执行该匿名函数
	// defer语句中的函数会在return语句更新返回值变量后再执行
	defer trace("bigSlowOperation")()
	time.Sleep(2 * time.Second)
}

func trace(msg string) func() {
	start := time.Now()
	log.Printf("enter %s", msg)
	return func() {
		log.Printf("exit %s (%s)", msg,time.Since(start))
	}
}

func main() {
	bigSlowOperation()
}
package main

import "fmt"

func main() {
    // 写入defer关键字,在程序运行完退出之前执行,类似于java中的final
    defer fmt.Println("main end1")
    // 2先执行,defer会先进栈,执行的时候出栈执行
    defer fmt.Println("main end2")
    
    fmt.Println("main::hello go 1")
    fmt.Println("main::hello go 2")
}

8. panic异常

当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。

9. recover捕获异常

六、方法

一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些方法,而一个方法则是一个和特殊类型关联的函数。

1. 方法声明

在函数声明时,在其名字之前加上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。

package main

import (
	"fmt"
	"math"
)

type Point struct{ X, Y float64 }

// 函数
func Distance(p, q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// Point类下的方法,该方法是属于Point类型的变量p的
func (p Point) Distance(q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}

func main() {
	p := Point{1, 2}
	q := Point{4, 6}
	fmt.Println(Distance(p, q)) // "5", function call
	// 方法中接收器p就是对应的调用该方法的变量本身
	fmt.Println(p.Distance(q))  // "5", method call
}

上面代码中第13行的附件变量p被称为方法的接收器,接收器名字任意。

对象.方法对象.属性的表达式被称为选择器,同一个对象的属性和方法不允许同名,因为会有歧义。

package main

import (
	"fmt"
	"math"
)

type Point struct{ X, Y float64 }

// Point类下的方法
func (p Point) Distance(q Point) float64 {
	return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// Path是线段的集合,是切片类型
type Path []Point

// 路径的长度,接收器的类型不一定是类,也可以是切片
func (path Path) Distance() float64 {
	sum := 0.0
	for i := range path {
		if i > 0 {
			sum += path[i-1].Distance(path[i])
		}
	}
	return sum
}

func main() {
	perim := Path{
		{1, 1},
		{5, 1},
		{5, 4},
		{1, 1},
	}
	fmt.Println(perim.Distance()) // "12"
}

还可以定义一个Path类型,这个Path代表一个线段的集合,并且也给这个Path定义一个叫Distance的方法。Path是一个命名的slice类型,而不是Point那样的struct类型,然而我们依然可以为它定义方法。

对于一个给定的类型,其内部的方法都必须有唯一的方法名,但是不同的类型却可以有同样的方法名。

2. 基于指针对象的方法

一般会约定如果Point这个类有一个指针作为接收器的方法,那么所有Point 的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。

为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,如:

type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type

想要调用指针类型方法 (*Point).ScaleBy ,只要提供一个Point类型的指针即可

r := &Point{1, 2}
r.ScaleBy(2)

p := Point{1, 2}
(&p).ScaleBy(2)

p.ScaleBy(2)

最后一种清空编译器会隐式地帮我们用&p去调用ScaleBy这个方法。以下三种情况都是可以的:

  • 接收器的实参和其形参相同,比如两者都是类型T或者都是类型 *T
  • 接收器实参是类型T,但接收器形参是类型 *T ,这种情况下编译器会隐式地为我们取变量的地址
  • 接收器实参是类型 *T ,形参是类型T。编译器会隐式地为我们解引用,取到指针指向的实际变量

总之不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。

(1) nil也是一个合法的接收器类型

// An IntList is a linked list of integers.
// A nil *IntList represents the empty list.
type IntList struct {
    Value int
    Tail  *IntList
}

// Sum returns the sum of the list elements.
func (list *IntList) Sum() int {
    if list == nil {
		return 0 
    }
    return list.Value + list.Tail.Sum()
}

3. 方法值和方法表达式

package main

import (
	"fmt"
)

type Point struct{ X, Y float64 }

func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} }
func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} }

type Path []Point

func (path Path) TranslateBy(offset Point, add bool) Path{
	// 变量op代表Point类型的addition或者 subtraction方法
	var op func(p, q Point) Point
	if add {
        // 让op变量等于方法值
		op = Point.Add
	} else {
		op = Point.Sub
	}
	for i := range path {
		// Call either path[i].Add(offset) or path[i].Sub(offset).
		path[i] = op(path[i], offset)
	}
	return path
}

func main() {
	offset := Point{1, 1}

	path := Path{
		{1, 1},
		{5, 1},
		{5, 4},
		{1, 1},
	}
	fmt.Println(path.TranslateBy(offset, true))
}

4. bit数组

Go语言里的集合一般会用map[T]bool这种形式来表示,T代表元素类型。

package main

import (
	"bytes"
	"fmt"
)

// IntSet是非负整数的集合,值为0表示空集
type IntSet struct {
	// 被称为字的切片,每个字有64位,可以用来表示64个元素的有无
	words []uint64
}

// 将元素x加入到集合
func (s *IntSet) Add(x int) {
	// word为元素在切片内的下标,bit为元素在字内的位下标
	word, bit := x/64, uint(x%64)
	// 若集合不够大,则增大集合
	for word >= len(s.words) {
		s.words = append(s.words, 0)
	}
	// 将切片指定下标的字的制定位设为1
	s.words[word] |= 1 << bit
}

// 集合是否包含元素x
func (s *IntSet) Has(x int) bool {
	word, bit := x/64, uint(x%64)
	// 判断切片指定下标的字的制定位是否为1
	return word < len(s.words) && s.words[word]&(1<<bit) != 0
}

// 求s和t的交集
func (s *IntSet) UnionWith(t *IntSet) {
	for i, tword := range t.words {
		if i < len(s.words) {
			// 按位求或
			s.words[i] |= tword
		} else {
			// 直接追加
			s.words = append(s.words, tword)
		}
	}
}

// 以可视化的方式打印集合的内容
func (s *IntSet) String() string {
	var buf bytes.Buffer
	buf.WriteByte('{')
	for i, word := range s.words {
		// 若当前字表示的子集无元素,则跳过
		if word == 0 {
			continue
		}
		// 遍历字的每一位
		for j := 0; j < 64; j++ {
			if word&(1<<uint(j)) != 0 {
				if buf.Len() > len("{") {
					buf.WriteByte(' ')
				}
				// 元素的值为 64*i+j
				fmt.Fprintf(&buf, "%d", 64*i+j)
			}
		}
	}
	buf.WriteByte('}')
	return buf.String()
}

func main() {
	s := IntSet{}
	s.Add(1)
	s.Add(5)
	s.Add(12)
	s.Add(51)
	s.Add(111)
	fmt.Println(s.String())
}

5. 封装

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。封装有时候也被叫做信息隐藏。Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。这种基于名字的手段使得在语言中最小的封装单元是package。

只用来访问或修改内部变量的函数被称为setter或者getter。在命名一个getter方法时,我们通常会省略掉前面的Get前缀。

七、接口

1. 接口类型

接口类型是对其它类型行为的抽象和概括。接口类型是对其它类型行为的抽象和概括

package main

import "fmt"

// interface{}是万能通用类型
func myFunc(arg interface{}) {
	fmt.Println("myFunc is called...")
	fmt.Println(arg)

	// interface{}如何区分此时引用的底层数据类型是什么?
	// 给interface{}提供“类型断言”机制
	value, ok := arg.(string)
	if !ok {
		fmt.Println("arg is not string type")
	}  else {
		fmt.Println("arg is string type, value = ", value)
	}
}

type Book struct {
	name string
}

func main() {
	book := Book{"Golang"}
	myFunc(book)
	myFunc(100)
	myFunc("abc")
	myFunc(3.14)
}

2. 实现接口的条件

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。

3. 类型断言

类型断言是一个使用在接口值上的操作。x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

这里有两种可能。第一种,如果断言的类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同。如果这个检查成功了,类型断言的结果是x的动态值,当然它的类型是T。换 句话说,具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败,接下来这个操作会抛出panic。

第二种,如果相反断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。 如果这个检查成功了,动态值没有获取到;这个结果仍然是一个有相同类型和值部分的接口 值,但是结果有类型T。

八、goroutine和通道

1. goroutine

在Go语言中,每一个并发的执行单元叫作一个goroutine。当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们叫它main goroutine。新的goroutine会用go语句来创建。在语法上,go语句是一个普通的函数或方法调用前加上关键字go。

可以把goroutine称为协程或纤程,协程是一个概念,纤程是Windows系统对协程的一种具体实现。一个线程包含多个协程。

package main

import (
	"fmt"
	"time"
)

func main() {
	go spinner(100 * time.Millisecond)
	const n = 45
	fibN := fib(n) // slow
	fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

func spinner(delay time.Duration) {
	for {
		for _, r := range `-\|/` {
			fmt.Printf("\r%c", r)
			time.Sleep(delay)
		} }
}

func fib(x int) int {
	if x < 2 {
		return x 
	}
	return fib(x-1) + fib(x-2)
}

以上代码使用一个goroutine打印字符动画。主goroutine递归计算斐波那契数列,这个过程很慢,等主goroutine结束(斐波那契数列计算完毕)后字符动画也停止。主函数返回时,所有的goroutine都会被直接打断,程序退出。

(1) Goroutine

package main

import (
	"time"
    "fmt"
)

// 子goroutine
func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new Goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second)
    }
}

// 主goroutine
func main() {
    // 创建一个go程去执行newTask()流程
    go newTask()
    
    // 若下面只有这一句,则将直接退出,因为主goroutine退出后子goroutine也会退出
    // fmt.Println("main gorouine exit")
    
    i := 0    
    for {
        i++
        fmt.Printf("main foroutine: i = %d\n", i)
        time.Sleep(1 * time.Second)
    }
}
package main

import (
	"fmt"
	"time"
)

func main() {
    // 匿名方法
    go func() {
        defer fmt.Println("A.defer")
        
        // 在{}后加()以调用该方法
        func() {
            defer fmt.Println("B.defer")
            // 退出当前goroutine
            // runtime.Goexit()
            fmt.Println("B")
        }()
        
        fmt.Println("A")
    }()
    
    // 带参数和返回值的匿名方法
    go func(a int, b int) bool {
        fmt.Println("a = ", a, ", b = ", b)
        return true
    }(10, 20)
    
    // 死循环
    for {
        time.Sleep(1 * time.Second)
    }
}

2. channel

一个 channel是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值信息。

可用通过make函数来创建一个指定类型的channel,如:

ch := make(chan int)

一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个 goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使 用 <- 运算符。在发送语句中, <- 运算符分割channel和要发送的值。在接收语句中, <- 运 算符写在channel对象之前。一个不使用接收结果的接收操作也是合法的。

// 发送数据
ch <- x
// 接收数据
x = <-ch
// 接收数据但不保存到变量
<-ch
// 关闭channel
close(ch)

(1) 无缓冲channel

用于两个goroutine之间的通信

package main

import (
	"fmt"
)

func main() {
    // 定义一个channel
    c := make(chan int)
    
    go func() {
        defer fmt.Println("goroutine结束")
        fmt.Println("goroutine 正在运行...")
        c <- 666 // 将666发送给c        
    }()
    
    // 从c中接收数据,并赋值给num
    num := <- c 
    fmt.Println("num = ", num)
    fmt.Println("main goroutine结束")
}

channel连接的两个goroutine会进行同步,当接收数据的一方走到接收数据的语句时会阻塞,等待对方发送数据。同理,发送数据的一方走到发送数据的语句时也会阻塞,等待接收方接收数据。

基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。因为这个原因,无缓存Channels有时候也被称为同步Channels。

(2) 有缓冲channel

package main

import (
	"fmt"
	"time"
)

func main() {
	// 有缓冲的channel
	c := make(chan int, 3)
	fmt.Println("len(c) = ", len(c), ", cap(c) = ", cap(c))

	go func() {
		defer fmt.Println("子go程结束")
		// 把3改成4试试
		for i := 0; i < 3; i++ {
			c <- i
			fmt.Println("子go程正在运行,发送的元素为 ", i, " len(c)=", len(c), ", cap(c)=", cap(c))
		}
	}()

	time.Sleep(2 * time.Second)

	for i := 0; i < 3; i++ {
		num := <-c
		fmt.Println("num = ", num)
	}

	fmt.Println("main 结束")
}

当channel已经满,再向里面写数据,则会阻塞。当channel为空,从里面取数据也会阻塞。

(3) channel关闭

当需要往通道写入一组数据,但不知道数据的个数时,由于通道的写入和读取都会阻塞协程,所以当全部数据写入完后,再从通道读取数据会导致阻塞。因此可以在数据写入完后将通道关闭,并在读取数据的协程中判断通道的状态,如果关闭则直接跳出循环,反之继续等待读取。

关闭channel后对基于该channel的任何发送操作都将导致panic异常。对一个已经被close过的channel进行接收操作依然可以接受到之前已经成功发送的数据;如果channel中已经没有数据的话将产生一个零值的数据。

package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        // 关闭一个channel
        close(c)
    }()
    
    for {
        // ok如果为true则channel没有关闭,反之已关闭
        if data, ok := <- c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }
    fmt.Println("Main finished...")
}
  • channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel
  • 关闭channel后,无法向channel再发送数据(引发panic错误后导致接收立即返回零值)
  • 关闭channel后,可以继续从channel接收数据
  • 对于nil channel,无论收发都会被阻塞

(4) channel与range

package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        // 关闭一个channel
        close(c)
    }()
    
    /*
    for {
        // ok如果为true则channel没有关闭,反之已关闭
        if data, ok := <- c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }
    */
    
    // 可以使用range来迭代不断操作channel
    for data := range c {
        fmt.Println(data)
    }
    
    fmt.Println("Main finished...")
}

(5) channel与select

单流程下一个go只能监控一个channel的状态,select可以监控多个channel的状态。select语句类似于switch语句,其case后跟的必须是channel。select户随机选择一个可以运行的case来执行,当没有case可以运行时,会尝试执行default,如果没有default则会被阻塞,直到有case可以运行。

package main

import (
   "fmt"
   "time"
)

func main() {
   ch1 := make(chan int)
   ch2 := make(chan int)

   go func() {
      time.Sleep(2 * time.Second)
      ch1 <- 100
   }()

   go func() {
      time.Sleep(2 * time.Second)
      ch2 <- 100
   }()

   select {
   case num := <-ch1:
      fmt.Println("ch1: ", num)
   case num, ok := <-ch2:
      if ok {
         fmt.Println("ch2: ", num)
      } else {
         fmt.Println("ch2已经关闭")
      }
   //default:
   // fmt.Println("default执行")
   }
}

(6) 串联的channel(pipeline)

Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的 输入。这种串联的Channels就是所谓的管道(pipeline)。

package main

import (
	"fmt"
)

func main() {
	naturals := make(chan int)
	squares := make(chan int)
	// 把数发送到第一个通道
	go func() {
		for x := 0; x < 10; x++ {
			naturals <- x
		}
		close(naturals)
	}()

	// 计算数的平方,并发送到第二个通道
	go func() {
		for x := range naturals {
			squares <- x * x
		}
		close(squares)
	}()

	// 在主goroutine获取第二个通道中的值
	for x := range squares {
		fmt.Println(x)
	}
}

(7) 单方向的channel

Go语言的类型系统提供了单方向的channel类型,分别用于 只发送或只接收的channel。类型 chan<- int 表示一个只发送int的channel,只能发送不能接收。相反,类型 <-chan int 表示一个只接收int的channel,只能接收不能发送。

一般不会直接创建单向通道,因为这没有任何意义,而是在主协程创建一个双向通道,而在子协程创建单向通道以在子协程内部限制通道只能读或者只能写。

package main

import (
	"fmt"
)

func counter(out chan<- int) {
	for x := 0; x < 10; x++ {
		out <- x
	}
	close(out)
}

func squarer(out chan<- int, in <-chan int) {
	for v := range in {
		out <- v * v
	}
	close(out)
}

func printer(in <-chan int) {
	for v := range in {
		fmt.Println(v)
	}
}

func main() {
	naturals := make(chan int)
	squares := make(chan int)
	go counter(naturals)
	go squarer(squares, naturals)
	printer(squares)
}

chan int 类型的naturals可以隐式地转换为 chan<- int<-chan int 类型,反向转换则不可以。

3. 并发的退出

Go语言并没有提供在一个goroutine中终止另一个goroutine的方法,因为这样会导致goroutine 之间的共享变量落在未定义的状态上。

九、使用共享变量实现并发

1. sync.Mutex互斥锁

import "sync"

var (
    mu      sync.Mutex // guards balance
    balance int
)

func Deposit(amount int) {
    mu.Lock()
    balance = balance + amount
    mu.Unlock()
}

func Balance() int {
    mu.Lock()
    b := balance
    mu.Unlock()
    return b
}

使用defer在函数返回之后或错误发生返回时进行解锁

func Balance() int {
    mu.Lock()
    defer mu.Unlock()
    return balance
}

2. sync.RWMutex读写锁

var mu sync.RWMutex
var balance int

func Balance() int {
    mu.RLock() // readers lock
    defer mu.RUnlock()
    return balance
}

单写多读,读与读之间不冲突,读与写、写与写之间冲突。多个 goroutine 可同时获取读锁(调用 RLock() 方法;而写锁(调用 Lock() 方法)会阻止任何其他 goroutine(无论读和写)进来,整个锁相当于由该 goroutine 独占。

3. sync.Once单次执行

如果初始化成本比较大的话,那么将初始化延迟到需要的时候再去做就是一个比较好的选择。

var loadOnce sync.Once
// 该方法接收初始化函数作为其参数,用来解决一次性初始化的问题
loadOnce.Do(初始化函数)

十、包和go工具

如果想同时导入两个有着名字相同的包,例如math/rand包和crypto/rand包,那么导入声 明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。

import (
    "crypto/rand"
    mrand "math/rand" // alternative name mrand avoids conflict
)

1. 匿名和别名导包方式

// main包
package main

import (
    // 路径要加上完整的GoPath
    // 匿名导包方式即加上“_ ”,无法使用当前包的方法,但会执行当钱包内部的init()方法
    _ "GoPath/lib1"
    
    // 别名导包方式
    mylib2 "GoPath/lib2"
    
    // 将包的全部方法导入到当前包的作用域中,使用时不用再写包名
    . "GoPath/lib2"
)

func main() {
    // 只导入不使用时会报错,此时可以用匿名导包方式
    // lib1.Lib1Test()
    
    // 别名导包方式
    // lib2.Lib2Test()
    mylib2.Lib2Test()
    
    // 将包的全部方法导入当前包的作用域中
    Lib2Test()
}

十一、测试

十二、反射

1. 反射

package main

import (
    "fmt"
    "reflect"
)

func reflectNum(arg interface{}) {
    fmt.Println("type: ", reflect.TypeOf(arg))
    fmt.Println("value: ", reflect.ValueOf(arg))
}

func main() {
    var num float64 = 1.2345
    reflectNum(num)
}
package main

import (
	"fmt"
	"reflect"
)

type User struct {
	Id int
	Name string
	Age int
}

func (this *User) Call() {
	fmt.Println("User is called")
	fmt.Printf("%v\n", this)
}

func main() {
	user := User{1, "zuzhiang", 25}
	DoFiledAndMethod(user)
}

func DoFiledAndMethod(input interface{}) {
	// 获取input的type
	inputType := reflect.TypeOf(input)
	fmt.Println("inputType is: ", inputType.Name())

	// 获取input的value
	inputValue := reflect.ValueOf(input)
	fmt.Println("inputValue is: ", inputValue)

	// 通过type获取里面的字段
	// 1. 获取interface的reflect.Type,通过Type得到NumField,进行遍历
	// 2. 得到每个field,数据类型
	// 3. 通过field的Interface()方法得到对应的value
	for i := 0; i < inputType.NumField(); i++ {
		field := inputType.Field(i)
		value := inputValue.Field(i).Interface()

		fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
	}

	// 通过type获取里面的方法,调用
	for i := 0; i < inputType.NumMethod(); i++ {
		m := inputType.Method(i)
		fmt.Printf("%s: %v\n", m.Name, m.Type)
	}
}

可以通过reflect.Elem()获取指针指向的元素类型,这个获取过程被称为取元素,等效于对指针类型变量做了一个*操作

十三、低级编程

十四、面向对象

1. 类

package main

import "fmt"

// 如果类名首字母大写,表示其他包也能访问
type Hero struct {
	// 如果类的属性首字母大写,表示该属性对外能够访问,否则只能在类的内部访问
	Name string
	Ad int
	Level int
}

/*
func (this Hero) Show() {
    fmt.Println("Name = ", this.Name)
    fmt.Println("Ad = ", this.Ad)
    fmt.Println("Level = ", this.Level)
}

func (this Hero) GetName() {
    fmt.Println("Name = ", this.Name)
}

func (this Hero) SetName(newName string) {
    // this是调用的该方法的对象的一个副本
    this.Name = newName
}
*/

func (this *Hero) Show() {
	fmt.Println("Name = ", this.Name)
	fmt.Println("Ad = ", this.Ad)
	fmt.Println("Level = ", this.Level)
}

func (this *Hero) GetName() {
	fmt.Println("Name = ", this.Name)
}

func (this *Hero) SetName(newName string) {
	this.Name = newName
}

func main() {
	// 创建一个对象
	hero := Hero{Name: "zhang3", Ad: 100, Level: 1}
	hero.Show()
	hero.SetName("li4")
	hero.Show()
}

2. 继承

package main

type Human struct {
    name string
    sex string
}

func (this *Human) Eat() {
    fmt.Println("Human.Eat()...")
}

func (this *Human) Walk() {
    fmt.Println("Human.Walk()...")
}

// ============================

type SuperMan struct {
    // 该类继承自Huamn类
    Human
    level int    
}

func (this *SuperMan) Eat() {
    fmt.Println("SuperMan.Eat()...")
}

func (this *SuperMan) Fly() {
    fmt.Println("SuperMan.Fly()...")
}

func main() {
    h := Human{"zhang3", "female"}   
    h.Eat()
    h.Walk()
    
    // 定义一个子类的对象
    // s := SuperMan{Human{"li4", "female"}, 88}
    var s SuperMan
    s.name = "li4"
    s.sex = "female"
    s.level = 88
    
    s.Walk() // 父类的方法
    s.Eat() // 子类的方法
    s.Fly() // 子类的方法
}

3. 多态

package main

import "fmt"

// 本质是一个指针
type AnimalIF interface {
	Sleep()
	// 获取动物的颜色
	GetColor() string
	// 获取动物的种类
	GetType() string
}

// 具体的类
type Cat struct {
	// 猫的颜色
	color string
}

func (this *Cat) Sleep() {
	fmt.Println("Cat is Sleep")
}

func (this *Cat) GetColor() string {
	return this.color
}

func (this *Cat) GetType() string {
	return "Cat"
}

// 具体的类
type Dog struct {
	// 狗的颜色
	color string
}

func (this *Dog) Sleep() {
	fmt.Println("Dog is Sleep")
}

func (this *Dog) GetColor() string {
	return this.color
}

func (this *Dog) GetType() string {
	return "Dog"
}

func showAnimal(animal AnimalIF) {
	fmt.Println("type = ", animal.GetType())
}

func main() {
	// 接口的数据类型,父类指针
	var animal AnimalIF
	animal = &Cat{"Green"}
	// 调用的是Cat的Sleep()方法
	animal.Sleep()

	animal = &Dog{"Yellow"}
	// 调用的是Dog的Sleep()方法
	animal.Sleep()

	cat := Cat{"Green"}
	dog := Dog{"Yellow"}
	showAnimal(&cat)
	showAnimal(&dog)
}

十五、版本管理

1. GOPATH

弊端:

  • 无版本控制
  • 无法同步一致第三方版本号
  • 无法指定当前项目引用的第三方版本号

2. Go Modules

请添加图片描述
请添加图片描述

十六、代码实例

例:从stdin或指定文件读取并找出重复行

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)
			// err为nil时表示成功打开
			if err != nil {
				fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
				continue
			}
			countLines(f, counts)
			f.Close()
		}
	}
	for i, n := range counts {
		if n > 1 {
			fmt.Printf("%d\t%s\n", n, i)
		}
	}
}

// 函数和其他包级别的实体可以任意次序声明
func countLines(f *os.File, counts map[string]int) {
	input:=bufio.NewScanner(f)
	for input.Scan() {
		// 更改子函数中的counts值会影响到main函数中counts的值
		counts[input.Text()]++
	}
}

输出从url获取的内容

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
)

func main() {
	for _, url := range os.Args[1:] {
        // 产生一个http请求
		resp, err := http.Get(url)
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
			os.Exit(1)
		}
        // 读取响应的响应体
		b, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
			os.Exit(1)
		}
		fmt.Printf("%s\n", b)
	}
}

并发获取多个url

package main

import (
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"time"
)

func main() {
	start := time.Now()
	// 创建一个字符串通道,通道是允许某一例程向另一例程传递指定类型的值的通信机制
	ch := make(chan string)
	for _, url := range os.Args[1:] {
		// main函数在一个goroutine中执行,go语句创建额外的goroutine,即一个并发执行的函数
		go fetch(url, ch)
	}
	for range os.Args[1:] {
		// <-ch是接收发送的值
		fmt.Println(<-ch)
	}
	fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
}

func fetch(url string, ch chan<- string) {
	start := time.Now()
	resp, err := http.Get(url)
	if err != nil {
		// 在通道ch上发送一个值
		ch <- fmt.Sprint(err)
		return
	}

	// io.Copy获取响应内容,并通过ioutil.Discard输出流进行丢弃
	nbytes, err := io.Copy(ioutil.Discard, resp.Body)
	resp.Body.Close()
	if err != nil {
		ch <- fmt.Sprintf("while reading %s: %v\n", url, err)
		return
	}
	secs := time.Since(start).Seconds()
	ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}

迷你web服务器

package main

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

func main()  {
	// 回声请求调用处理程序
	http.HandleFunc("/",handler)
	log.Fatal(http.ListenAndServe("localhost:8000",nil))
}

func handler(w http.ResponseWriter,r *http.Request)  {
	// 回显请求URL r的路径部分
	fmt.Fprintf(w,"URL.Path = %q\n",r.URL.Path)
}

迷你回声和计数服务器

package mainimport (	"fmt"	"log"	"net/http"	"sync")var mu sync.Mutexvar count intfunc main() {	http.HandleFunc("/", handler)	http.HandleFunc("/count", counter)	log.Fatal(http.ListenAndServe("localhost:8000", nil))}func handler(w http.ResponseWriter, r *http.Request) {	// 加锁	mu.Lock()	count++	mu.Unlock()	fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)}func counter(w http.ResponseWriter, r *http.Request) {	mu.Lock()	fmt.Fprintf(w, "Count %d\n", count)	mu.Unlock()}

使用&操作符获取一个变量的地址,使用*操作符获取指针引用的变量的值。

1. bit数组

Go语言里的集合一般会用map[T]bool这种形式来表示,T代表元素类型。

package main

import (
	"bytes"
	"fmt"
)

// IntSet是非负整数的集合,值为0表示空集
type IntSet struct {
	// 被称为字的切片,每个字有64位,可以用来表示64个元素的有无
	words []uint64
}

// 将元素x加入到集合
func (s *IntSet) Add(x int) {
	// word为元素在切片内的下标,bit为元素在字内的位下标
	word, bit := x/64, uint(x%64)
	// 若集合不够大,则增大集合
	for word >= len(s.words) {
		s.words = append(s.words, 0)
	}
	// 将切片指定下标的字的制定位设为1
	s.words[word] |= 1 << bit
}

// 集合是否包含元素x
func (s *IntSet) Has(x int) bool {
	word, bit := x/64, uint(x%64)
	// 判断切片指定下标的字的制定位是否为1
	return word < len(s.words) && s.words[word]&(1<<bit) != 0
}

// 求s和t的交集
func (s *IntSet) UnionWith(t *IntSet) {
	for i, tword := range t.words {
		if i < len(s.words) {
			// 按位求或
			s.words[i] |= tword
		} else {
			// 直接追加
			s.words = append(s.words, tword)
		}
	}
}

// 以可视化的方式打印集合的内容
func (s *IntSet) String() string {
	var buf bytes.Buffer
	buf.WriteByte('{')
	for i, word := range s.words {
		// 若当前字表示的子集无元素,则跳过
		if word == 0 {
			continue
		}
		// 遍历字的每一位
		for j := 0; j < 64; j++ {
			if word&(1<<uint(j)) != 0 {
				if buf.Len() > len("{") {
					buf.WriteByte(' ')
				}
				// 元素的值为 64*i+j
				fmt.Fprintf(&buf, "%d", 64*i+j)
			}
		}
	}
	buf.WriteByte('}')
	return buf.String()
}

func main() {
	s := IntSet{}
	s.Add(1)
	s.Add(5)
	s.Add(12)
	s.Add(51)
	s.Add(111)
	fmt.Println(s.String())
}

2. 排序

package main

import (
	"fmt"
	"sort"
)

type StringSlice []string

// 序列长度
func (p StringSlice) Len() int           { return len(p) }
// 元素比较方式
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
// 元素交换方式
func (p StringSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

func main() {
	str := StringSlice{"098","145","057","049","111","849"}
	// 内置排序算法需要指定:序列长度、元素比较方式、元素交换方式
	sort.Sort(str)
	fmt.Println(str)
}

3. http.Handler接口

package main

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

func main() {
	db := database{"shoes": 50, "socks": 5}
	log.Fatal(http.ListenAndServe("localhost:8000", db))
}

type dollars float32

func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }

type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	for item, price := range db {
		fmt.Fprintf(w, "%s: %s\n", item, price)
	}
}

运行程序后在浏览器访问localhost:8000,显示

shoes: $50.00
socks: $5.00

十七、其他

在做单元测试时,测试代码的文件名必须以_test.go结尾,并且测试函数的名称必须以Test开头。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值