Go语言学习笔记

发布于:2025-04-01 ⋅ 阅读:(20) ⋅ 点赞:(0)

随着计算机硬件的发展,编程语言无法充分利用CPU多核特性,并且缺乏一个简洁高效的语言,因此2007 年,谷歌工程师 Rob Pike, Ken Thompson 和 Robert Griesemer 设计了Go语言 ,它有以下特点:

  1. 静态类型+编译型语言,运行速度快
  2. 脚本化编程语言,易于编写
  3. 从 C 语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型,调用参数传值,指针等,也保留了和 C 语言一样的编译执行方式及弱化的指针
  4. 引入包的概念,用于组织程序结构,Go 语言的一个文件都要归属于一个包,而不能单独存在
  5. 垃圾回收机制,内存自动回收,不需开发人员管理
  6. 从语言层面支持并发
  7. 吸收了管道通信机制,形成 Go 语言特有的管道 channel 通过管道 channel , 可以实现不同的 goroute之间的相互通信。
  8. 函数可以返回多个值
  9. 新的创新:比如切片 slice、延时执行 defer

Quick Start

如下所示为一个hello.go文件

// 每个文件都必须归属于一个包
package main

// 引入一个包 fmt
import "fmt"

// 主程序入口
func main(){
	// 调用 fmt 包的函数 Println 输出
	fmt.Println("hello world!")
}

通过命令go build hello.go可以对源代码进行编译,生成可执行文件然后允许;也可以直接go run hello.go,会自动编译运行代码

Go 语言严格区分大小写;
编译器是按行进行编译的,一行只能由一条语句,且结尾不需要用分号;
go 语言定义的变量或者 import 的包如果没有使用到,代码不能编译通过;
go函数体的{需要放在行尾,下面写法会报错

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

1 基本数据类型

变量

变量声明

有三种方式:

	//第一种:指定变量类型,Go中声明后若不赋值,编译器会赋默认值例如int 的默认值是0 
	var i int 
	fmt.Println("i=", i)

	//第二种:根据值自行判定变量类型(类型推导)
	var num  = 10.11
	fmt.Println("num=", num)

	//第三种:省略var
	name := "tom"
	fmt.Println("name=", name)

	//一次性声明多个变量的方式2 
	var n1, name , n3 = 100, "tom", 888
	fmt.Println("n1=",n1, "name=", name, "n3=", n3)

	//一次性声明多个变量的方式3, 同样可以使用类型推导
	n1, name , n3 := 100, "tom~", 888
	fmt.Println("n1=",n1, "name=", name, "n3=", n3)

// 一次性声明全局变量
var (
	n3 = 300
	n4 = 900
	name2 = "mary"
)

变量类型

和其它语言一样,可以理解为基本和复杂数据类型

基本数据类型:

  • 整数类型(Integer Types):Go语言提供不同大小的整数类型,如int、int8(1字节)、int16、int32和int64。无符号整数类型有uint、uint8、uint16、uint32和uint64。整数类型的大小取决于计算机的架构,例如32位或64位。
  • 浮点数类型(Floating-Point Types):Go语言提供float32和float64两种浮点数类型,分别对应单精度和双精度浮点数。
  • 复数类型(Complex Types):Go语言提供complex64和complex128两种复数类型,分别对应由两个浮点数构成的复数。
  • 布尔类型(Boolean Type):布尔类型用于表示真(true)和假(false)的值,用于条件判断和逻辑运算。
  • 字符类型(Rune Type):用byte来保存单个字母字符
  • 字符串类型(String Type):字符串类型表示一系列字符。字符串是不可变的,可以使用双引号"或反引号`来定义。

复杂数据类型:

  • 数组类型(Array Types):数组是具有固定大小的同类型元素集合。声明数组时需要指定元素类型和大小。
  • 切片类型(Slice Types):切片是对数组的一层封装,是动态长度的可变序列。切片不保存元素,只是引用底层数组的一部分。
  • 映射类型(Map Types):映射是键值对的无序集合,用于存储和检索数据。键和值可以是任意类型,但键必须是可比较的。
  • 结构体类型(Struct Types):结构体是一种用户定义的复合数据类型,可以包含不同类型的字段,每个字段有一个名字和类型。
  • 接口类型(Interface Types):接口是一种抽象类型,用于定义一组方法。类型实现了接口的方法集合即为实现了该接口。
  • 函数类型(Function Types):函数类型表示函数的签名,包括参数和返回值类型。函数可以作为参数传递和返回。
  • 通道类型(Channel Types):通道是用于在协程之间进行通信和同步的一种机制。通道有发送和接收操作。
  • 指针类型(Pointer Types):指针类型表示变量的内存地址。通过指针可以直接访问和修改变量的值。
var n1 = 100 
// 查看某个变量的数据类型
fmt.Printf("n1 的 类型 %T \n", n1)

// 查看某个变量的占用字节大小和数据类型
var n2 int64 = 10
fmt.Printf("n2 的 类型 %T  n2占用的字节数是 %d ", n2, unsafe.Sizeof(n2))

//Golang 的浮点型默认声明为float64 类型
var num5 = 1.1
fmt.Printf("num5的数据类型是 %T \n", num5)

字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。对于传统的字符串是由字符组成的,而 Go 的字符串不同,它是由字节组成的.
Go字符采用UTF-8编码,如果字符在 ASCII 表的,比如[0-1, a-z,A-Z…]直接可以保存到 byte;如果字符对应码值大于 255,可以考虑使用 int 类型保存

var c1 byte = 'a'
//当我们直接输出byte值,就是输出了的对应的字符的码值
fmt.Println("c1=", c1)
//如果我们希望输出对应字符,需要使用格式化输出
fmt.Printf("c1=%c\n", c2)

GO中的String字符串一旦定义不支持修改,

// 可以使用双引号、反引号包裹字符串
str3 := ` 
	package main
	import (
		"fmt"
		"unsafe"
	)
	`

// 可以使用 + 拼接字符串,注意换行时 + 在行尾
str4 := "hello " + "world" + "hello " + "world" + "hello " + 
	"world" + "hello " + "world" + "hello " + "world" + 
	"hello " + "world"

Go 在不同类型的变量之间赋值时需要显式转换,转换是把i值赋值给n,i本身数据类型并不会变

	var i int32 = 100
	var n float32 = float32(i)
	fmt.Printf("i type is %T\n", i) // int32

	var n1 int32 = 12
	var n2 int64
	var n3 int8
	// go不会进行自动类型转换,下面n2是int64,n1是int32,会编译报错
	n2 = n1 + 20  

可以使用fmt.Sprintf函数或者strconv包来实现基本类型转字符串

import (
	"fmt"
	_ "unsafe"
	"strconv"
)
func main() {
	var num1 int = 99
	var num2 float64 = 23.456
	var b bool = true
	var myChar byte = 'h'
	var str string //空的str

	// fmt.Sprintf方法
	str = fmt.Sprintf("%d", num1)
	str = fmt.Sprintf("%f", num2)
	str = fmt.Sprintf("%t", b)
	str = fmt.Sprintf("%c", myChar)

	//第二种方式 strconv 函数 
	var num3 int = 99
	var num4 float64 = 23.456
	var b2 bool = true
	
	str = strconv.FormatInt(int64(num3), 10)
	str = strconv.Itoa(num3)
	// 'f' 格式 10:表示小数位保留10位 64 :表示这个小数是float64
	str = strconv.FormatFloat(num4, 'f', 10, 64)
	str = strconv.FormatBool(b2)
}

使用strconv包实现str向其他数据类型的转换

	var str string = "true"
	var b bool
	// 函数会返回两个值 (value bool, err error)略
	b , _ = strconv.ParseBool(str)
		
	var str2 string = "1234590"
	var n1 int64
	var n2 int
	n1, _ = strconv.ParseInt(str2, 10, 64)
	n2 = int(n1)
	
	var str3 string = "123.456"
	var f1 float64
	f1, _ = strconv.ParseFloat(str3, 64)

	// 如果转换失败,会返回该类型变量的默认值
	var str4 string = "hello"
	var n3 int64 = 11
	n3, _ = strconv.ParseInt(str4, 10, 64)
	fmt.Printf("n3 type %T n3=%v\n", n3, n3)

Go中指针类型的用法和C语言一样:

  1. 基本数据类型,变量存的就是值,也叫值类型
  2. 指针类型:指针变量存的是一个地址,这个地址指向的空间存的才是值,var ptr *int = &num
  3. &获取变量的地址,比如: var num int, 获取 num 的地址:&num
  4. *获取指针类型所指向的值,var ptr int, 使用ptr 获取 ptr 指向的值
	var i int = 10
	fmt.Println("i的地址=", &i)
	
	var ptr *int = &i 
	fmt.Printf("ptr 本身=%v\n", ptr)
	fmt.Printf("ptr 的地址=%v", &ptr) 
	fmt.Printf("ptr 指向的值=%v", *ptr)

值类型:变量直接存储值,内存通常在栈中分配,包括基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,指针、slice 切片、map、管道 chan、interface 等都是引用类型

Go语言中通常使用fmt包中的scan函数获取标准输入

var name string
	var age byte
	var sal float32
	var isPass bool
	fmt.Println("请输入姓名 ")
	// 1 使用Scanln获取一行输入
	fmt.Scanln(&name)
	
	// 2:fmt.Scanf,可以按指定的格式输入
	fmt.Println("请输入你的姓名,年龄,薪水, 是否通过考试, 使用空格隔开")
	fmt.Scanf("%s %d %f %t", &name, &age, &sal, &isPass)
	fmt.Printf("名字是 %v \n年龄是 %v \n 薪水是 %v \n 是否通过考试 %v \n", name, age, sal, isPass)

数组与切片

数组是多个相同类型数据的组合,一旦声明/定义了,其长度是固定的,不能动态变化。如下所示为数组的定义和使用

// 几种初始化数组的方式
var numArr01 [3]int = [3]int{1, 2, 3}	// var 数组名 [数组大小]数据类型
var numArr02 = [3]int{5, 6, 7}	// 省略变量类型
var numArr03 = [...]int{8, 9, 10}	// 自动推导元素个数
var score = [...]int{1: 800, 0: 900, 2:999}	// 定义各个元素
strArr05 := [...]string{1: "tom", 0: "jack", 2:"mary"}	// 类型推导

// 遍历各个元素
for i := 0; i < len(score); i++ {
	fmt.Printf("请输入第%d个元素的值\n", i+1)
	fmt.Scanln(&score[i])
}
score[0] = 200
//变量数组打印
for i := 0; i < len(score); i++ {
	fmt.Printf("score[%d]=%v\n", i, score[i])
}

for i, v := range score {	// 也可以使用for range的方式遍历数组
	fmt.Printf("i=%v v=%v\n", i , v)
}

// 定义/声明二维数组
var arr [4][6]int
// 赋初值
arr[1][2] = 1
arr[2][3] = 3
arr3  := [2][3]int{{1,2,3}, {4,5,6}}

数组在内存中的分布是一整块地址空间,地址可以通过数组名来获取 &score[i],第一个元素的地址,就是数组的首地址,各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8个字节

数组创建后会为每个元素赋默认值

Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝,数组间不会相互影响,如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)

func test02(arr *[3]int) {
	fmt.Printf("arr指针的地址=%p", &arr)
	(*arr)[0] = 88 //!!
} 

长度是数组类型的一部分,不同长度的数组可以认为是不同类型,因此在参数传递时需要注意。

切片Slice是数组的一个引用,因此切片是引用类型。切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度都一样。不同的是切片的长度是可以变化的,因此切片是一个可以动态变化数组

切片有两种定义方式

  1. 第一种是通过截取数组的一部分成为切片,由于切片是引用,其地址指向截取数组的开头;这时切片和数组其实指向同一块内存地址,对切片进行修改后,数组内容也会生效。
  2. 通过`var 切片名 []type = make([]type, len, [cap])`手动创建一个新的切片,type 就是数据类型, len 切片长度, cap 可选,指定切片容量。也可以省略make直接定义一个slice,其实现原理相同
    

可以把切片看作一个结构体,包含指向首地址的指针、长度len和cap容量。容量是分配的地址空间指总共可容纳元素个数,比如一个slice虽然只有三个元素,但其空间容量为5;在slice长度动态增加超过容量时,会按照一定的规则扩容。slice扩容时会分配一片新的地址空间,然后把原有的值拷贝过去。

var arr [5]int = [...]int{10, 20, 30, 40, 50}
slice := arr[1:4]	// 如果省略为arr[:4]代表从头开始,arr[1:]代表直到结尾

//使用常规的for循环遍历切片
for i := 0; i < len(slice); i++ {
	fmt.Printf("slice[%v]=%v ", i, slice[i])
}

//使用for--range 方式遍历切片
for i, v := range slice {
	fmt.Printf("i=%v v=%v \n", i, v)
}

slice2 := slice[1:2] //  slice [ 20, 30, 40]    [30]
slice2[0] = 100  // 因为arr , slice 和slice2 指向的数据空间是同一个,因此slice2[0]=100,其它的都变化

//用append内置函数,可以对切片进行动态追加
slice3 = append(slice, 400, 500, 600)

//通过append将切片slice3追加给slice3
slice3 = append(slice3, slice3...) // 100, 200, 300,400, 500, 600 100, 200, 300,400, 500, 600

//切片使用copy内置函数完成拷贝,举例说明
var slice4 []int = []int{1, 2, 3, 4, 5}
var slice5 = make([]int, 10)
copy(slice5, slice4)

由于slice时引用,因此参数传递时是进行引用传递

func test(slice []int) {
	slice[0] = 100  //这里修改slice[0],会改变实参
}
	
func main() {
	var slice = []int {1, 2, 3, 4}	// 省略make直接定义,有长度声明为数组,没有为slice
	fmt.Println("slice=", slice) // [1,2,3,4]
	test(slice)
	fmt.Println("slice=", slice) // [100, 2, 3, 4]
}

string 底层是一个 byte 数组,因此 string 也可以进行切片处理。由于string是不可变的,因此不能通过 str[0] = ‘z’ 方式来修改字符串,可以先将其转为slice,然后进行修改

	str := "hello@你好"
	slice := str[6:] 	// 使用切片获取到子串

	arr1 := []byte(str) 
	arr1[0] = 'z'
	str = string(arr1)
	
	// 如果需要处理中文,转为rune按字符处理切片
	arr1 := []rune(str) 
	arr1[0] = '北'
	str = string(arr1)

Map

Go中集合map的 key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组。需要注意的是声明不会分配内存,初始化需要 make 分配内存后才能赋值和使用

var a map[string]string
a = make(map[string]string, 10)	// 给map分配数据空间
a["no1"] = "value1"

cities := make(map[string]string)	// 直接申请空间

heroes := map[string]string{	// 定义并初始化
	"hero1" : "宋江",
}
heroes["hero2"] = "林冲"

map的增删改查操作

	cities := make(map[string]string)
	cities["no1"] = "北京"	// 新增
	cities["no1"] = "上海~" 	// 修改
	fmt.Println(cities)

	delete(cities, "no1")		// 删除元素
	cities = make(map[string]string)		// 如果希望一次性删除所有,make 一个新的,让原来的成为垃圾,被 gc 回收
	fmt.Println(cities)
	
	val, ok := cities["no2"]	// 查找
	if ok {
		fmt.Printf("有no1 key 值为%v\n", val)
	} else {
		fmt.Printf("没有no1 key\n")
	}

map可以进行嵌套定义

	studentMap := make(map[string]map[string]string)
	
	studentMap["stu01"] =  make(map[string]string, 3)		// map的元素还是一个map
	studentMap["stu01"]["name"] = "tom"
	studentMap["stu01"]["sex"] = "男"

	studentMap["stu02"] =  make(map[string]string, 3) 
	studentMap["stu02"]["name"] = "mary"
	studentMap["stu02"]["sex"] = "女"

	for k1, v1 := range studentMap {		// 使用for range进行遍历
		fmt.Println("k1=", k1)
		for k2, v2 := range v1 {
				fmt.Printf("\t k2=%v v2=%v\n", k2, v2)
		}
		fmt.Println()
	}

切片的数据类型如果是 map,即map 切片,这样使用则 map 个数就可以动态变化了

	var monsters []map[string]string		//1. 声明一个map切片
	monsters = make([]map[string]string, 2)
	//2. 增加第一个妖怪的信息
	if monsters[0] == nil {
		monsters[0] = make(map[string]string, 2)
		monsters[0]["name"] = "老张"
		monsters[0]["age"] = "50"
	}

	newMonster := map[string]string{
		"name" : "小李",
		"age" : "20",
	}
	monsters = append(monsters, newMonster)		// append函数动态的增加

注意

  • map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map
  • map 的容量达到后,再想 map 增加元素,会自动扩容

2 流程控制

标识符

Golang 对各种变量、方法、函数等命名时使用的字符序列称为标识符。其命名规则如下:

  • 由 26 个英文字母大小写,0-9 ,_ 组成
  • 数字不可以开头
  • Golang 中严格区分大小写。
  • 标识符不能包含空格
  • 下划线"_"本身在 Go 中是一个特殊的标识符,称为空标识符。可以代表任何其它的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用
  • 不能以系统保留关键字作为标识符
  • 变量名、函数名、常量名采用驼峰法命名
  • 如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果小写,则只能在本包中使用。Go中没有可见性关键字,首字母大写是公开的,首字母小写是私有的

运算符

算术运算符:包含:+, - , * , / , %, ++, – ,需要注意的是Go中的自增、自减运算符没有返回值,因此不能将结果进行赋值或者进行运算

var i int = 1;
var j = i++	 // 错误,不能赋值

if i++ > 10 {	// 错误,不能参与比较
	fmt.Println("ok")
}

关系运算符:==、!=、>、<、>=、<=,用于比较数值,结果为bool值

逻辑运算符:用于连接逻辑表达式进行逻辑运算,与&&、或||、非!

位运算符:对二进制数据进行的运算(如加、减、乘、除)被称为位运算,即对二进制数的每一位进行操作的运算

运算 符号 规则
& 两个位都为1时,结果才为1
两个位都为0时,结果才为0
异或 ^ 两个位相同为0,相异为1
取反 ~ 0变1,1变0
左移 << 各二进位全部左移若干位,高位丢弃,低位补0
右移 >> 各二进位全部右移若干位,高位补0或符号位补齐

赋值运算符就是将某个运算后的值,赋给指定的变量,包括=、+=、-=、*=、/=、%=,还有位运算相关的左移赋值<<=、右移赋值>>=、按位与赋值&=、 按位异或赋值^=、按位或赋值|=

分支语句

顺序分支,从上向下执行
单、双、多分支,通过if进行分支判断,需要注意的是:

  • golang支持在if中,直接定义一个变量
  • 单分支不能省略大括号
  • else不要换行
if age := 20; age > 18 {
	fmt.Println("已成年!")
} else {
	fmt.Println("未成年!")
}

//多分支判断
var score int
fmt.Println("请输入成绩:")
fmt.Scanln(&score)

if score == 100 {
	fmt.Println("奖励一辆BMW")
} else if score > 80 && score <= 99 {
	fmt.Println("奖励一台iphone7plus")
} else {
	fmt.Println("什么都不奖励")
}

Switch的使用,注意

  • go中不需要break
  • switch\case后面可以跟常量、变量、表达式
  • case后面可以同时匹配多个条件
  • 通过关键字fallthrough实现穿透,执行下一个条件
	var key byte 
	fmt.Println("请输入一个字符 a,b,c,d,e,f,g")
	fmt.Scanf("%c", &key)

	switch test(key)+1 { 
		case 'a':
			fmt.Println("周一, 穿新衣")
			fallthrough	// 执行到这里会向下穿透一层,执行下面的一个case
		case 'b':
			fmt.Println("周二,当小二")
		case 'c','d':
			fmt.Println("周三,爬雪山")
		//...
		default:
			fmt.Println("输入有误...")
	}

循环

golang中通过for来实现循环,有如下几种灵活的形式

	for i := 1; i <= 10; i++ {
		fmt.Println("你好,尚硅谷", i)
	}
	//for循环的第二种写法
	j := 1 //循环变量初始化
	for j <= 10 { //循环条件
		
		fmt.Println("你好,尚硅谷~", j)
		j++ //循环变量迭代
	}

	//for循环的第三种写法, 通常会配合break跳出循环
	k := 1
	for {  // 这里也等价 for ; ; { 
		if k <= 10 {
			fmt.Println("ok~~", k)
		} else {
			break
		}
		k++
	}

	// 通过for range遍历字符串和数组
	str = "abc~ok"
	for index, val := range str {	// 遍历获取各个位置的字符
		fmt.Printf("index=%d, val=%c \n", index, val)
	}

Go中的break默认跳出最近的一层循环;break和continue可以使用一个label指定跳出多层循环

lable2: 
for i := 0; i < 4; i++ {
for j := 0; j < 10; j++ {
	if j == 2 {
		break lable2 // 跳转到最外层
	}
	fmt.Println("j=", j) 
}

here:
for i:=0; i<2; i++ {
	for j:=1; j<4; j++ {
	if j==2 {
		continue here
	}
	fmt.Println("i=",i,"j=",j)
}

Golang中也支持使用goto跳转到代码的指定位置,但是这样会造成程序混乱可读性差,一般不支持使用。

3 函数

3.1 基本定义和使用

Go中函数的基本格式为

func 函数名 (形参列表)(返回值列表){
	执行语句
	return 返回值
}

如下所示定义一个四则运算的函数,注意函数首字母需要大写才能被其他包使用

package utils 
import (
	"fmt"
)

func Cal(n1 float64, n2 float64, operator byte) float64 {
	var res float64
	switch operator {
		case '+':
			res = n1 + n2
		case '-':
			res = n1 - n2
		case '*':
			res = n1 * n2
		case '/':
			res = n1 / n2
		default:
			fmt.Println("操作符号错误...")
	}
	return res
}

Go中也是用包对项目文件进行管理,通过package定义包名,每个文件都必须属于一个包下面;通过包还可以对函数和变量的作用域进行控制。如下所示通过引入上面包中定义的函数并进行调用,路径从 环境变量定义的$GOPATH 的 src 下开始;引入的时候可以为包取别名

package main
import (
	"fmt"
	util "go_code/chapter06/fundemo01/utils"
)

func main() {
	var n1 float64 = 1.2
	var n2 float64 = 2.3
	var operator byte = '+'
	result := util.Cal(n1, n2 , operator) 
	fmt.Println("result~=", result)
}

如果要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main 。如果是写一个库 ,包名可以自定义,在编译main文件时相关的库函数会被编译为以.a结尾的库文件

Go语言中的基本数据类型一般分配在内存中的栈区,引用数据类型分配在堆区。在调用一个函数时,会给该函数分配一个新的堆栈,不同的堆栈之间彼此不可见,因此函数变量彼此隔离。当一个函数调用完毕(执行完毕)后,程序会销毁这个函数对应的栈空间。因此在函数调用传递形参时需要注意传递的值仅在当前函数中可见。

基本数据类型和数组默认都是值传递的,即进行值拷贝,在函数内修改,不会影响到原来的值。如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用

Go 函数不支持函数重载

在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量。函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用

func getSum(n1 int, n2 int) int {
	return n1 + n2
}

func myFun(funvar func(int, int) int, num1 int, num2 int ) int {	// 入参是一个函数类型
	return funvar(num1, num2)
}

func main() {
	res2 := myFun(getSum, 50, 60)	// 将函数getSum作为参数传递
	fmt.Println("res2=", res2)
}

为了简化数据类型定义,Go 支持自定义数据类型

// 给int取了别名后, go认为myInt和int是两个类型
	type myInt int 

go支持对返回参数进行命名

func getSumAndSub(n1 int, n2 int) (sum int, sub int){
	sub = n1 - n2
	sum = n1 + n2
	// 返回值的顺序按照函数定义,与这里写的前后无关
	return sub,sum
}

Go 支持可变参数,可变参数需要放在形参列表最后

func sum(n1 int, args... int) int {
	sum := n1 
	// 遍历可变参数列表args 
	for i := 0; i < len(args); i++ {
		sum += args[i]  //args[0] 表示取出args切片的第一个元素值,其它依次类推
	}
	return sum
}

3.2 函数机制

init函数

每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用;注意,import包中的函数优先于本文件执行,即执行顺序为:外部包、全局变量、init、main

var age = test()
// 全局变量是先被初始化的,我们这里先写函数
func test() int {
	fmt.Println("test()") //1
	return 90
}

// init函数,通常可以在init函数中完成初始化工作
func init() {
	fmt.Println("init()...") //2
}

func main() {
	fmt.Println("main()...age=", age) //3
}
匿名函数

Go 支持匿名函数,匿名函数就是没有名字的函数,如下所示有三种使用场景

package main
import (
	"fmt"
)

var (
	// 定义全局匿名函数
	Fun1 = func (n1 int, n2 int) int {
		return n1 * n2
	}
)

func main() {
	// 定义匿名函数时就直接调用,只调用一次
	res1 := func (n1 int, n2 int) int {
		return n1 + n2
	}(10, 20)
	fmt.Println("res1=", res1)

	// 将匿名函数赋给变量,通过a完成调用
	a := func (n1 int, n2 int) int {
		return n1 - n2
	}
	res2 := a(10, 30)
	fmt.Println("res2=", res2)

	// 全局匿名函数的使用
	res4 := Fun1(4, 9)
	fmt.Println("res4=", res4)
}
闭包

闭包就是一个函数和与其相关的引用环境组合的一个整体

func AddUpper(n int) func (int) int {
	return func (x int) int {
		n = n + x
		return n
	}
}

func main() {
	f := AddUpper(10)
	fmt.Println(f(1))// 10 + 1 = 11 
	fmt.Println(f(2))// 11 + 2 = 13
}

如上面代码所示,AddUpper返回一个匿名函数,其中用到了变量n,这样匿名函数和它用到的变量n就形成了一个闭包。这里的变量n是闭包的一个变量,只会初始化一次n=10,第一次调用结果n为11,第二次调用的时候n在11的基础上进行累加。

可以看到,使用闭包可以灵活定制并返回一个基数n为10的函数f

defer

Go中的延时执行机制,当执行到defer时,暂时不执行,会将defer的语句压入到独立的栈中;当函数执行完毕后,再按照先入后出的方式出栈执行。通常用于在函数执行完毕后,进行释放资源。在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈

func sum(n1 int, n2 int) int {
	defer fmt.Println("ok1 n1=", n1) //defer 3. ok1 n1 = 10
	defer fmt.Println("ok2 n2=", n2) //defer 2. ok2 n2= 20

	n1++ // n1 = 11
	n2++ // n2 = 21
	res := n1 + n2 // res = 32
	fmt.Println("ok3 res=", res) // 1. ok3 res= 32
	return res

}

func main() {
	res := sum(10, 20)
	fmt.Println("res=", res)  // 4. res= 32
}
内置函数
  1. len:用来求长度,比如 string、array、slice、map、channel
  2. new:用来分配内存,主要用来分配值类型,比如 int、float32,struct…返回的是指针
  3. make:用来分配内存,主要用来分配引用类型,比如 channel、map、slice。

3.3 字符串

Go中常见的字符串处理相关函数

	str2 := "hello北京"
	// 字符串的长度,golang的编码统一为utf-8 ,字母和数字占一个字节,汉字占用3个字节,用rune(str)处理
	r := []rune(str2)
	for i := 0; i < len(r); i++ {
		fmt.Printf("字符=%c\n", r[i])
	}

	//字符串转整数
	n, err := strconv.Atoi("123")
	if err != nil {
		fmt.Println("转换错误", err)
	}else {
		fmt.Println("转成的结果是", n)
	}

	//4)整数转字符串  
	str = strconv.Itoa(12345)

	//5)字符串 转 []byte
	var bytes = []byte("hello go")
	str = string([]byte{97, 98, 99}) 
	fmt.Printf("str=%v\n", str)

	//10进制转 2, 8, 16进制,返回对应的字符串
	str = strconv.FormatInt(123, 16)
	fmt.Printf("123对应的16进制是=%v\n", str)

	//查找子串是否在指定的字符串中
	b := strings.Contains("seafood", "mary")

	//统计一个字符串有几个指定的子串 4
	num := strings.Count("ceheese", "e")

	// 不区分大小写的字符串比较(==是区分字母大小写的)
	b = strings.EqualFold("abc", "Abc")
	fmt.Println("结果","abc" == "Abc") // false 区分字母大小写

	// 返回子串在字符串第一次出现的index值,如果没有返回-1 : 
	index := strings.Index("NLT_abcabcabc", "abc") // 4

	// 返回子串在字符串最后一次出现的index
	index = strings.LastIndex("go golang", "go") //3

	//将指定的子串替换成 另外一个子串,n可以指定你希望替换几个,如果n=-1表示全部替换
	str2 = "go go hello"
	str = strings.Replace(str2, "go", "北京", -1)

	// 按照指定的某个字符分割字符串 
	strArr := strings.Split("hello,wrold,ok", ",")
	for i := 0; i < len(strArr); i++ {
		fmt.Printf("str[%v]=%v\n", i, strArr[i])
	} 

	//15)将字符串的字母进行大小写的转换: 
	str = "goLang Hello"
	str = strings.ToLower(str) 
	str = strings.ToUpper(str) 

	//将字符串左右两边的空格去掉
	str = strings.TrimSpace(" tn a lone gopher ntrn   ")

	// 将字符串左右两边指定的字符去掉 
	str = strings.Trim("! he!llo! ", " !")

	// 判断字符串是否以指定的字符串开头:
	b = strings.HasPrefix("ftp://192.168.10.1", "hsp") //true

3.4 时间相关

如下所示为Go中时间有关函数的使用

//1. 获取当前时间
	now := time.Now()
	fmt.Printf("now=%v now type=%T\n", now, now)

	//获取到年月日,时分秒
	fmt.Printf("年=%v\n", now.Year())
	fmt.Printf("月=%v\n", now.Month())
	fmt.Printf("月=%v\n", int(now.Month()))
	fmt.Printf("日=%v\n", now.Day())
	fmt.Printf("时=%v\n", now.Hour())
	fmt.Printf("分=%v\n", now.Minute())
	fmt.Printf("秒=%v\n", now.Second())

	//格式化日期时间
	dateStr := fmt.Sprintf("当前年月日 %d-%d-%d %d:%d:%d \n", now.Year(), 
	now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
	fmt.Printf("dateStr=%v\n", dateStr)

	//格式化日期时间,Go设计者规定了用2006-01-02 15:04:05这个常量来指代时间格式
	fmt.Printf(now.Format("2006-01-02 15:04:05"))
	fmt.Println()
	fmt.Printf(now.Format("2006-01-02"))
	fmt.Println()

  // 休眠100毫秒
	time.Sleep(time.Millisecond * 100)

	// 获取秒和纳秒的时间戳
	fmt.Printf("unix时间戳=%v unixnano时间戳=%v\n", now.Unix(), now.UnixNano())

3.5 异常处理

Go 语言不支持传统的 try…catch…finally 这种处理,而是通过抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常

func test() {
	defer func() {
		err := recover()  // recover()内置函数,可以捕获到异常
		if err != nil {
			fmt.Println("err=", err)
		}
	}()	// 定义并调用匿名函数
	num1 := 10
	num2 := 0
	res := num1 / num2	// 除0异常
	fmt.Println("res=", res)
}

也可以使用errors.New抛出自定义异常;使用panic 内置函数可以接收 error 类型的变量,输出错误信息,并退出程序

func readConf(name string) (errInfo error) {
	if name == "config.ini" {
		return nil
	} else {
		return errors.New("读取文件错误..") // 抛出一个自定义错误
	}
}

func test02() {
	errInfo := readConf("config2.ini")
	if errInfo != nil {
		panic(errInfo)	// 输出这个错误,并终止程序
	}
}

3.6 单元测试

Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试

import (
	"fmt"
	"testing" //引入go 的testing框架包
)

func TestAddUpper(t *testing.T) {	// 测试addUpper是否正确
	res := addUpper(10)	//调用
	if res != 55 {
		t.Fatalf("AddUpper(10) 执行错误,期望值=%v 实际值=%v\n", 55, res)
	}
	t.Logf("AddUpper(10) 执行正确...")
}
  1. 测试用例文件名必须以 xxx_test.go 结尾。 比如 cal_test.go
  2. 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper
  3. TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T
  4. 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub
  5. 运行测试用例指令go test 只有运行错误时会输出日志, go test -v运行正确或是错误,都输出日志
  6. 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序
  7. t.Logf 方法可以输出相应的日志
  8. 测试单个文件,一定要带上被测试的原文件go test -v cal_test.go cal.go
  9. 测试单个方法go test -v -test.run TestAddUpper

4 面向对象

结构体

Golang 没有类(class),而是通过结构体(struct)来实现 OOP 特性的,它去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函
数、隐藏的 this 指针等。

它仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承:Golang 没有 extends 关键字,继承是通过匿名字段来实现。

在创建一个结构体变量后,如果没有给字段赋值,都对应一个默认值,指针、slice、和 map 都是 nil ,即还没有分配空间,使用前记得手动make空间

如下所示为结构体四种声明和使用方式

type Person struct{
	Name string
	Age int
}

func main() {
	var person Person	// 1 直接声明
	
	p2 := Person{"mary", 20}	// 2 声明并初始化
	p2.Name = "tom"

	var p3 *Person= new(Person)	// 3 使用指针
	//p3是一个指针,标准赋值方式(*p3).Name = "smith",Go底层进行了处理,可以简写成 p3.Name = "smith"
	(*p3).Name = "smith" 
	p3.Name = "john"

	var person *Person = &Person{}	//方式4-{}
	(*person).Name = "scott"
	person.Name = "scott~~"
}

	// 返回结构体变量
	var stu1 = Stu{"小明", 19} // stu1---> 结构体数据空间

	var stu3 = Stu{	// 把字段名和字段值写在一起, 不依赖字段的定义顺序.
			Name :"jack",
			Age : 20,
	}

	// 返回结构体的指针类型
	var stu5 *Stu = &Stu{"小王", 29}  // stu5--> 地址 ---》 结构体数据[xxxx,xxx]
	var stu7 = &Stu{
		Name : "小李",
		Age :49,
	}
	fmt.Println(stu1, stu3, *stu5, *stu7) //

结构体是值类型,变量直接是值拷贝,修改不会传递;但是如果使用指针指向结构体地址,则修改的是同一个内容

	var p1 Person
	p1.Age = 10
	p1.Name = "小明"
	var p2 *Person = &p1 // 定义指针指向p1代表的结构体

	p2.Name = "tom~"	// 会同步修改p1的结构体
	fmt.Printf("p2.Name=%v p1.Name=%v \n", (*p2).Name, p1.Name) // 注意.运算顺序比价高,(*p2)要括起来
  • 不同类型结构体之间进行强制类型转换时需要有完全相同的字段。
  • 使用type关键字可以为变量赋别名,Go认为其是一种新的类型,但是可以通过类型转换
  • struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常用于序列化和反序列化。
type Monster struct{
	Name string `json:"name"` // `json:"name"` 就是 struct的 tag
	Age int `json:"age"`		// 为了使外部包可见,变量名首字母大写
}
func main() {
	//1. 创建一个Monster变量
	monster := Monster{"小牛", 500}

	//2. 将monster变量序列化为 json格式字串
	jsonStr, err := json.Marshal(monster)
	if err != nil {
		fmt.Println("json 处理错误 ", err)
	}
	fmt.Println("jsonStr", string(jsonStr))	// 序列化输出为name,而不是Name
}

方法

Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型都可以有方法,不仅 struct

// 定义结构体和变量
type Person struct{
	Name string
}

/*
func (recevier type) methodName(参数列表) (返回值列表){
	方法体
	return 返回值
}
*/
func (p Person) speak() { // 给Persion类型添加一个方法
	fmt.Println(p.Name, "是一个goodman~")	// 形参p是值传递
}

func main() {
	var p Person
	p.Name = "tom"
	p.test() // test 方法只能通过 Person 类型的变量来调用
	fmt.Println("main() p.Name=", p.Name) //输出 tom
}

变量调用方法时,该变量本身也会作为一个参数传递到方法,如果变量是值类型则进行值拷贝,如果是引用类型则进行地址拷贝。因此需要修改变量本身需要传递指针类型。

如下需要注意函数和方法在使用方式上的不同:

//对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
func test01(p Person) {
	fmt.Println(p.Name)
}
func test02(p *Person) {
	fmt.Println(p.Name)
}

func main() {
	p := Person{"tom"}
	test01(p)	// 只能传递值类型
	test02(&p)	// 只能传递指针
}


//对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法,效果是值拷贝;反过来同样也可以。主要是看方法的定义是什么类型
func (p Person) test03() {	// 接收值类型
	p.Name = "jack"
	fmt.Println("test03() =", p.Name)
}

func main() {
	p := Person{"tom"}
	p.test03()		// 可以直接用值调用
	(&p).test03() // 也可以用指针调用,但是本质仍然是值拷贝
}

封装

Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题,并通过get/set方法对属性进行设置,这就是Go中对象封装的特性

type student struct{
	Name string
	score float64
}

//因为student结构体首字母是小写,因此是只能在model使用,通过定义外部可见的方法来提供初始化变量
func NewStudent(n string, s float64) *student {
	return &student{
		Name : n,
		score : s,
	}
}

func (s *student) GetScore() float64{	// 由于结构体score是小写,这里提供一个get方法供其他包获取查询
	return s.score 
}

继承

在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性

type Student struct {	// 父类
	Name string
	Age int
	Score int
}

func (stu *Student) ShowInfo() {	// 父类的公共方法
	fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n", stu.Name, stu.Age, stu.Score)
}
func (stu *Student) setScore(score int) {
	stu.Score = score
}

type Pupil struct { 
	Student //	在子类中嵌入了Student匿名结构体
}

func (p *Pupil) testing() {	// 定义子类的特有方法
	fmt.Println("小学生正在考试中.....")
}

func main() {
	pupil := &Pupil{}
	pupil.Student.Name = "tom~"	// 访问父类的属性
	pupil.Age = 8							// 可以省略父类路径,编译器会自动进行属性查找
	pupil.testing() 						// 调用子类方法
	pupil.Student.setScore(70)	// 调用父类方法,不论大小写都可以
	pupil.Student.ShowInfo()
}

通过子类对象访问父类字段、属性时,父类名可以省略,当子类和父类有相同的字段或者方法时,编译器采用就近原则先访问子类的;如希望访问父类的字段和方法,可以加上父类匿名结构体名来区分。
结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错

如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,在访问组合的结构体的字段或方法时,必须带上结构体的名字

type D struct {
	a A //有名结构体
}

var d D 
d.a.Name = "jack"

嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值

type Goods struct {
	Name string
	Price float64
}
type Brand struct {
	Name string
	Address string
}
type TV struct {
	Goods			// 多重继承
	Brand	
}

func main() {
	tv2 := TV{ 
		Goods{
			Price : 5000.99,
			Name : "电视机002", 
		},  
		Brand{
			Name : "夏普", 
			Address :"北京",
		}, 
	}
}

接口

interface 类型可以定义一组方法,但是不需要实现,并且不包含任何变量。当使用的时候,再根据具体情况进行实现
多态是面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的,即按照统一的接口来调用不同的实现。

type Usb interface {	// 定义接口
	Start() 		// 声明了两个没有实现的方法
	Stop()
}

type Phone struct {							// 定义结构体
}  
func (p Phone) Start() {				// 实现Usb接口的方法,Go中并不会显示指出实现的接口名,但是在调用的时候会校验是否实现了接口的所有方法
	fmt.Println("手机开始工作。。。")
}
func (p Phone) Stop() {
	fmt.Println("手机停止工作。。。")
}

type Camera struct {
}
func (c Camera) Start() {					// 实现Usb接口的方法
	fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop() {
	fmt.Println("相机停止工作。。。")
}

type Computer struct {		// 通过计算机调用Usb接口
}
func (c Computer) Working(usb Usb) {		// Working方法可以接收所有Usb接口类型变量
	usb.Start()		// 统一通过usb接口变量来调用Start和Stop方法
	usb.Stop()
}

func main() {
	computer := Computer{}
	phone := Phone{}
	camera := Camera{}

	computer.Working(phone)		// 统一对实现了接口的变量进行调用
	computer.Working(camera)
}
  • 一个接口可以继承多个别的接口,这时如果要实现该接口,必须将父接口的方法也全部实现,注意多个接口之间不能有相同的方法名
  • Interface 类型默认是一个指针(引用类型),如果没有初始化就使用,会输出 nil
  • 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口, 即我们可以把任何一个变量赋给空接口
type Stu struct {
}
type T  interface{
}
func main() {
	var stu Stu
	var t T  = stu	// 将变量stu赋值给空接口类型的t
	fmt.Println(t)
}

和继承不同,实现接口可以在不改变结构体的基础上,对其功能进行扩展。
继承的价值主要在于:解决代码的复用性和可维护性。
接口的价值主要在于:设计好各种规范(方法),让其它自定义类型去实现这些方法

type Monkey struct {
	Name string
}
func (this *Monkey) climbing() {
	fmt.Println(this.Name, " 生来会爬树..")
}

type LittleMonkey struct {
	Monkey //继承
}

//声明接口
type BirdAble interface {
	Flying()
}

// 在不改变LittleMonkey的基础上,为其增加了新的方法
func (this *LittleMonkey) Flying() {
	fmt.Println(this.Name, " 通过学习,会飞翔...")
}

func main() {
	monkey := LittleMonkey{
		Monkey {Name : "悟空",},
	}
	monkey.climbing()
	monkey.Flying()
}

通过类型断言,可以将一个接口转换成具体的类型

	var x interface{}
	var b2 float32 = 2.1
	x = b2  //空接口,可以接收任意类型

	if y, ok := x.(float32); ok {	// 将接口类型x转换为float32
		fmt.Printf("y 的类型是 %T 值是=%v", y, y)
	} else {
		fmt.Println("convert fail")
	}

如下所示,在遍历多态数组时,使用类型断言可以对不同的类型进行判断,进而执行不同的代码逻辑

func (computer Computer) Working(usb Usb) {
	usb.Start()
	if phone, ok := usb.(Phone); ok {	// 使用类型断言,如果是Phone,额外执行Call方法
		phone.Call()
	}
	usb.Stop()
}

func main() {
	var usbArr [3]Usb
	usbArr[0] = Phone{"vivo"}		// 多态数组,存放实现了接口的不同类型对象
	usbArr[1] = Phone{"小米"}
	usbArr[2] = Camera{"尼康"}

	var computer Computer
	for _, v := range usbArr{		// 统一进行遍历
		computer.Working(v)
		fmt.Println()
	}
}

反射

反射可以在运行时动态获取变量的各种信息, 比如变量的类型(type),类别(kind),结构体的字段、方法;通过反射,可以修改变量的值,并且调用关联的方法

普通的数据类型通过interface可以和reflect.Type之间进行转换

func main() {
	var num int = 100
	reflectTest01(num)
}

func reflectTest01(b interface{}) {
	//通过反射获取的传入的变量的 type , kind, 值
	rTyp := reflect.TypeOf(b)	// 获取 reflect.Type
	fmt.Println("rType=", rTyp)
	
	rVal := reflect.ValueOf(b)	// 获取到 reflect.Value,注意这里rVal并不是真正的变量,而是反射的变量类型
	n2 := 2 + rVal.Int()		// 通过反射变量定义的方法获取到具体值
	fmt.Println("n2=", n2)

	// 反过来,先将 rVal 转成 interface{},再通过断言转成需要的类型
	iV := rVal.Interface()
	num2 := iV.(int)
	fmt.Println("num2=", num2)
}

// 结构体的反射
func reflectTest02(b interface{}) {	
	rTyp := reflect.TypeOf(b)		// 获取到 reflect.Type
	rVal := reflect.ValueOf(b)
	
	kind1 := rVal.Kind()		// 获取 变量对应的Kind
	kind2 := rTyp.Kind()

	// 通过反射获取结构体的值
	iV := rVal.Interface()
	stu, ok := iV.(Student)
	if ok {
		fmt.Printf("stu.Name=%v\n", stu.Name)
	}
}

5 文件

Go在os.File 封装所有文件相关操作,File 是一个结构体

如下所示为读取文件两个方式

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

func main() {
	file , err := os.Open("d:/test.txt")		// 打开文件
	if err != nil {
		fmt.Println("open file err=", err)
	}
	defer file.Close() // 结束时及时关闭file句柄,否则会有内存泄漏.

	reader := bufio.NewReader(file)	// 使用带缓冲区的读取
	for {
		str, err := reader.ReadString('\n') // 每读到一个换行就停止
		if err == io.EOF { // io.EOF表示文件的末尾
			break
		}
		fmt.Printf(str)
	}

	content, err := ioutil.ReadFile(file)	// 使用ioutil.ReadFile一次性将文件读取,不需要手动进行打开关闭
	if err != nil {
		fmt.Printf("read file err=%v", err)
	}
	fmt.Printf("%v", string(content)) // []byte,把读取到的内容转为字符串
}

如下所示为文件写入的方式

	filePath := "d:/abc.txt"
	file, err := os.OpenFile(filePath, os.O_WRONLY | os.O_CREATE, 0666)	// 以读写的方式进行打开,不存在则创建
	if err != nil {
		fmt.Printf("open file err=%v\n", err)
		return 
	}
	
	defer file.Close()
	
	str := "hello,Gardon\r\n"

	writer := bufio.NewWriter(file)	// 使用带缓存的 *Writer进行写入
	for i := 0; i < 5; i++ {
		writer.WriteString(str)
	}
	writer.Flush()	// 将缓存中的内容刷入文件

go可以flag 包用来解析命令行参数

//定义几个变量,用于接收命令行的参数值
	var user string
	var pwd string

	//"u" ,就是 -u 指定参数
	//"" , 默认值
	//"用户名,默认为空" 说明
	flag.StringVar(&user, "u", "", "用户名,默认为空")
	flag.StringVar(&pwd, "pwd", "", "密码,默认为空")
	flag.Parse()		// 转换, 必须调用该方法

	//输出结果
	fmt.Printf("user=%v pwd=%v host=%v port=%v", user, pwd, host, port)

使用json包对结构体进行序列化和反序列化

func testStruct() {
	monster := Monster{
		Name :"牛魔王",
		Age : 500 ,
	}
	
	data, err := json.Marshal(&monster) //将monster 序列化
	if err != nil {
		fmt.Printf("序列号错误 err=%v\n", err)
	}
	fmt.Printf("monster序列化后=%v\n", string(data))
}

func unmarshalStruct() {
	str := "{\"Name\":\"小王\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"Sal\":8000,\"Skill\":\"eat\"}"
	var monster Monster
	err := json.Unmarshal([]byte(str), &monster)		// 反序列化
	if err != nil {
		fmt.Printf("unmarshal err=%v\n", err)
	}
	fmt.Printf("反序列化后 monster=%v monster.Name=%v \n", monster, monster.Name)
}

6 协程与管道

Goroutine

并发:多线程程序在单核上运行
并行:多线程程序在多核上运行
在 Go 主线程上,可以再起多个协程(Goroutine),它有如下特点:

  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程,与线程实现并发相比资源消耗小

如下所示通过go开启了一个协程,它会和主线程同步一起执行,直到执行完成或者主线程退出

func test() {
	for i := 1; i <= 10; i++ {
		fmt.Println("test () hello,world " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

func main() {
	go test() // 开启了一个协程

	for i := 1; i <= 10; i++ {
		fmt.Println(" main() hello,golang" + strconv.Itoa(i))		// test和main会一起执行,交替输出
		time.Sleep(time.Second)
	}
}

cpuNum := runtime.NumCPU()
fmt.Println("cpuNum=", cpuNum)	// check num of cpu
runtime.GOMAXPROCS(cpuNum - 1)	// 设置使用多个cpu

MPG模型:如图所示,有两个物理线程 M,每个 M 绑定一个处理器 P,并运行一个 Goroutine。
在这里插入图片描述

  • P 的数量可以通过 GOMAXPROCS() 设置。它实际上表示真正的并发级别,即可以同时运行的 Goroutine 数量。
  • 图中灰色的 Goroutine 尚未运行,处于就绪状态,等待被调度。P 维护了这些 Goroutine 的队列(称为运行队列 runqueue)。
  • 每次执行 go 语句时,都会将一个 Goroutine 添加到运行队列末尾。在下一个调度点,会从运行队列中取出一个 Goroutine 执行
  • 当某个操作系统线程(如 M0)被阻塞时(如下图所示),P 会切换到另一个线程(如 M1)。M1 可能是新创建的,也可能是从线程缓存中取出的。
  • 当 M0 返回时,它需要尝试获取一个 P 来运行 Goroutine。如果无法获取 P,它会将 Goroutine 放入全局运行队列,并进入休眠状态(进入线程缓存)。所有 P 会定期检查全局运行队列,并运行其中的 Goroutine;否则,全局运行队列中的 Goroutine 将永远无法执行。

在使用多个协程运行程序的时候,很容易出现同时写入一个资源的冲突,有两种解决方式

  1. 使用全局互斥锁对资源进行加锁
  2. 使用管道 channel 来解决

如下所示通过启动多个协程计算 1-200 的各个数的阶乘

package main
import (
	"fmt"
	_ "time"
	"sync"
)

var (
	myMap = make(map[int]int, 10)  
	lock sync.Mutex	//声明一个全局的互斥锁
)

func calculateN(n int) {	// 计算n的阶乘
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	
	lock.Lock()		// 操作myMap前加锁
	myMap[n] = res 
	lock.Unlock()
}

func main() {
	for i := 1; i <= 20; i++ {	// 开启多个协程完成这个任务
		go calculateN(i)
	}
	
	time.Sleep(time.Second * 5)	// 主线程休眠等待协程运算结束
	
	lock.Lock()		// 读取myMap时也需要加锁
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
	lock.Unlock()
}

上面使用互斥锁的方式并不优雅,需要手动估算协程运行结束时间,通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作

服务器通常启动一个协程对请求进行响应

package main
import (
	"fmt"
	"net"
	_"io"
)

func process(conn net.Conn) {
	defer conn.Close() //关闭conn

	// 循环接收客户端发送的数据
	for {
		buf := make([]byte, 1024)
		// 等待客户端通过conn发送信息,如果客户端没有wrtie[发送],那么协程就阻塞在这里
		n , err := conn.Read(buf) //从conn读取
		if err != nil {
			fmt.Printf("客户端退出 err=%v", err)
			return //!!!
		}
		
		fmt.Print(string(buf[:n])) 	// 打印接收到的内容
	}
}

func main() {
	fmt.Println("服务器开始监听....")
	listen, err := net.Listen("tcp", "0.0.0.0:8888")	// 监听本地TCP的8888端口
	if err != nil {
		fmt.Println("listen err=", err)
		return 
	}
	defer listen.Close() //延时关闭listen

	//循环等待客户端来链接
	for {
		fmt.Println("等待客户端来链接....")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("Accept() err=", err)
		} else {
			fmt.Printf("Accept() suc con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
		}
		// 启动协程处理客户端连接
		go process(conn)
	}
}

channel

channel本质就是一个先进先出的队列,它本身就是线程安全的,支持多 goroutine 同时访问

	// 创建一个可以存放3个int类型的管道
	var intChan chan int
	intChan = make(chan int, 3)

	num := 211
	intChan<- num	// 向管道写入数

	// 管道的实际长度和总cap(容量),写入数据时,不能超过其容量,超过再写入的话就会阻塞而报deadlock
	fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3

	var num2 int
	num2 = <-intChan // 从管道中读取数据,如果数据已经全部取出,再取就会报告 deadlock
	fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))  // 2, 3

	close(intChan)	// 使用内置函数 close 可以关闭 channel, 关闭后就不能再向 channel 写数据了

	close(intChan2)	//在遍历时,如果channel没有关闭,则会出现deadlock的错误
	for v := range intChan2 {		// 需要使用for range进行遍历管道
		fmt.Println("v=", v)
	}

当使用interface类型的管道时,需要注意从管道中取出来的还是interface类型,需要做一下转换

	//定义一个存放任意数据类型的管道 3个数据
	allChan := make(chan interface{}, 3)

	cat := Cat{"小花猫", 4}
	allChan<- cat
	allChan<- 10
	allChan<- "tom jack"

	newCat := <-allChan	
	//fmt.Printf("newCat.Name=%v", newCat.Name)		// 编译不通过
	
	a := newCat.(Cat) 	//使用类型断言
	fmt.Printf("newCat.Name=%v", a.Name)

管道的一些使用细节:

	//1. 在默认情况下下,管道是双向的,也可以声明为只读/只写
	var chan2 chan<- int	// 声明为只写
	chan2 = make(chan int, 3)
	chan2<- 20
	var chan3 <-chan int	// 声明为只读
	num2 := <-chan3

	// 2 在实际开发中,可能我们不好确定什么关闭该管道.可以使用select 方式可以解决
	for {
		select {
			// 这里如果intChan一直没有关闭,不会一直阻塞而deadlock,会自动到下一个case匹配
			case v := <-intChan : 
				fmt.Printf("从intChan读取的数据%d\n", v)
			case v := <-stringChan :
				fmt.Printf("从stringChan读取的数据%s\n", v)
			default :
				return 
		}
	}

网站公告

今日签到

点亮在社区的每一天
去签到