Go学习[合集]

文章目录

Go学习-Day1

  • 个人博客:CSDN博客

  • 打卡。

  • Go语言的核心开发团队:

    • Ken Thompson (C语言,B语言,Unix的发明者,牛人)
    • Rob Pike(UTF-8发明人)
    • Robert Griesemer(协助HotSpot编译器,Js引擎V8)
  • Go语言有静态语言的安全和性能和动态语言开发维护的效率。

  • Go语言特性

    • 继承了C语言很多概念(Ken爷!)包括指针。
    • 引入包的概念
    • 垃圾回收机制
    • 天然并发(核心)
    • 管道通信机制(Channel)
    • 函数可以有多返回值
    • 新增切片slice,延时执行defer
  • Hello World (一定要注意目录结构!)

  • 通过go build来编译go文件,得到exe文件

  • 关于文件夹架构,一定要准确,不然找不到包。

  • %GOPATH%

    • src

      • go_code

        • project00 //项目名open这个项目

        • project01

          • main//包
          • pkg//其他包
  • 注意配置PATH,GOPATH(项目的位置),GOROOT(SDK的位置)

  • 并且配置一些settings里面相应的变量


  • 琐碎的细节
    • go语言没有分号结尾,因此一行就写一条语句
    • 定义的变量和导入的包如果没有用到就无法通过编译
    • 块注释不能嵌套,尽量使用行注释

Go学习-Day2

标识符

  • 驼峰法,首字母大写可以在其他包里使用,首字母小写只能在本包内使用
  • 跨包使用,的import地址从src的子目录开始,src以及src所在的GOPATH自动补全

变量基础语法

  • 定义变量

go 复制代码
var i int = 10
  • var关键字+变量名+变量类型

go 复制代码
var i = 10
  • 自动推断类型

go 复制代码
i := 10
  • 简略写法

  • 对应的,可以声明多个变量

go 复制代码
var a, b, c int = 1, "a", 2
go 复制代码
var a, str1, b = 1, "a", 2
go 复制代码
a, str1, b := 1, "a", 2

go 复制代码
var (
	i = 1
	j = 2
)
  • 另一种声明方法,开发中常用

go 复制代码
import (
	"fmt"
    "unsafe"
)
  • 导包也可以类似这样

  • 不能改变变量的类型,例如开始赋值整数,后来又赋值浮点数。
  • 默认值,数默认为0,字符串默认为空串

字符串类型

  • 利用UTF-8编码,支持中文

  • go中字符串是常量,无法修改

  • 引号

    • 双引号"" :会识别转义字符
    • 反引号``:不识别转义字符(防止SQL注入之类的?)
  • 加号拼接,可以分行写(加号放行尾)

类型转换

  • go不会自动转换类型,需要显式转换

go 复制代码
var i int = 1
var j float32 = float32(i)

string和其他基本类型转换

其他类型转string

go 复制代码
func main() {
	var a int = 10
	var b float32 = 3.14
	var s string = fmt.Sprintf("%d %.2f", a, b)
	
	fmt.Println(s)
}

string转其他类型

go 复制代码
b, err := strconv.ParseBool("true")
f, err := strconv.ParseFloat("3.1415", 64)//返回64位要 强转
i, err := strconv.ParseInt("-42", 10, 64)//进制 和 位数
u, err := strconv.ParseUint("42", 10, 64)
  • 返回值有两个
  • 可以使用_代替err,下划线是特殊的变量,表示忽略返回值。
  • 如果无法转换则返回0
  • 学英语:parse是分析的意思,strconv = string-conversion

指针类型

  • 和C语言类似,不赘述。

运算符

  • 没有三元运算符,只能用if else,if后面没有小括号
  • 运算与C语言一致
  • 自增自减只能单独使用,不能在自增自减的同时给变量赋值
  • 自增自减的++和--都必须放在变量的后边!

标准IO

  • string也是基本类型,传入&地址。

go 复制代码
func main() {
	var str string
	_, _ = fmt.Scanln(str)
	fmt.Println(str)
}

分支语句

  • 基本和C语言一致
  • switch 不用break;
  • 可以匹配多个表达式,逻辑或的关系
  • case,switch后面是一个表达式(不一定是常量)
  • case和switch的数据类型必须一致
  • case的常量(字面量)不能重复
  • switch后面可以不带表达式,可以代替if else作分支选择
  • fallthrough关键字可以穿透到下一分支,用来代偿省略break的功能

Go学习-Day3

循环语句

  • 传统方法

go 复制代码
func main() {
	for i := 1; i < 10; i++ {
		fmt.Println("hello!")
	}
}
  • for - range方法

go 复制代码
func main() {
	str := "abcde"
	for idx, val := range str {
		fmt.Printf("%v %c\n", idx, val)
	}
}
  • idx是下标,val是值

  • go没有while和do-while使用for来实现

go 复制代码
	for {
		if i > 10 {
			break
		}
		fmt.Println(i)
		i++
	}

函数

声明

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

go 复制代码
func add(a int, b int) int {
	return a + b
}
  • 分包写函数

go 复制代码
package main

import (
	"fmt"
	"go_code/project01/model"
)

func main() {
	fmt.Println(model.Add(1, 2))
}
  • 包名和文件夹名可以不一致,这样下面调用的时候也要用另外的包名,一般来说,我们习惯于名字保持一致,这样我们导入包的时候,就就能知道包名是什么了。

  • 再复习一个点,Go语言没有public private关键字,是用变量和函数第一个字母大小写来判断公有还是私有,大写是公有,小写是私有。

  • 导包的路径,是从GOPATH/src 后面的部分,一直导到包文件夹。

  • 在同一包下,不能又相同的函数名,不支持函数的重载

  • 如果要编译可执行文件必须声明main包,main包是唯一的。

init函数

  • 每个源文件都可以包含一个init函数,这个函数会在main函数之前被调用,全局变量定义,init函数,main函数的顺序调用
  • 可能相当于类当中的构造函数

匿名函数

  • 相当于把整个函数体当作函数的名字,后面的括号就是传入的参数列表

go 复制代码
func main() {
	res := func(a int, b int) int {
		return a + b
	}(1, 2)
	fmt.Println(res)
}
  • 如果不带括号,可以重复调用匿名函数,类似lamda表达式

go 复制代码
	res := func(a int, b int) int {
		return a + b
	}
	//fmt.Println(res)
	
	fmt.Println(res(1, 2))

闭包

go 复制代码
func main() {
	f := func() func(int) int {
		var n int = 10
		return func(x int) int {
			n = x + n
			return n
		}
	}
	ff := f()
	ff(1)
	ff(2)
	fmt.Println(ff(3))
}
  • 答案16

  • 这里是一个返回值是(int)int的匿名函数,返回了一个含有未知参数并且引用了n的匿名函数,对这个匿名函数多次调用。

  • 函数和引用的外部变量构成了闭包,相当于一个类,第一次调用得到一个匿名函数,可以类比成一个构造方法,构造出了一个类,n是类的一个成员。

  • 或者,我们这样想,这个匿名函数和他所引用的变量构成的闭包,在匿名函数第一次返回的时候,这些变量也在相同的作用域进行声明。

defer

  • to delay sth until a later time 推迟;延缓;展期(摘自牛津)

go 复制代码
func main() {
	defer fmt.Println("ok1")
	defer fmt.Println("ok2")
	
	fmt.Println("ok3")
	fmt.Println("ok4")
	
}
  • 输出顺序是:3->4->2->1

  • defer先不执行,等到函数快要释放的时候,defer执行顺序遵从栈的顺序,先进后出

  • 当语句压入栈的时候,相关引用的变量也会拷贝一份进入栈。

Go学习-Day4

函数

值传递,引用传递

  • 值传递直接拷贝值,一般是基本数据类型,数组,结构体也是
  • 引用传递传递地址 ,效率高,指针,slice切片,map,管道,interface等

常用的函数

go 复制代码
len(string str)//求字符串长度,自带的不用包,中文一个字三字节
  • 转成[]rune来处理

  • \]byte转string

    go 复制代码
    str = string([]byte{...})
  • 查找子串是否存在

  • 若干字符函数

go 复制代码
strings.Contains("aaa", "aaa") //bool
strings.Index("aaa", "aaa")//返回下标
strings.LastIndex("aaa", "aaa")//返回最后一个下标,没有就返回-1
strings.Replace(str, str1, str2, n)//把1中str1替换成str2,n是替换个数,-1表示全部替换
strings.Split(str, "某字符")//分割字符串
strings.TrimSpace(str)//裁剪空格,去掉前导和后导空格
strings.Trim(str, "字符集")//去掉指定字符
strings.TrimLeft()//同上,去掉左侧,并且还有TrimRight
strings.HasPrefix(str, "后缀")//前缀匹配
strings.HasSuffix()//同上,但是后缀
  • 若干时间函数

go 复制代码
now := time.Now()//返回时间类型,当前时间
//2023-08-23 16:37:07.5402748 +0800 CST m=+0.001148901 大概是这样
//时间类型是结构体,可以使用.运算符来获取其他时间信息,now.Year()
//月份可以直接转int
time.Sleep(time.Millisecond * 100)//只能用乘法,不能有浮点数,利用时间单位常量
time.Unix()//获取unix秒时间戳
time.UnixNano()//unix纳秒时间戳
  • 内置函数built-in

go 复制代码
len()//统计字符串长度,数组大小
new(Type) *Type //参数为类型,返回一块对应大小的清空的内存块的指针

异常处理

  • Go中没有try catch

  • Go利用defer panic recover来处理异常

  • 抛出一个panic的异常,在defer中通过recover捕获异常

go 复制代码
package main

import "fmt"

func test() {
	defer func() {
		err := recover() //捕获异常
		if err != nil {
			fmt.Println(err)
		}
	}()
	num1 := 10
	num2 := 0
	res := num1 / num2
	fmt.Println(res)
}
func main() {
	test()
	fmt.Println("ok")
}
  • 通过捕获异常,可以使得程序不崩溃停止!main函数的其他部分照常运行

  • 自定义错误

go 复制代码
func myError(x int) (err error) {
	if x == 0 {
		return nil
	} else {
		return errors.New("错误")
	}
}

func test() {

	err := myError(1)
	if err != nil {
		panic(err)
	}
}

func main() {
	test()
}
  • panic会终止程序

  • 捕获自定义错误

go 复制代码
func myError(x int) (err error) {
	if x == 0 {
		return nil
	} else {
		return errors.New("错误")
	}
}

func test() {
	defer func() {
		err := recover() //捕获异常
		if err != nil {
			fmt.Println(err)
		}
	}()
	err := myError(1)
	if err != nil {
		panic(err)
	}
}

func main() {
	test()
	fmt.Println("ok")
}

数组

  • 定义

go 复制代码
func main() {

	var arr [10]int
	arr[0] = 1
	fmt.Println(arr)

}
  • 数组名地址&arr

  • 初始化

go 复制代码
var arr [3]int = [3]int{1, 2, 3}

var arr = [3]int{1, 2, 3}

var arr = [...]int{1, 2, 3}

var arr = [...]int{1: 800, 0: 900, 2: 999}//指定下标

var arr := [...]int{1, 2, 3} //自动推导
  • 遍历for-range 同string 不赘述

  • 数组中的元素可以是任何合法的类型,但是不能混用

  • Go中数组是值类型,会进行拷贝,要想修改原数组,需要使用指针,写法类似C语言的行指针

Slice切片

  • 切片是引用类型,传递地址

  • 切片和数组类似,但是长度是可以变化的!

  • 声明

go 复制代码
var a []int
go 复制代码
func main() {
	var arr [5]int = [...]int{1, 2, 3, 4, 5}

	slice := arr[1:3] //从下标1与下标3,左闭右开的区间
	fmt.Println(slice)
}
  • 通过make声明

go 复制代码
func main() {
	slice := make([]int, 2, 4)//容量可以不声明
	fmt.Println(slice)
}
  • make在底层维护一个数组,这个数组对外不可见

  • 直接声明

go 复制代码
var slive []string = []string 
  • 遍历和数组类似,不再赘述

  • 简写

go 复制代码
var slice = arr[:end] // var slice = arr[0:end]前缀,不含end
var slice = arr[start:]//var slice = arr[start:]后缀
var slice = arr[:]//var slice = arr[0:len(arr)]全长
  • 切片可以继续切片

  • 切片可以追加,可以追加多个数,可以追加多个切片,利用append将追加后的切片赋值给原来的切片

go 复制代码
slice1 = append(slice1, 1, 2, 3)//追加数
slice1 = append(slice1, slice1...)//要三个点
  • Go底层会创建一个新的数组,然后切片这个新的数组,这些过程均不可见

  • string可以进行切片处理

go 复制代码
str := "sssssss"
slice := str[2:]//从下标2开始切后缀
  • string底层也指向一个byte数组,我们用切片来拷贝这个只读的byte数组再进行操作

  • 通过切片能够改变字符串

go 复制代码
arr := byte[](str)
arr[0] = 'a'
str = string(arr)
//但是不支持中文
arr := rune[](str)
arr[0] = '好'
str = string(arr)
//弄中文
func main() {
	str := "?????"
	arr := []rune(str)
	arr[0] = '好'
	fmt.Println(string(arr))
}

Go学习-Day5

map

  • map是一个key-value的数据结构,又称为字段或关联数组

  • Golang自带的map是哈希表

  • 声明

go 复制代码
import "fmt"

func main() {
	var a map[int]int
	fmt.Println(a)
}
  • slice,map和func不能作为键值

  • 声明map是不会分配内存的,初始化需要用make

go 复制代码
import "fmt"

func main() {
	var a map[int]int
    a = make(map[int]int, 3)//可以存放三个键值对
	fmt.Println(a)
}
  • Go的map的键值是没有顺序的

  • 自动增长

go 复制代码
func main() {
	a := make(map[int]int)
	a[1] = 2
	a[2] = 1
	fmt.Println(a)
}
  • 直接初始化

go 复制代码
func main() {
	a := map[int]int{
		1: 1,
		2: 2,//这里也要,
	}
	fmt.Println(a)
}

增加和更新

  • 直接给键值赋值即可

删除

go 复制代码
delete(map, 1)//删除map键值为1的对,如果不存在不会操作
  • 如果要完全清空

  • 遍历key来删除,或者让map赋值一个新的map,给GC回收

查询

go 复制代码
val, flag := mp[1] //flag是bool,找到是true,没找到是false,val是对应值

遍历(for-range)

go 复制代码
func main() {
	a := map[int]int{
		1: 1,
		2: 2,
	}
	for k, v := range a {
		fmt.Println(k, v)
	}
	fmt.Println(a)
}

map切片

  • 同样slice的用法,注意map也要make就行,相当于在底层维护一个map类型的数组

关于哈希表遍历的一点看法

  • 大概是这样的,在底层有一个迭代器链表,或者,有一个指针数组,每次存放一个指针,迭代器依次访问这些指针,但是在添加元素的时候, 会rehash,导致顺序变化。所以Go的设计者把他设置成无序,每次都打乱这个数组,防止程序员误用哈希map

对map的key排序

  • 将键值追加到切片内,然后对切片排序

go 复制代码
import (
	"fmt"
	"sort"
)

func main() {

	mp := make(map[int]int, 10)
	mp[1] = 2
	mp[3] = 1
	mp[2] = 5
	mp[5] = 6
	var keys []int //切片
	for key, _ := range mp {
		keys = append(keys, key)
	}

	sort.Ints(keys)
	fmt.Println(keys)
}
  • map是引用类型

结构体与OOP

声明、初始化、序列化

  • go语言是用struct来面向对象的

  • 声明

go 复制代码
type Node struct {
	X int
	Y int
}

func main() {

	var a Node//空间自动分配
	fmt.Println(a)

}
//输出{0 0}
  • 赋值

go 复制代码
func main() {

	var a Node
	a.X = 1
	a.Y = 2
	fmt.Println(a)

}
//输出{1 2}
  • 初始化

go 复制代码
//方法一
a := Node{1, 2}

//方法二
ptr *Node = new(Node)
(*ptr).X = 1
(*ptr).Y = 2
//--------
func main() {

	var ptr *Node = new(Node)
	(*ptr).X = 1
	(*ptr).Y = 2
	fmt.Println(*ptr)

}
//---底层自动识别,这样也可以
func main() {

	var ptr *Node = new(Node)
	ptr.X = 1
	ptr.Y = 2
	fmt.Println(*ptr)
}
//方法三
var node *Node = &Node{}
  • 结构体的所有字段在内存中是连续的

  • 两个结构体的字段类型完全相同的话可以强制类型转换

  • 用type struct1 struct2给struct2取别名,相当于定义了一个新的类型,两者之间可以强制类型转换,但是不能直接赋值

  • struct的每个字段上可以写上一个tag,该tag可以通过反射机制获取,常见于序列化和反序列化

  • 复习一个点,Go没有public和private,所以用首字母大写和小写来确定是公共的还是私有的

  • 序列化:对象转json字符串

  • 反序列化:json字符串转对象

  • 动手

go 复制代码
import (
	"encoding/json"
	"fmt"
)

type Node struct {
	Name     string
	Password string
}

func main() {

	a := Node{
		"aaaaaa",
		"123456",
	}

	str, _ := json.Marshal(a)
	fmt.Println(a)
	fmt.Println(string(str))
}
//输出
//{aaaaaa 123456}
//{"Name":"aaaaaa","Password":"123456"}
  • 但是这种大写的变量很多客户端不习惯,所以使用tag,如果使用小写,就无法被结构体外部函数使用,无法序列化

go 复制代码
import (
	"encoding/json"
	"fmt"
)

type Node struct {
	Name     string `json:"name"`
	Password string `json:"password"`
}

func main() {

	a := Node{
		"aaaaaa",
		"123456",
	}

	str, _ := json.Marshal(a)
	fmt.Println(a)
	fmt.Println(string(str))
}
//输出
//{aaaaaa 123456}
//{"name":"aaaaaa","password":"123456"}

方法

  • 方法是作用在指定类型上的函数

go 复制代码
func (a Node) ok() {
    fmt.Println("ok")
}
//这个函数绑定给了Node
//调用
var a Node
p.ok()
  • 动手

go 复制代码
import (
	"fmt"
)

type Node struct {
	Name     string `json:"name"`
	Password string `json:"password"`
}

func (a Node) ok() {
	fmt.Println(a.Name)
}
func main() {

	a := Node{
		"aaaaaa",
		"123456",
	}

	a.ok()
}
//输出了Node的名字
  • 这个方法只能用指定类型来调用,不能直接调用

  • 如果想要修改原来的参数,我们使用结构体指针,并且这更常用,不用深拷贝,速度更快

go 复制代码
type Node struct {
	Name     string `json:"name"`
	Password string `json:"password"`
}

func (a *Node) ok() {
	a.Name = "bbbb"
}
func main() {

	a := Node{
		"aaaaaa",
		"123456",
	}

	a.ok()//编译器底层自动识别变为&a

	fmt.Println(a)
}
  • 可以通过实现String方法,可以自定义格式化输出

工厂模式

  • 类似于构造函数,在结构体所在包下写相应构造的函数,返回结构体指针,这样就可以在结构体私有的情况下,在其他包调用这个函数直接返回结构体对象

Go学习-Day6

封装

  • 类似java的类的封装,这里我们利用大小写和工厂模式来实现封装的功能
  • 略过

继承

  • 相似的类具有相似的方法,反复绑定相同的方法,代码冗余,所以引入了继承的概念

  • 嵌套匿名结构体来实现继承的效果

  • 动手实践!

go 复制代码
type Node struct {
	Name     string `json:"name"`
	Password string `json:"password"`
}

type Point struct {
	Node
	X int
	Y int
}

func (a *Node) ok() {
	a.Name = "bbbb"
}
func main() {

	var a Point = Point{
		Node{"aaa", "bbb"},
		1,
		2,
	}

	a.ok()

	fmt.Println(a)
}
  • 注意看,a.ok()其实是a.Node.ok()底层自动识别,可以省略匿名结构体

  • 基本数据类型可以匿名,但是不能出现多个相同类型的匿名基本类型

接口

  • 多态主要就是由接口来实现的

  • 声明

go 复制代码
type inter interface {
	a()
	b()
}
  • 实现接口

go 复制代码
import "fmt"

type inter interface {
	a()
	b()
}
type obj1 struct {
}

type union struct {
}

func (o obj1) a() {
	fmt.Println("okkk")
	//用node对象给接口的a()实现
}

func (o obj1) b() {
	fmt.Println("ohhh")
}

func (u union) ok(in inter) {
	in.a()
	in.b()
}

func main() {

	x := union{}
	y := obj1{}
	x.ok(y)
}
  • 从上面我们可以看到,在声明一个接口之后,我们用工厂方法法obj对应接口的所有方法给实现了,然后另外整一个抽象类似的的结构体,绑定一个方法运行接口,这样我们就能通过接口把这两个类给链接在一起。

  • 总结。

  • interface类型可以定义一组方法,方法不用实现,并且不能含有变量

  • Go语言没有implements关键字,只要一个变量类型,绑定了接口中所有的方法,这个变量就能实现这个接口

  • 这个变量就能导入到含有接口作为变量的函数内

Go学习-Day7

断言

go 复制代码
type Node struct {
	x int
	y int
}

func main() {
	var a interface{}
	var n Node = Node{1, 2}
	a = n
	var b Node
	b = a.(Node)
	fmt.Println(b)
}
  • 此处我们有一个结构体n给空接口a赋值(空接口没有方法,相当于方法被n给完全实现,所以是可以赋值给接口的),然后我们想把接口赋值给具体的结构体对象,但是,这里会报错,需要使用类型断言的语法。用.(类型)类声明a的类型。

  • 类型断言之后,编译器会判断这个变量是否是指向这个类型,如果是,就转换成这个类型来赋值

  • 就是把抽象的接口转换成具体的类型的方法

  • 如果类型不匹配的话,就会报panic,通过这个方法,可以判断接口个具体类型,执行特定操作

go 复制代码
if a, flag := u.(xxx); flag == true {
    xxxx
} 
  • x.(type)会返回类型

文件

打开/关闭文件

go 复制代码
import (
	"fmt"
	"os"
)

func main() {
	f, err := os.Open("E:\\JetBrains\\GoLandSpace\\src\\go_code\\project01\\main\\test.txt")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Printf("%v", f)

	err = f.Close()
	if err != nil {
		fmt.Println(err)
	}
}
//返回&{0xc00010c780}
  • 先用os包打开文件,再用方法关闭,上面的绝对路径当中,如果用正斜杠要双写,反斜杠就不用

读取文件

go 复制代码
import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	f, err := os.Open("E:/JetBrains/GoLandSpace/src/go_code/project01/main/test.txt")
	if err != nil {
		fmt.Println(err)
	}

	defer f.Close() //函数推出的时候自动关闭

	reader := bufio.NewReader(f) //创建一个缓冲区来读入

	for {
		str, err := reader.ReadString('\n') //读到换行符就停止
		if err == io.EOF {
			break //读到末尾
		}
		fmt.Println(str)
	}
	fmt.Println("-------------------")
}
  • 首先打开文件,然后按行读取,注意读到EOF要结束死循环

  • ioutil.ReadFile可以一次性读入到一个字节数组内,不过文件需要比较小的情况下使用

写入文件

go 复制代码
import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	filePath := "E:/JetBrains/GoLandSpace/src/go_code/project01/main/a.txt"
	file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
	//后面那个int在windows下无用
	if err != nil {
		fmt.Printf("%v", err)
		return
	}
	defer file.Close()

	str := "ok\n"

	writer := bufio.NewWriter(file)

	writer.WriteString(str)

	writer.Flush() //从缓冲区压入文件

}
  • 写入文件的流程也是非常亲切
  • 我们写入写入缓冲区之后,我们必须要flush刷新一下缓冲区,将缓冲区内的字符刷到磁盘上
  • 通过不同的标识符,有追加,截断清除的写入操作
  • 类似的可以拷贝文件,有io.Copy()函数可以更方便地拷贝文件

命令行参数解析

Args

  • os.Args这是go的命令行参数的数组,但是不方便

flag包

go 复制代码
flag.StringVar(&xxx, "x", "", "sss")
//第一个变量是传入的值所存的变量的地址,"x"是传入的参数的名字,""是默认值, "sss"是说明
  • 设置完这些变量之后使用flag.parse()来解析命令行的参数

JSON

  • JSON(JavaScript Object Notation)是一种轻量级的数据交换格式 key-val的形式
  • 中括号表示数组,大括号表示对象,一个个的键值对用,号分隔开
  • 序列化方法上面写过了,用json.Marshal()
  • 反序列化用json.Unmarshal([]byte(str), &xxx)

Go学习-Day8

单元测试

  • testing框架会将xxx_test.go的文件引入,调用所有TestXxx的函数

  • 在cal_test.go文件里面写这个

go 复制代码
package main

import "testing"

func TestAdd(t *testing.T) {
	a, b := 1, 2
	if add(a, b) != 4 {
		t.Fatalf("Wrong Answer!")
	}
}
  • 在cal.go文件里写这个

go 复制代码
package main

func add(a int, b int) int {
	return a + b
}
  • 运行go test -v的命令,就能运行单测

  • 可以得到结果

shell 复制代码
=== RUN   TestAdd
    cal_test.go:8: Wrong Answer!
--- FAIL: TestAdd (0.00s)
  • testing框架import这个test文件之后,会调用所有TestXxx的函数,注意大写!

Goroutine

进程和线程

  • 进程是程序的在操作系统的一次执行过程
  • 线程是比进程更小的单位,一个进程能创建销毁多个线程
  • 一个程序至少有一个进程,一个进程至少有一个线程

并发和并行

  • 多线程在单核上运行,就是并发
  • 多线程在多核上运行,就是并行

Go协程和主线程

  • 主线程类似进程

  • 协程类似线程,是轻量级的线程

  • 协程的特点

    • 有独立的空间
    • 共享程序的堆空间
    • 调度由用户控制
    • 协程是轻量级的线程
go 复制代码
import (
	"fmt"
	"strconv"
	"time"
)

func test() {
	for i := 0; i < 5; i++ {
		fmt.Println("test() calls! " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

func main() {
	go test()

	for i := 0; i < 5; i++ {
		fmt.Println("main() calls! " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}
  • 输出

shell 复制代码
main() calls! 0
test() calls! 0
test() calls! 1
main() calls! 1
main() calls! 2
test() calls! 2
test() calls! 3
main() calls! 3
main() calls! 4
test() calls! 4
  • go关键字会另起一个协程,主线程执行到这里会开一个协程并行执行,如果主线程执行完毕退出,协程会被强制退出

MPG模式

  • M(Machine)是操作系统的主线程,也就是物理线程

  • P(Processor)协程执行的上下文

  • G(Gorountine)协程

  • Go语言的协程是轻量级的,是逻辑态的,可以起上万个协程;而C/java的多线程是内核态的,几千个就会耗光CPU

CPU相关

go 复制代码
runtime.NumCPU()
//获取本地CPU数目
runtime.GOMAXPROCS(int)
//设置GO最大可用的CPU数目
//Go Max Processors

协程并行的资源竞争

  • 多个协程同时访问一个资源会发生冲突,会发生并发问题

  • 在java中我们有锁和原子类来保证并发安全

  • 声明一个全局锁变量lock

go 复制代码
lock sync.Mutex
//sync是同步的意思,Muti-excluded互斥锁?
go 复制代码
lock.Lock()//在进行并发的读写操作的时候,先上个锁
...//在进行操作的时候,别的协程会排队等待
lock.Unlock()//解锁之后,才能给别的协程使用
  • 主线程读的时候也需要加锁,因为底层不知道协程已经解锁了,会发生资源冲突

  • 但是这样不同协程之间没办法通讯,不知道什么时候协成完成任务了,白白空转浪费时间,或者提前结束主线程,终止协程,管道可能能解决这些问题,明天再学

Go学习-Day9

Channel

  • Channel本质是一个队列
  • 多goroutine访问时不需要加锁,Channel天然线程安全
  • channel有类型,只能写入相同类型
  • channel是引用类型
  • channel必须初始化才能写入数据,make分配内存

声明

go 复制代码
	var intChan chan int
	intChan = make(chan int, 3)
  • java不是很熟悉,感觉chan有点像java的原子类

存入取出

go 复制代码
intChan<- xxx //存入
a := <= intChan//取出
  • 管道不会自然增长,不能超过容量,不能从空的管道里取出数据,会上DeadLock

  • 如果想要存储任意类型的管道,可以用空借口

go 复制代码
var allChan chan interface{}
  • 但是,取出的时候注意类型断言

go 复制代码
close(intChan)
  • channel关闭之后就不能再写入了,但是能继续读出

  • 关闭之后能用for-range来遍历,如果不关闭的话会出现死锁

  • 死锁的情况很多,建议多找几篇文章看看,写写实操一下

  • 空的缓冲chan相当于无缓冲的chan,无缓冲的chan需要接收者,传入者,否则就会死锁,注意及时关闭

  • 只向管道内写入,不读取就会deadlock,读得慢没有关系

  • 关键是要给每个管道安排一个发送者,和接收者!!!

一个简单的死锁分析

go 复制代码
package main

import (
	"fmt"
	"time"
)

func write(intChan chan int) {
	for i := 0; i < 5; i++ {
		fmt.Println("写入: ", i)
		intChan <- i
		time.Sleep(time.Second)
	}
	//close(intChan)
}

func read(intChan chan int, exitChan chan bool) {
	for {
		val, ok := <-intChan
		if !ok {
			break
		}
		fmt.Println("读到", val)
	}
	exitChan <- true
	close(exitChan)
}
func main() {

	intChan := make(chan int, 20)
	exitChan := make(chan bool, 1)
	go write(intChan)
	go read(intChan, exitChan)

	for {
		_, ok := <-exitChan
		if !ok {
			break
		}
	}
}
  • 输出
shell 复制代码
写入:  0
读到 0
写入:  1
读到 1
写入:  2
读到 2
写入:  3
读到 3
写入:  4
读到 4
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        E:/JetBrains/GoLandSpace/src/go_code/project01/main/hello.go:36 +0xe8

goroutine 7 [chan receive]:
main.read(0x0?, 0x0?)
        E:/JetBrains/GoLandSpace/src/go_code/project01/main/hello.go:19 +0x99
created by main.main
        E:/JetBrains/GoLandSpace/src/go_code/project01/main/hello.go:33 +0xd9

Process finished with the exit code 2
  • 下面是个人的分析,不一定对,有大佬可以来指正
  • 如果我们不close,channel是可以读的,我们可以边读,边写,并且,读的速度是可以更慢或者更快的,go底层会通过上下文自行判断。
  • 但是这里,我们写的协程,我们关闭channel,在程序运行完之后自行关闭,此时我们读的协程会卡在intChan,等待读入,但是此时还不会报错,因为协程会因为主线程结束而结束。但是后面的exitChan会导致报错
go 复制代码
package main

import (
	"fmt"
	"time"
)

func write(intChan chan int) {
	for i := 0; i < 5; i++ {
		fmt.Println("写入: ", i)
		intChan <- i
		time.Sleep(time.Second)
	}
	//close(intChan)
}

func read(intChan chan int, exitChan chan bool) {
	for {
		val, ok := <-intChan
		if !ok {
			break
		}
		fmt.Println("读到", val)
	}
	fmt.Println("到了这里")
	//exitChan <- true
	//close(exitChan)
}
func main() {

	intChan := make(chan int, 20)
	exitChan := make(chan bool, 1)
	go write(intChan)
	go read(intChan, exitChan)

	time.Sleep(time.Second * 10)
	//for {
	//	_, ok := <-exitChan
	//	if !ok {
	//		break
	//	}
	//}
}
  • 这样并没有报错,并且发现到了这里没有打印,说明read函数作为intChan的接收者一直在等待,这时候。

  • 但是,主线程运行到下面的for的时候,此时exitChan是空的,因为intChan一直在死循环等待,所以触发了死锁

  • 只读只写

go 复制代码
var chanIn chan<- int//只写
go 复制代码
var chanOut <-chan int//只读
  • select {case ...}可以安全地取出数据

  • 使用recover捕获协程终端 panic

Go学习-Day10

反射

  • 编写函数适配器,序列化和反序列话可以用到

  • 反射可以在运行时,动态获取变量的各种信息,例如类型,结构体本身的信息,修改变量的值,调用关联的方法

  • 反射是不是和映射相反?是一种逆函数?

  • 变量到空接口相互转换,空接口和reflect.value相互转换

  • 动手一下

go 复制代码
import (
	"fmt"
	"reflect"
)

func test(a interface{}) {
	b := reflect.TypeOf(a)
	fmt.Println(b)
}

func main() {
	var a int = 10
	test(a)
}
  • 打印 "int"

go 复制代码
reflect.TypeOf()//从接口获取原类型
reflect.ValueOf()//从接口获取reflect.Value类型.Int能取到具体的类型
//如果需要原类型,需要类型断言
reflect.Interface//把reflect.Value转换成空接口
  • Kind是大的种类,Type是小的类型

  • 常量在定义的时候必须初始化

  • reflect.Value.Kind返回的是常量

  • 如果传入指针类型的话(反射常常需要改变原来的值)指针类型需要.Elem方法取到值,再用.SetInt之类的方修改原来的值

go 复制代码
Value//指reflect.Value
Value.NumField()//获取字段数
Value.Field()//根据下标,获取第几个字段,返回的也是relect.Value
Tpye//指reflect.Type
Tpye.Field().Tag.Get("key")//可以获取tag,键值是结构体里面设置的例如,"json:"的key就是json,序列化反序列化的键值固定取json,其实可以自定义
Value.NumMethod()//获取方法数
Value.Method().Call(...)//获取第几个方法,然后调用
//这个顺序是按照函数名字典序排列的,Call传的是Value切片,返回的也是Value切片
//输入的时候需要定义一个Value切片,用reflect.ValueOf(xx)插入这个切片
Value.Elem().Field().SetXxx//修改字段
...FieldByName()//可以用字段名来找
Value.New()//为指针申请空间,可以通过反射来创建类型

网络编程

  • Golang的主要设计目标之一就是面向大规模的后端服务程序,网络通信是服务端程序必不可少的一部分
  • 网络编程有两种 TCP(Transmission Control Protocol) socket编程和HTTP编程(建立在前者之上)
  • 做服务器尽量少开端口,一个端口只能被一个程序监听

监听端口小Demo

  • net包提供了可以指的I/O接口

go 复制代码
package main

import (
	"fmt"
	"net"
)

func main() {

	fmt.Println("开始监听")
	//使用tcp协议,监听本机
	listen, err := net.Listen("tcp", "0.0.0.0:8888")
	if err != nil {
		fmt.Println("err=", err)
	}
	//延迟关闭
	defer listen.Close()
	//循环等待
	for {
		//等待客户端连接
		fmt.Println("等待连接...")
		//获取连接
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("err=", err)
		} else {
			fmt.Println("con=", conn)
		}
		//起一个协程为客户端服务
	}
}
  • 用telnet呼叫一下 telnet 127.0.0.1 8888

go 复制代码
开始监听
等待连接...
con= &{{0xc00010ec80}}
等待连接...
//返回

客户端

go 复制代码
conn, err := net.Dial("tcp", "ip...:端口")
//获取连接
//Dial是拨号的意思
  • 通过端口就能和对应的程序进行交流

go 复制代码
func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("err=", err)
	}
	fmt.Println("连接成功conn=", conn)
}
//注意此时要开着上面的监听程序
//输出 连接成功conn= &{{0xc00010ca00}}

发送&&接收

server.go

go 复制代码
package main

import (
	"fmt"
	"net"
)

func process(conn net.Conn) {
	//连接过多不关闭的话就会导致其他连接无法成功
	defer conn.Close()

	for {
		buf := make([]byte, 512)
		//如果没有Write会停在这里,类似我们stdin输入的时候,光标会停在输入的位置
		//如果连接突然中断的话,这里会报错
		//TCP底层会定时发送消息,检查连接是否存在
		n, err := conn.Read(buf)
		if err != nil {
			fmt.Println("err=", err)
			return
			//有可能是关闭了
		}
		//字节切片要强制转换
		//buf后面的存的可能是乱七八糟的东西,注意取前n个!
		fmt.Print(string(buf[:n]))

	}
}

func main() {

	fmt.Println("开始监听")
	//使用tcp协议,监听本机
	listen, err := net.Listen("tcp", "0.0.0.0:8888")
	if err != nil {
		fmt.Println("err=", err)
	}
	//延迟关闭
	defer listen.Close()
	//循环等待
	for {
		//等待客户端连接
		fmt.Println("等待连接...")
		//获取连接
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("err=", err)
		} else {
			fmt.Println("con=", conn)
		}
		//起一个协程为客户端服务
		go process(conn)
	}
}

client.go

go 复制代码
package main

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

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:8888")
	if err != nil {
		fmt.Println("err=", err)
	}
	fmt.Println("连接成功conn=", conn)

	//创建标准stdin的reader
	reader := bufio.NewReader(os.Stdin)
	//读取一行
	str, err := reader.ReadString('\n')
	if err != nil {
		fmt.Println("err=", err)
	}

	n, err := conn.Write([]byte(str))
	if err != nil {
		fmt.Println("err=", err)
	}
	fmt.Println("发送了n个字节n=", n)
}
  • 一个小点,发送的字节数多2,应该是回车键的缘故,可能这里是当成\n\r
相关推荐
花酒锄作田4 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
西岸行者4 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意4 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode