golang速成教程

1、go语言概述

go核心编程方向:

  • 区块链研发工程师
  • go服务器端/游戏软件工程师
  • go分布式/云计算软件工程师

go的优势:

  • 数据处理
  • 高并发

google为什么要创造go语言:

  • 硬件技术更新频繁,性能提高很快,现有语言不能合理利用多核多CPU的优势
  • 现有语言计算能力不够,处理大并发不够好
  • 想兼顾运行速度和开发速度

发展简史:

  • 2015年,go1.5版本发布,移除了最后残余的c代码
  • 2017年,先后发布了go1.8和1.9
  • 2018年,发布了go1.10版本

go的特点:

  • go=c+python
  • 从c语言继承了很多理念,如指针
  • 自动垃圾回收
  • 天然并发
    • 从语言层面支持并发
    • goroutine,轻量级线程,可实现大并发处理,高效利用多核
    • 基于CPS并发模型实现
  • 通过管道实现不同的goroute之间的相互通信
  • 函数可以返回多个值
  • 新的语法:延时执行defer、切片slice等

2、基础语法

1、变量使用方式

  1. 指定变量类型,声明后若不赋值,使用默认值
  2. 根据值自动推导类型
  3. 连var也一起省略,但要使用:=符,等价于声明+赋值
var i int
fmt.Println("i=",i)

var j = "hello"
fmt.Println("j=",j)

k := 10.5
fmt.Println("k=",k)

注意:main函数要放在main包下面才能运行,如下图所示。

golang速成教程

2、一次性声明多个变量

	var i,j,k int
	fmt.Println("i=",i,"j=",j,"k=",k)

	var o,p,q = 10,"hello",10.5
	fmt.Println("o=",o,"p=",p,"q=",q)

	r,s,t := 10,"hello",10.5
	fmt.Println("r=",r,"s=",s,"t=",t)

3、声明全局变量

package main

var n1 = 100
var n2 = "okk"
var (
	n3 = 36.9
	n4 = "yes"
)

func main() {
}

4、常量

const a int = 10

常量必须初始化

常量只能修饰bool、数值类型(int、float系列)、string类型

5、go的数据类型

基本数据类型

  • 数值型
    • 整数类型(int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64)
    • 浮点类型(float32,float64)
  • 字符型(没有专门的字符型,使用byte来保存单个字母字符)
  • 布尔型(bool)
  • 字符串

派生/复杂数据类型

  • 指针
  • 数组
  • 结构体
  • 管道
  • 函数
  • 切片(动态数组)
  • 接口
  • map

6、值类型和引用类型

值类型:基本数据类型、数组、结构体

引用类型:指针、切片、map、管道、interface

7、运算符

golang的自增和自减只能当做独立语句使用,不能b:=a++

golang没有++i

3、go字符与字符串

1、go字符的不同之处

对于其它语言来说,字符串是由一串字符组成的,而go的字符串是由一串字节组成的。

    var b1 byte = 'a'
	fmt.Println("b1=",b1) // 输出97

	var b2 byte = 97
	fmt.Printf("b2=%c\n",b2) // 输出a

	b3 := '你'
	fmt.Println("b2=",b3) // 输出unicode码值

下面测试字符和布尔类型分别占用几个字节。

b := 'a'
success := false

fmt.Println(unsafe.Sizeof(b)) // 输出4
fmt.Println(unsafe.Sizeof(success)) // 输出1

如果一个字符的UTF-8编码的字节数大于1,则不能用byte去存,而用int存就没问题。如下图所示。

golang速成教程

2、字符串的基本使用

    var s1 = "hello"+"world" // 拼接
	s1+="hi"
	// 由于go可以不以分号结尾,因此在字符串拼接语句太长需要换行时,每行的结尾必须以+号结尾
	s2 := "我是"+"一个"+
		"中国人"+"hello"+
		"world"
	s3 := `将字符串按原样输出,适用于输出源代码等。\n\t`
	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(s3)

3、基本数据类型之间的显示类型转换

    var a int = 9999
	var b byte = byte(a)
	fmt.Println(b)

4、String和基本类型的相互转换

基本类型转string,第一种方法,如下代码所示。

    var a int = 100
	s := fmt.Sprintf("%d", a) // 使用Sprintf函数进行转换
	fmt.Printf("type=%T,value=%q",s,s) // 输出s的类型和值,%q比%s的输出结果多一个双引号

基本类型转string,第二种方法,如下代码所示。

    var a int = 100
	s := strconv.FormatInt(int64(a), 2)
	fmt.Printf("type=%T,value=%q",s,s)

	// f表示格式,10表示小数位保留10位,64表示这个小数是float64
	s = strconv.FormatFloat(12.99, 'f', 10, 64)
	fmt.Printf("type=%T,value=%q",s,s)

string转为基本类型,如下代码所示。

    b, err := strconv.ParseBool("fals1")
	// b, _ := strconv.ParseBool("true") // 如果不想获取err信息则使用_
	fmt.Printf("%v",b) // 如果不能转换成一个有效的值,则转为默认值,string转为其它类型也一样
	fmt.Println(err)

4、获取用户输入

	// 方式1
	var name string
	var age int
	// fmt.Scanln(&name)
	// fmt.Println("你的姓名是:",name)

	// 方式2
	fmt.Scanf("%s %d",&name,&age)
	fmt.Println("你的姓名是:",name,",你的年龄是:",age)

5、生成随机数

	// 设置种子
	rand.Seed(time.Now().Unix())
	n:=rand.Intn(100)+1
	fmt.Println(n)

6、指针

    // 输出基本类型的地址
	i := 10
	fmt.Println(&i)

	// 声明一个指针,类型是*int,且p本身的值是&i
	var p *int = &i
	fmt.Println(p) // 输出p本身的值
	fmt.Println(&p) // 输出p的地址
	fmt.Println(*p) // 输出p指向的值

7、流程控制语句

1、if和switch

	if 5>3 {
		fmt.Println("你好")
	}

	// 支持在if中定义变量
	if age:=10;age>9 {
		fmt.Println("hello")
	}

	score:=30
	switch score {
	case 10,20: fmt.Println("差劲") // 自动带break
	case 30,40: fmt.Println("还行"); fallthrough // 默认只穿透一层
	default: fmt.Println("走3")
	}

	// 这种写法跟if-else差不多
	switch {
	case score<10 || score>90:fmt.Println("奇葩")
	default:fmt.Println("中规中矩")
	}

	var service interface{}
	var y = 10
	service = y
	switch service.(type) {
	case nil:fmt.Println("空类型")
	case int:fmt.Println("int类型")
	case float64:fmt.Println("float64类型")
	}

2、for循环

    for i := 1; i < 10; i++ {
		fmt.Println("hello")
	}
	// 相当于while循环,go中没有while关键字
	j:=1
	for j<10{
		fmt.Println("hi")
		j++
	}
	// 无限循环的两种写法
	// for ;;{} 或 for{}

	// for-range,可遍历字符串和数组
	// 传统方式遍历字符串,按字节遍历,不能含中文
	s:="hello,world中"
	for i:=0;i<len(s);i++{
		fmt.Printf("%c\n",s[i])
	}
	// for-range是按字符遍历的
	for index,item:=range s{
		fmt.Printf("%d-%c\n",index,item)
	}
	// 将string转为切片也可以实现按字符遍历
	s2 := []rune(s)
	for i:=0;i<len(s2);i++{
		fmt.Printf("%c\n",s2[i])
	}

3、goto语句

	label:
	fmt.Println("goto test1")
	fmt.Println("goto test2")
	goto label

8、函数

1、函数核心

语法:

func 函数名 (形参列表) (返回值类型列表){
}

注意点:

  • 在接收多个返回值时,可以用占位符_忽略某个返回值。

  • 如果返回值只有一个,返回值类型列表可以不写。

  • go函数不支持重载。

  • go函数的参数传递是值传递,如果希望在函数内修改变量值,则可以传入变量地址,在函数内用指针操作变量。

函数也是一种数据类型,可以赋值给变量,函数可以作为形参。如下代码所示。

	// 定义匿名函数,并赋值给变量
	mysum := func(a int,b int) int {
		return a+b
	}
	fmt.Printf("调用mysum[%T][%d]",mysum,mysum(10,20))

自定义数据类型:type myint int、type mysum func(int,int) int,相当于取别名。如下代码所示。(这个语法用处不大)

	type lensum func(s1 string,s2 string)int
	var totallen lensum
	totallen = func(s1 string,s2 string) int {
		return len(s1)+len(s2)
	}
	fmt.Println("自定义类型:",totallen("hello","hi"))

也支持给函数返回值命名,如下代码所示。(这个语法用处不大)

func sum(a int,b int)(sum int,sub int){
	sum = a+b
	sub = a-b
	return
}

可变参数:

// args是切片,可以通过索引访问
func sum2(args... int) int {
	var sum int
	for i := 0; i < len(args); i++ {
		sum +=args[i]
	}
	return sum
}

初始化函数:

// 每个源文件都可以定义一个init函数,在main函数前执行,被go运行框架调用
func init(){
   fmt.Println("Test_07_function.go初始化了")
}

函数闭包:

	initClickCnt := func(initCnt int) func(int){
		totalCnt := initCnt
        // totalCnt和cnt构成了函数的闭包,totalCnt只初始化一次,相当于全局变量,而cnt相当于局部变量
		return func(cnt int) {
			totalCnt += cnt
			fmt.Println("当前点击次数:",totalCnt)
		}
	}
	printClickCnt := initClickCnt(100)
	printClickCnt(8) // 108
	printClickCnt(10) // 118
	printClickCnt(2) // 120	

defer延迟执行:

	deferTest := func() {
		// 相关语句进入defer栈中,待方法结束后,以后进先出的方式执行defer栈中的语句。可用于关闭文件句柄、数据库连接、锁等资源。
		defer fmt.Println("关闭某资源")
		defer fmt.Println("关闭某句柄")
		fmt.Println("执行逻辑")
	}
	deferTest()

2、系统函数

时间和日期相关函数:

	now := time.Now() // 获取当前时间
	fmt.Printf("%02d-%02d-%02d %02d:%02d:%02d\n",now.Year(),now.Month(),now.Day(),now.Hour(),
		now.Minute(),now.Second())
	fmt.Println(now.Format("2006-01-02 15:04:05")) // 必须要这样写
	time.Sleep(3*time.Second) // 休眠3秒

3、内置函数

len() 求字符串、数组长度

new() 用来分配内存,主要用来分配值类型(填充默认值),返回的是指针。如下代码所示。

	p := new(int)
	fmt.Printf("类型[%T]指针值[%v]指针地址[%v]指针指向的值[%v]指针指向的值类型[%T]",p,p,&p,*p,*p)	

make() 用来分配内存,主要用来分配引用类型,返回的是指针

9、go语言规范

1、包规范

golang速成教程

如果想要编译成一个可执行文件,需要有main包,如果只是写一个库,则可以不需要main包。

在gopath目录下执行如下命令编译成可执行文件:

// 编译时需要编译main包所在的文件夹
// 编译后生成的可执行文件再gopath目录下,也可以通过-o参数指定路径和文件名
go build -o bin/my.exe project_name/package_name/main

在import包时,路径从GOPATH/src开始写,不用带src。

在main包下面有多个main函数,在编译时如何忽略?

  • 只需要在要忽略的main函数所在文件顶部加上// +build ignore 并且至少留一个空行即可。

2、标识符规范

包名尽量和目录名一致

如果变量名、函数名、常量名首字母大写,则为public,否则为private

10、go语言异常处理

func main() {
	// 测试异常处理
	test()
	// 自定义错误
	customError := func(s string) error{
		if(s == "小明"){
			return nil
		}else{
			return errors.New("参数值不是小明!")
		}
	}
	if err := customError("小张");err!=nil {
		panic(err)
	}
}

func test() {
	defer func() {
		if err:=recover();err != nil{
			fmt.Println("test方法发生了异常")
		}
	}() // 写()是为了要执行它
	a:=10
	b:=0
	c:=a/b
	fmt.Println(c)
}

11、数组与切片

1、数组

	// 定义数组的几种方式
	// 方式1
	var arr [5]int
	arr[0] = 2
	fmt.Println(arr[0])
	// 方式2
	var arr2 = [5]int{1,2,3,4,5}
	fmt.Println(arr2[0])
	// 方式3
	var arr3 = [...]int{1,2,3}
	fmt.Println(arr3[0])
	//方式4,如果写...则为数组类型,否则为切片
	var arr4 = [...]string{1:"关羽",0:"刘备",2:"郑飞"}
	fmt.Printf("%T",arr4)

2、切片

	// 定义切片
	// 方式1:引用数组。如果startIndex为0或endIndex为数组长度则可以省略,如arr[:]
	var slice []int = arr2[1:4]
	fmt.Println("切片的值:",slice)
	fmt.Println("切片的元素个数:",len(slice))
	fmt.Println("切片的容量:",cap(slice))
	// 方式2
	var slice2 []int = make([]int,4,16)
	fmt.Println("切片的值:",slice2)
	// 方式3:直接引用数组
	var slice3 []int = []int{1,2,3,4,5}
	fmt.Println("切片的值:",slice3)
	// 切片动态增长
	slice3 = append(slice3,6)
	fmt.Println("切片的值:",slice3)
	// 切片拷贝(数组不行)
	var slice4 []int = make([]int,10,10)
	copy(slice4,slice3)
	fmt.Println(slice4)

3、string与slice

string底层是byte数组,因此也可以切片处理

	// 字符串转成切片,也可以转成[]byte
	s := "hello中国"
	slice5 := []rune(s)
	for _,item:=range slice5 {
		fmt.Printf("%c",item)
	}

4、多维数组

	var arrtwo [3][4]int = [3][4]int{{1,2,3,4},{5,6,7,8},{9,10,11,12}}
	fmt.Println("\n",arrtwo)

12、Map

key的类型:

  • 可以的:bool、数字、string、指针、channel、接口、结构体、数组等
  • 不可以:slice、map、function,因为这几个没法用==判断
	// map可以动态扩容
	var wordcnt map[string]int = make(map[string]int,10)
	wordcnt["关羽"] = 20

	// 声明时就初始化
	var wordcnt2 map[string]int = map[string]int{
		"张飞":20,
		"赵云":16,
		"曹操":32,
	}
	fmt.Println(wordcnt2)

	//删除一个key,如果想删除所有key,则可以用make函数分配新内存
	delete(wordcnt2,"张飞")

	// 查找
	value,b := wordcnt2["曹操"]
	fmt.Println(value,b)

	// 遍历,只能用for-range
	for k,v:=range wordcnt2 {
		fmt.Println(k,"=",v)
	}

	// map切片
	var mapslice []map[string]int = make([]map[string]int,2)
	// 动态增加
	element := map[string]int{
		"hello":5,
		"ok":8,
	}
	mapslice = append(mapslice,element)
	fmt.Println(mapslice)

13、go语言面向对象编程

1、结构体struct

注意点:

  • struct与class类似。
  • go面向对象编程非常简洁,没有继承、方法重载、构造函数、析构函数、隐藏的this指针等。
  • go仍然有封装、继承、多态,只不过实现方式不一样,比如继承没有extends关键字,继承通过匿名字段(组合)实现。
  • 面向接口编程是go很重要的特性。
  • 结构体属性的地址是连续分配的
  • 两个不同的结构体做类型转换时,需要对应的字段全部相同
  • 可以给每个字段起一个tag,在序列化时会用到
  • 由于go的方法作用在指定数据类型上,因此不仅是struct,int、float等也可以有方法
  • 类型可以实现string方法(等同于Java中的toString方法)
type User struct {
	name string `json:"name"`
	age int `json:"age"`
	password string `json:"password"`
}

func (u *User) addAge(b int) {
	// 结构体是值类型,在参数传递时是值拷贝,因此不能修改原struct的属性
	// 如果想修改原struct的属性,需要传入指针,即*User,且底层做了优化,可以用u.age代替(*u).age
	u.age = u.age+b
}

func main() {
	// 方式1:声明结构体变量并赋值
	var u1 User = User{"张三",10,"123456"}
	fmt.Println(u1)
	// 方式2:
	var u2 *User = new(User)
	(*u2).name = "李四"
	u2.password = "ok" // 这样写也可以是因为:在底层做了转换
	fmt.Println(*u2)
	// 方式3
	var u3 *User = &User{}
	u3.name="王五"

	// 调用方法
	u1.addAge(5)
	fmt.Println(u1.age)
}

2、继承

继承通过匿名字段实现。

type Animal struct {
	name string
	age int
}
type Dog struct {
	Animal
	owner string
}

func main() {
	dog := Dog{}
	dog.owner = "张三"
	dog.name = "拉布拉多"
	dog.age = 1
	fmt.Println(dog)
}

注意点:

  • 可继承所有字段和方法,包括私有
  • 当父子含有相同名称的字段或方法时次,采用就近原则
  • 当多重继承时,且多个父结构体之间存在同名字段或方法,则必须指定是哪个父结构体

3、接口与多态

type UserService interface {
	get() string
	insert() int
}

type UserServiceImpl struct {

}

func (service UserServiceImpl) get() string {
	fmt.Println("执行get方法")
	return "name:张三;age:10"
}
func (service UserServiceImpl) insert() int {
	fmt.Println("执行insert方法")
	return 8
}

func doService(service UserService) {
	fmt.Println(service.get())
	fmt.Println(service.insert())
}
func main() {
	doService(UserServiceImpl{})
}

注意点:

  • go没有implements关键字
  • 实现接口只需要一个自定义类型,然后实现接口里面的所有方法即可
  • 空接口interface{}没有任何方法,因为认为所有类型实现了该接口,相当于Java总的Object类
  • 接口中不能定义变量
  • 接口可以继承
  • go中多态是通过接口实现的,支持多态参数和多态数组

4、类型断言与强转的区别

	// 类型断言与强转的区别
	var b UserService
	var a interface{} = UserServiceImpl{}
	// 强转。报错,因为不知道a的具体类型,因此无法强转
	// b = UserService(a) //
	// 类型断言
	b = a.(UserService)
	doService(b)
	// 带检测的类型断言
	c,ok:=a.(UserService)
	if ok {
		doService(c)
	}

14、文件操作

1、基本使用

// 打开文件
	file, _ := os.OpenFile("F:/tmp/test.txt",os.O_RDONLY,os.ModePerm)
	// 关闭文件
	defer file.Close()
	// 循环读取文件
	reader := bufio.NewReader(file)
	for{
		line, err2 := reader.ReadString('\n')
		if err2 == io.EOF {
			// 表示读到了文件末尾
			break
		}
		fmt.Println(line)
	}
	
	// 一次性读取文件
	content, _ := ioutil.ReadFile("F:/tmp/test.txt")
	fmt.Println(content)

	// 写文件
	writer := bufio.NewWriter(file)
	writer.WriteString("hello")
	writer.Flush()
	
	// 判断文件是否存在
	_, err := os.Stat("F:/tmp/test.txt")
	if err == nil {
		fmt.Println("存在")
	}else if os.IsNotExist(err){
		fmt.Println("不存在")
	}else{
		fmt.Println("不确定")
	}
	
	// 文件拷贝
	io.Copy(writer,reader)

2、案例

1、分割文件

描述:将一个大文件分割为n个小文件

参数:

  • inFilePath 大文件路径
  • outFilePath 小文件存放目录
  • splitLineNum 行号数组,如[2,4,6],会分别在大文件的第2、4、6行切割,最后生成4个小文件
func Split(inFilePath string,outFilePath string,splitLineNum []int)  {
	file, err := os.OpenFile(inFilePath, os.O_RDONLY, os.ModePerm)
	suffix := path.Ext(inFilePath)
	defer file.Close()
	if nil != err{
		fmt.Println("文件打开失败!")
		return
	}
	reader := bufio.NewReader(file)

	// 初始化变量
	cnt := 0
	index := 0
	num := splitLineNum[index]
	splitFile,_ := os.OpenFile(outFilePath+"/split_"+fmt.Sprintf("%d",index)+suffix, os.O_APPEND|os.O_CREATE, os.ModePerm)
	writer := bufio.NewWriter(splitFile)

	defer  splitFile.Close()
	for {
		line, err := reader.ReadString('\n')
		if io.EOF == err {
			break
		}
		// 判断是够读到了分割线
		if cnt == num {
			// 一定要在文件close之前flush
			writer.Flush()
			splitFile.Close()
			// 换下一个文件
			index++
			if index < len(splitLineNum) {
				num = splitLineNum[index]
			}
			splitFile,_ = os.OpenFile(outFilePath+"/split_"+fmt.Sprintf("%d",index)+suffix, os.O_APPEND|os.O_CREATE, os.ModePerm)
			writer = bufio.NewWriter(splitFile)
		}
		writer.WriteString(line)
		cnt++
	}
	writer.Flush()

}

2、计算子文件大小

描述:输入一个路径,输出该路径下所有子文件的大小,默认按文件大小从大到小排序

参数:

  • inFilePath 输入路径
  • unit 单位
func calculateChildFileSize(inFilePath string) int64{
	stat, _ := os.Stat(inFilePath)
	file, _ := os.OpenFile(inFilePath, os.O_RDONLY, os.ModePerm)
	defer file.Close()
	if stat.IsDir() {
		childs, _ := file.Readdir(-1)
		var total int64
		for _,item := range childs{
			total += calculateChildFileSize(inFilePath + "/" + item.Name())
		}
		return total
	}else{
		return stat.Size()
	}
}
func ChildFileSize(inFilePath string,unit string){
	file, _ := os.OpenFile(inFilePath, os.O_RDONLY, os.ModePerm)
	defer file.Close()
	childs, _ := file.Readdir(-1)
	var fileSizeMap map[int64]string = make(map[int64]string,10)
	for _,item := range childs{
		fileSizeMap[calculateChildFileSize(inFilePath+"/"+item.Name())] = item.Name()
	}
	var keys []int
	for k,_ := range fileSizeMap {
		keys = append(keys,int(k))
	}
	sort.Ints(keys)
	for i:=len(keys)-1;i>=0;i--{
		if unit == "KB" {
			fmt.Println(fileSizeMap[int64(keys[i])],"\t",keys[i]/1024,"KB")
		}else if unit == "MB" {
			fmt.Println(fileSizeMap[int64(keys[i])],"\t",keys[i]/(1024*1024),"MB")
		}else{
			fmt.Println(fileSizeMap[int64(keys[i])],"\t",keys[i],"B")
		}
	}
}

3、文件同步

描述:将文件从一个地方同步到另一个地方,默认以覆盖方式同步,但目的地有而源头没有的文件不会被删除

参数:

  • dst:目的地路径
  • src:源文件或源文件夹
func Sync(dst string,src string) {
	// 判断是否是文件夹,假设src是都存在的
	stat, _ := os.Stat(src)
	if !stat.IsDir() {
		f1, _ := os.OpenFile(src, os.O_RDONLY, os.ModePerm)
		if _, err := os.Stat(dst+"/"+path.Base(src));nil == err || os.IsExist(err){
			// 如果存在,则删除之前的文件
			os.Remove(dst+"/"+path.Base(src))
		}
		f2, _ := os.OpenFile(dst+"/"+path.Base(src), os.O_RDWR|os.O_CREATE, os.ModePerm)
		defer f1.Close()
		defer f2.Close()
		io.Copy(f2,f1)
	}else {
		if _, err := os.Stat(dst+"/"+path.Base(src));nil != err && os.IsNotExist(err){
			// 如果不存在,则创建文件夹
			os.Mkdir(dst+"/"+path.Base(src),os.ModePerm)
		}
		f, _ := os.OpenFile(src, os.O_RDONLY, os.ModePerm)
		childs, _ := f.Readdir(-1)
		for _,item := range childs{
			Sync(dst+"/"+path.Base(src),src+"/"+item.Name())
		}
	}
}

15、命令行参数

可以通过os.Args获取所有命令行参数。

更方便的是,可以指定参数名(如-name),在指定参数名的情况下,通过如下方式获取参数。

var name string
flag.Stringvar(&name,"name","默认值","用户名")
flag.Parse()

16、序列化与反序列化

type Student struct {
	Name string `json:"name"`
	Age int `json:"age"`
	Score int `json:"score"`
}
// 序列化
	stu := Student{"张三",23,98}
	jsonStr, _ := json.Marshal(stu)
	fmt.Println(string(jsonStr))
// 反序列化
	var stu2 Student
	_ = json.Unmarshal([]byte(jsonStr), &stu2)
	fmt.Println(stu2)

17、goroutine与channel

1、goroutine概述

go可以轻轻松松起上万个协程,原因是在底层做了优化,可以理解为更轻量级的线程

go协程的特点:

  • 有独立栈空间
  • 共享程序堆空间
  • 调度由用户控制

如果主线程退出了,则协程即使还没有执行完毕,也会退出。

主线程是一个物理线程,直接作用在CPU上,是重量级的,非常耗CPU资源。

协程是从主线程开启的,是轻量级的线程,是逻辑态,对资源消耗相对小。

其它编程语言是基于线程的,开启过多的线程资源耗费大,这一点go的并发优势就很大。

2、goroutine并发模型

MPG模式:

  • M:操作系统的主线程(是物理线程)
  • P:协程执行需要的上下文
  • G:协程

一个程序可以有多个M,当多个M在不同的CPU上运行就是并行。

每个M对应一个P、一个正在运行的G和一个协程等待队列。

如何开启一个协程?

go test()

3、并发安全

当有并发安全问题时,编译带-race参数,然后在执行exe文件,结果是正确的。

如何保证线程安全:

  • var lock sync.Mutex
    lock.Lock()
    lock.Unlock()
    
  • channel(不同goroutine之间进行通讯)

4、channel概述

channel本质就是一个队列。

多goroutine访问channel时,不需要加锁,channel本身是线程安全的。

channel是有类型的,string channel只能存放string类型。

channel的基本使用:

	// 声明channel
	var intChan chan int
	intChan = make(chan int,10)
	// 写数据,写多了就报错
	intChan<-10
	intChan<-20
	intChan<-30
	intChan<-40
	// 这个容量不会动态增长
	fmt.Println(len(intChan),cap(intChan))
	// 读数据,读多了就报错
	ele := <-intChan
	fmt.Println(ele)
	// 关闭管道,关闭之后只能读不能写
	close(intChan)
	// channel可以遍历,但如果channel没有关闭,则会报错,否则正常遍历
	for item:=range intChan{
		fmt.Println(item)
	}

注意事项:

  • channel可以声明为只读或只写。
  • 使用select可以解决从管道读数据的阻塞问题。
  • 在goroutine中使用recover,可以避免goroutine因panic而退出。

18、反射

通过反射(reflect.TypeOf函数)可以获取到变量的类型(reflect.Type接口)。

通过反射(reflect.ValueOf函数)可以获取到变量的值(reflect.Value结构体)。

19、网络编程

func server() {
	server, err := net.Listen("tcp", "0.0.0.0:8888")
	if err != nil {
		fmt.Println(err)
		return
	}
	defer server.Close()
	fmt.Println("服务端启动成功!")
	for {
		client, _ := server.Accept()
		fmt.Println("客户端连接成功!")
		var data []byte = make([]byte,128)
		client.Read(data)
		fmt.Println("服务端接收到消息:",string(data))
	}
}
func client() {
	client, _ := net.Dial("tcp", "localhost:8888")
	defer client.Close()
	data := []byte("你好,golang!")
	client.Write(data)
}

func main() {
	go server()
	time.Sleep(1*time.Second)
	client()
}
上一篇:解析struct的内存布局


下一篇:查看运算顺序,执行顺序