Golang又叫go语言,golang是它的全称,是由Google开发的一种静态强类型,编译型,并发型,并具有垃圾回收功能的编程语言
go语言确保达到静态编译语言的安全和性能的同时,又达到动态语言的开发维护效率
Go语言天生支持并发,提供自动垃圾回收机制
go的源文件是xxx.go
值得一提的是哔哩哔哩网站后端就是用golang开发,这个足以说明golang的并发功能有多强大了
检查是否安装成功go
go version
环境配置
GOROOT对应着go的安装目录
GOPATH对应着go的源代码目录(可以放多个目录)
GOBIN对应着 go install安装和编译的二进制程序的安装目录
检查go环境
go env
源程序默认为UTF-8编码,;可省略
第一个go程序
package main
import "fmt"
func main(){
fmt.Println("hallo golang")
}
go run hallo.go
当然作为一个编译型语言,编译成二进制文件是支持的
go build hallo.go
作为一个静态强类型语言,如果学过java的话,理解还是很轻松的
定义包 package : 必须在源程序上声明该文件是属性那个包的
引入包 import : 导入包,引用外部包开扩展功能
注释
// 单行
/* 多 行 */
标识符命名规范:第一个字母必须是字母或者下划线,而且不能是关键字,特殊符号只支持下划线
常用的数据类型有:
整型:int(有符号),uint(无符号),rune,int/unint(8,16,32,64)
浮点型:float(32,64),comple
布尔型:bool(true,false)(bool默认值为false)
字符串型:string
数组:array
结构体:struct
变量
var abc string = “hallo”
注意:如果声明变量了,但是没有初始化,那个该变量的值为系统默认设置的值(零值)
定义多个变量
var abc xyz string = “hallo”,“word”
而且go会根据提供值来判断数据类型是什么,例如:
var xyz = 666
go还提供一个特殊的运算符 :=,可以在变量不被声明的情况下使用,例如:
hallo,word := “hallo”, “word”
:=在实质开发中会经常使用的
go类型强制转换(和java一样,高精度转为低精度会失真)
var abc int = 666
var xyz float = 3.14
var gg string = “123”
float(abc) // 强制转换为浮点数
int(xyz) // 强制转为整型,会失去小数点之后的数值
strconv.Itoa(abc) // 强制转为字符串
strconv.Atoi(gg) // 强制转为整型
注意:同一作用域中不能重复声明,而且必须要声明变量才能使用,而且必须要使用
声明多个变量
var abc,xyz,hallo int
var(
abc int
xyz float
)
常量
和其他语言一样,常量表示的就是不可修改的变量
const abc, xyz int = 123, 666
go还提供了一种特殊的定义方式,例如:
const(
a = "hi"
b = "hallo"
)
如果声明多个常量时,省略了值那么就是表示和第一行值相同,例如:
const(
a = 123
b
c
)
注意:定义的时候必须赋值
iota常量计数器
const(
a = iota
b
_
d
)
iota在出现const关键字时重置为0,const每增加一行常量声明,iota加1(自增长),_(下划线,空白标识符(占位),常用于忽略函数多个返回值,例如err)为跳过某些值
类型
golang的数据类型分为基本数据类型和复合数据类型
基本数据类型有整型,浮点型,字符串,布尔型
复合数据类型有数组,函数,切片,结构体,字典(map),通道(channel),接口
注意:像整型有很多种,像int8,int16,int32,uint8,uint16等等,如果直接写int的话,在不同的操作系统中是不一样的,32为操作系统的int指的是int32,64位操作系统指int64
可使用unsafe.Sizeof查看变量的长度(在内存中的存储空间),例如:
var num100 = 100
fmt.Println(unsafe.Sizeof(num100))
值类型:值类型被声明变量后,不管有没有赋值,都会分配内存给它,也就是说值类型的声明不需要分配内存,因为在声明的时候已经分配好了内存
引用类型:变量存储的是一个地址,地址对应的内存空间就是真正存储数据的,引用类型必须申请内存才能使用,例如make()
自定义类型:可以使用type关键字来自定义类型,例如:type Testint int // 将Testint定义为int类型
类型转换
注意:高位转低位会出现精度丢失,例如
var abc int16 = 666
fmt.Println(int8(abc)) // -102
数字字面量语法
go1.13版本+,引入了数字字面量语法,方便使用二进制,八进制,十六进制的格式定义数字,例如:
abc := 0b10100010101000001011
fmt.Println(abc) // 666123
0b表示二进制
0o表示八进制
0x表示十六进制
进制的转换
var abc int64 = 100
fmt.Println(abc)
a := strconv.FormatInt(abc, 2) // 二进制
fmt.Println(a)
b := strconv.FormatInt(abc, 8) // 八进制
fmt.Println(b)
c := strconv.FormatInt(abc, 10) // 十进制(默认)
fmt.Println(c)
d := strconv.FormatInt(abc, 16) // 十六进制
fmt.Println(d)
浮点型(也就是小数)
Go的浮点型分为float32和float64
float32的浮点数的最大范围为3.4028234663852886e+38,可用math.MaxFloat32输出查看
float64的浮点数的最大范围为1.7976931348623157e+308,可用math.MaxFloat64输出查看
var pi = math.Pi
fmt.Printf("%f\n", pi) // 默认小数点6位
fmt.Printf("%.10f\n", pi) // 指定输出小数点后几位
字符串
go语言字符串编码为UTF-8
例如: hallo := “你好 Word”
可输出多行字符串
var str123 = `第一行
第二行
第三行`
fmt.Println(str123)
len():字符串长度
+或fmt.Sprintf:拼接字符串
strings.Index():返回字符在字符串中的位置
strings.contains:返回是否包含某个字符
布尔类型
var abc = false var xyz = true
byte和rune类型
组成字符串的元素叫字符,可遍历字符串获取字符
go语言的字符分为2种类型,分别为uint8(byte,表示ACII码的一个字符)和rune类型(表示一个UTF-8字符)
因此使用非ACII码的字符,需要使用rune类型(一个汉字占3个字节,字母只占一个字节)
注意:修改字符串必须将其转换为byte或者rune类型,完成修改后再转回string,例如:
hallo := "你好,golang"
abc := []rune(hallo)
abc[0] = '您'
fmt.Println(string(abc))
整型转字符串类型
var a int = 100
b := fmt.Sprintf("%d", a)
fmt.Printf(b)
字符串转整型或者浮点型
str := "666"
str1 := "3.14"
strScore, err := strconv.Atoi(str)
fmt.Println(strScore, err) // err为转换失败的信息
num, err := strconv.ParseInt(str, 10, 64)
fmt.Println(num, err)
num1, err := strconv.ParseFloat(str1, 10)
fmt.Println(num1, err)
运算符
+:加,-减,*乘,/除,%求余
注意:在go中,++和–是单独使用的,是没有++i的,正确写法例如:
var i int = 64
i++
// ++i 错误
if判断
package main
import "fmt"
func main(){
abc := 123
if abc >666{
fmt.Println("abc大于666")
} else if abc < 666{
fmt.Println("abc小于666")
}else{
fmt.Println(abc)
}
}
if的另一种写法:
if abc:=123; xyz>=100{
fmt.Println("abc>=100")
}
switch判断(用于对大量的值进行判断)
xyz := "abc"
switch xyz {
case "abc":
fmt.Println("1")
case "123":
fmt.Println("2")
default:
fmt.Println(xyz)
}
switch判断的另一种使用方法
switch abc := "hallo"; abc {
case "hallo":{
fmt.Println("hallo")
break
// 不用break也能跳出switch语句
}
case "hi","hello":{
// 多个值用逗号分隔
fmt.Println("hi")
fmt.Println("hello")
break
}
default:{
fmt.Println("hallo word")
break
}
}
如果想继续执行下一个case,可以使用fallthrough语句(switch最后一个分支不要使用fallthrough语句,否则报错,而且不能在case语句中间使用,必须是在case语句最后一个语句中使用)例如:
func hallo() int {
abc := 100 + 666 + 123
return abc
}
func main() {
switch abc := hallo(); {
case abc > 500:
fmt.Printf("num < 500\n")
fallthrough
case abc > 666:
fmt.Printf("num < 666\n")
fallthrough
case abc > 123:
fmt.Printf("num < 123\n")
}
}
for循环
for abc := 1; abc<10 ; abc++{
fmt.Println(abc)
}
golang是没有while循环语句,但是可以使用for循环来做出类似的功能
abc := 1
for abc<10 {
fmt.Println(abc)
abc++
}
for循环可用break,goto,return,panic语句退出循环
break跳出循环
一次性跳出多层循环 break LOOP
continue是退出当前循环
goto是无条件转移到goto语句的行,可用于跳出循环,条件转移,例如:
func main() {
var abc int = 123
LOOP:
for abc < 666 {
if abc == 233 {
abc = abc + 1
goto LOOP
}
fmt.Printf(abc)
abc++
}
}
return可用于函数或者方法中,用于跳出当前函数或者方法,如果return语句在main函数中,将是终止程序运行,在普通函数中,将是终止当前函数执行(return后面的程序不执行了)
当return没有带返回值,将是终止,如果带返回值,那么就是终止并且返回值
go语言并没有像java那样的异常嵌套机制,go语言用panic-and-recover来替代
panic可中断原来的流程控制,某个函数调用了panic,将终止该函数的执行,如果有延迟函数(defer)则执行该defer函数,返回其调用者,一直到goroutine(goroutine是go语言的轻量级线程,由runtime管理,go语言会智能的将goroutine中的任务合理分配给每个CPU,go程序在运行时,会给main()函数建立一个默认goroutine,用go关键字创建goroutine,例如:go 函数名(参数))的全部函数都返回,然后打印panic信息,堆栈信息,最后到终止程序
panic:在任何地方都可以触发 recover:只在用了defer修饰的函数内有效
注意:平时不要用panic-and-recover,而是errors,只有当程序不能继续执行了才用,例如出现不可恢复的错误,不能再让它继续执行了,这里举个例子:
func abc() {
fmt.Println("hallo word")
}
func hallo() {
defer func() {
err := recover()
if err != nil {
fmt.Println("hallo word")
}
}()
panic("runtime error!!!")
}
func main() {
abc()
hallo()
}
errors(在go语言中,发生错误,是通过返回errprs值,来对errprs值进行修改或者忽略,当没有错误时,rrprs值为nil,因此可以通过判断errors的值是否为nil来知道是否出现错误,以及处理错误)
for range循环(可用于遍历数组,字符串,字典(map),切片(数组),channel(通道))
for range循环数组,字符串,切片返回索引和值,字典(map)返回键和值,channel(通道)返回通道内的值
例如:
var abc = "hallo word"
for key, index := range abc {
fmt.Printf("%v:%c,", key, index)
}
数组(在go中,数组是指同一系列,同一类型的数据集合,组成数组的数据叫元素,go语言的数组的元素是被分配到连续的内存地址中,索引元素是速度非常快的)
var abc [16]int // 定义int类型,元素个数为16的数组
abc[0] = 10
abc[1] = 66
fmt.Println(abc)
var xyz = [3]string {"hallo word","golang","hahaha"}
fmt.Println(xyz)
var a = [...]int{1, 2, 3, 4, 5} // 自动判断数组长度
fmt.Println(a)
b := [...]int{1: 66, 3: 100}
fmt.Println(b)
遍历数组
hallo := [...]int{1: 66, 3: 100}
for i := 0; i < len(hallo); i++ {
fmt.Print(hallo[i], "\n")
}
也可以用for range方法
数组是值类型,将数组赋值给另一个变量,会生成副本,修改另一个变量,只会修改副本,不会修改原来的数组,例如:
hallo := [...]int{1: 66, 3: 100}
abc := hallo
abc[1] = 100
fmt.Println(hallo, abc)
可以看到在golang中数组不是引用类型
二维数组
var abc = [...][2]int{{100,123},{222,333},{666,60}}
fmt.Println(abc)
for i := 0; i < len(abc); i++ {
for j := 0; j < len(abc[0]); j++ {
fmt.Println(abc[i][j])
}
}
数组的初始值为nil(nil表示为空)
知识点:指针和引用类型的默认值为nil,需要分配空间
切片
切片和数组类似(切片是基于数组类型进行一层封装),不过切片是引用类型(调用的只是在内存中的地址,指针)的
var hallo = []int{100, 2333, 666}
abc := hallo
abc[0] = 123
xyz = abc[1:2]
fmt.Println(hallo, abc, xyz)
for i := 0; i < len(abc); i++ {
fmt.Print(abc[i], "\n")
}
长度用len()获取,容量用cap()获取
make函数创建切片
var data = make([]int, 2, 8) // int类型,长度为2,容量为8
fmt.Printf("长度:%d, 容量%d", len(data), cap(data))
append()切片扩容
var hallo = []int64{100, 2333, 666}
hallo = append(hallo, 123)
fmt.Println(hallo) // 100, 2333, 666, 123
abc := []int64{1,3,4,5,6}
hallo = append(hallo, abc...) // 将两个切片合并
fmt.Println(hallo) // 100,2333,666,123,1,3,4,5,6
copy()函数复制切片(可以理解为深拷贝)
var hallo = []int64{100, 2333, 666}
var abc = make([]int64, len(hallo), len(hallo))
copy(hallo, abc)
abc[0] = 4
fmt.Println(hallo,abc)
删除切片的值(golang中并没有删除切片的值的方法,不过可以用append()实现)
var hallo = []int64{100, 2333, 666}
hallo = append(hallo[:2], hallo[3:]...)
fmt.Println(hallo)
遍历切片和数组一样
map(字典)
map(字典)是无序的基于key-value的数据结构,在golang中map是引用类型
var abc = make(map[string]int) // string是键的类型,int是键对应的值的类型
abc["user"] = 123
abc["pass"] = 666
fmt.Println(abc)
fmt.Println(abc["pass"])
或者
var hallo = map[string]string {
"user":"root",
"pass":"abchahaha",
}
fmt.Println(hallo)
遍历map
var hallo = map[string]string {
"user":"root",
"pass":"abchahaha",
}
for key, value := range hallo {
fmt.Println("key:", key, " value:", value)
}
判断map中某个键值是否存在
var hallo = map[string]string {
"user":"root",
"pass":"abchahaha",
}
value, yes := hallo["pass"] // 返回2个值。value为返回结果,yes为是否存在该键值
fmt.Println(value, yes)
delete()删除键值对
var hallo = map[string]string {
"user":"root",
"pass":"abchahaha",
}
delete(hallo, "pass")
value, no := hallo["pass"]
fmt.Println(value, no)
通道
Golang中还有一个特殊的类型chan,这个类型一般用来线程之间的数据传输
声明chan
var a chan int a := make(chan int, 1) a <- 999 b := <- a
chan底层是指针,指针初始值为空,需要实例化,make()就是实例化了chan
<- 999:将值放进通道
<- a : 将 999 从通道中提取出来
函数
函数通过func关键字来声明和定义函数
func hallo(a ...string){
fmt.Println(a)
}
func main(){
hallo("hallo golang!!!")
}
匿名函数
func main() {
func () {
fmt.Println("我是匿名函数")
}()
}
因为没有函数名,没法像正常函数那样被调用,需要将其赋值或者作为立即执行函数
返回值
func hallo(a, b int) (int,int,int) {
x := a+b
y := a-b
z := a*b
return x, y, z
}
func main(){
abc, xyz, go := hallo(6,3)
fmt.Println(abc, xyz, go)
}
当然也是可以自动返回
func hallo(a, b int) (x int,y int,z int){}
golang的全局变量和局部变量
全局变量:常驻内存中,污染全局
局部变量:不常驻内存中,不污染全局
闭包:让变量可以常驻内存的同时,不污染全局
闭包的写法:
func hallo() func() int {
i := 123
return func() int {
return i + 666
}
}
func main() {
var abc = hallo()
fmt.Println(abc())
}
或者
func hallo() func(a int) int {
var i = 10
return func(a int) int {
i = i + a
return i
}
}
func main() {
var abc = hallo()
fmt.Println(abc(666))
}
defer修饰语句可以延迟执行语句,例如:
fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
如果使用多个defer修饰语句,会逆序执行(先使用defer最后执行,最后使用defer,最早执行(相对于使用defer修饰的语句))
在golang中的time包提供了时间显示和测量时间的函数
获取时间(年-月-日)例如:
timedata := time.Now()
year := timedata.Year()
month := timedata.Month()
day := timedata.Day()
fmt.Printf("%d-%02d-%02d \n", year, month, day)
格式化日期(在golang中,并且不是使用Y-m-d H:M:S模板格式化,而且是使用Go的诞生时间:2006年1月2日 15点04分)
例如:
timedata := time.Now()
fmt.Println(timedata.Format("2006-01-02 15:04:05")) // 24小时制
fmt.Println(timedata.Format("2006-01-02 03:04:05")) // 12小时制
获取时间戳(时间戳是自1070年1月1日08:00:00GMT至今的总毫秒数,又叫Unix时间戳)
例如:
timedata := time.Now()
unixTime := timedata.Unix() // 毫秒时间戳
unixNaTime := timedata.UnixNano() // 纳秒时间戳
fmt.Println(unixTime,unixNaTime)
时间戳转正常日期
var abc = time.Unix(1642746020, 0)
var xyz = abc.Format("2006-01-02 15:04:05")
fmt.Println(xyz)
正常日期转时间戳
var abc = "2022-01-21 14:21:55"
var tmp = "2006-01-02 15:04:05"
timedata, err := time.ParseInLocation(tmp, abc, time.Local)
fmt.Println(timedata.Unix())
时间的间隔(两个时间之间的间隔,单位为纳秒,time.Duration是time定义的类型,表示一段时间间隔,最大可以表示290年)
data := time.Now()
abc := data.Add(time.Hour)
xyz := data.Add(time.Second)
fmt.Println(abc) // 输出1个小时后的时间
fmt.Println(xyz) // 输出1秒后的时间
指针
golang的指针有3个概念,地址,类型,取值
&:获取地址,*:根据地址取值
a := 100
b := &a
fmt.Println(b) // 获取地址
fmt.Println(*b) // 根据地址取值
*b = 666
fmt.Println(a) // 根据地址修改值,改变内存中的值,会改变原来的变量值
注意:指针必须在创建内存后才能使用(其他引用类型也是一样,需要用make分配空间或者在定义的时候分配空间,值类型在声明的时候已经分配了默认空间,因此值类型不用分配空间)
new关键字分配内存(new是一个内置函数,调用new函数得到的是指定类型的指针,并且指针对应的值为该类型的零值)
abc := new(int)
fmt.Printf(abc)
fmt.Println(*abc)
make和new的区别:虽然这两个都是用来内存分配的,不过make是用于map,channel,切片(slice)的初始化,返回的值为这3个类型的本身,而new是用来类型的内存分配,内存对应的值为类型的零值,返回的值为指向类型的指针
Golang语言是面向对象语言又不是,虽然有类型和方法,也支持面向对象的编程风格,但是go没有对象(object)这个类型,也没有类(class)的概念,在go中用结构体替代面向对象语言的类(class)
type关键字(go通过type关键字定义结构体,结构体是值类型)
自定义类型(type也可以来定义自定义类型)
type dataStr string
dataStr自定义类型具备string类型的特性
type dataStr string
func main() {
var abc dataStr = "hallo word"
fmt.Printf("%v %T", abc, abc)
}
类型别名(只是别名,实质上还是同一个类型)
type dataStr = string
结构体的定义以及初始化(type关键字和struct关键字)
type Data struct{
user string // 定义结构体
age int
pass string
}
func main(){
var data Data // 实例化
data.user = "root"
data.age = 20
data.pass = "123456789"
fmt.Printf("%#v", data)
}
也可以使用new关键字实例化
func main(){
var data = new(Data) // 实例化
data.user = "root"
// 等于 (*data).user = "root"
data.age = 20
data.pass = "123456789"
fmt.Printf("%#v", data) // 结构体指针
}
在golang中支持直接对结构体指针使用,来访问结构体的属性
另外几种实例化结构体的方法
var data = &Data{
data.user = "root",
data.age = 20,
data.pass = "123456789",
}
fmt.Printf("%#v", data)
var data = Data{
data.user = "root",
data.age = 20,
data.pass = "123456789",
}
fmt.Printf("%#v", data)
var data = Data{
"root",
20,
"123456789",
}
fmt.Printf("%#v", data)
结构体的方法和接收
type Data struct{
// 定义结构体
user string
age int
pass string
}
func (d Data) DataMain() {
// 定义方法
fmt.Print("user:", d.user)
fmt.Print("age:", d.age)
fmt.Print("pass:", d.pass)
fmt.Println()
}
func (d *Data) GetData(user string, age int, pass string) {
// 接收方法,因为结构体是值类型,需要使用指针
d.user = user
d.age = age
d.pass = pass
}
func main() {
var data = Data{
"root",
20,
"123456789",
}
data.DataMain()
data.GetData("admin", 22, "abc12345")
data.DataMain()
}
输出结果为:
user:rootage:20pass:123456789 user:adminage:22pass:abc12345
自定义类型方法(类型一样可以定义方法)
type dataStr string
func (d dataStr) dataInfo(){
fmt.Println("hallo golang") // 自定义类型的自定义方法
}
func main() {
var abc dataStr = "hallo word"
abc.dataInfo()
}
结构体匿名字段(go允许字段在声明的时候没有字段名,只有类型,因为结构体要求字段名唯一,因此在同一个结构体中同种类型的匿名字段只能出现一次)
type Data struct{
// 定义结构体
string
int
string
}
func main(){
var data = Data{
// 结构体匿名字段
"root",
20,
"123456789",
}
}
注意:结构体的字段类型可以是任意类型(包括自定义类型,结构体类型),但是如果类型是引用类型(例如map,指针)需要先使用make分配空间再使用
结构体嵌套
type Data struct{
// 定义结构体
user string
age int
pass string
datamain DataMain //嵌套DataMain结构体
}
type DataMain struct{
// 定义结构体
email string
phone string
}
func main() {
var d Data
d.user = "xiaochen"
d.age = 20
d.pass = "123456789"
var datamain DataMain
datamain.email = "a@xiaochenabc123.test.com"
datamain.phone = "18888888888"
d.datamain = datamain
fmt.Printf("%#v", d)
}
输出结果为main.Data{user:“xiaochen”, age:20, pass:“123456789”, datamain:main.DataMain{email:“a@xiaochenabc123.test.com”, phone:“18888888888”}}
嵌套结构体可能出现字段名相同,go默认先从父结构体查找,如果没有再到子结构体中查找,这时如果子结构体存在相同的字段,会报错,因为不知道该设置哪个字段(所以字段名要全局唯一)
结构体继承(可以理解为类的继承,实质效果和结构体嵌套类似)
type Data struct{
// 定义结构体
user string
age int
pass string
DataMain //通过结构体嵌套实现继承
}
func (data Data) datamax() {
fmt.Printf("email: %v \n", data.email)
}
type DataMain struct{
// 定义结构体
email string
phone string
}
func (datamain DataMain) dataabc() {
fmt.Printf("email: %v \n", datamain.email)
}
func main() {
var data = Data{
user: "root",
DataMain: DataMain{
email: "a@xiaochenabc123.test.com",
},
}
data.datamax();
data.dataabc();
}
可以看到Data结构体拥有DataMain结构体的方法
注意结构体的字段首字母要大写,表示公有,小写为私用
go结构体和json序列化
将结构体转换为json叫json序列化,将json转换为结构体叫json反序列化
json序列化和json反序列化主要依赖于encoding/json包的json.Marshal()方法和json.Unmarshal()方法
type Data struct{
// 定义结构体
User string
Age int
Pass string
}
func main() {
var data = Data{
User: "root",
Age: 20,
Pass: "123456789",
}
// 结构体转换成Json(返回值为是byte类型的切片)
jsonByte, _ := json.Marshal(data)
// byte类型转string类型
jsonStr := string(jsonByte)
fmt.Printf(string(jsonByte))
fmt.Printf(jsonStr)
}
json字符串转结构体
type Data struct{
// 定义结构体
User string
Age string
Pass string
}
func main() {
// Json字符串转换成结构体
var str = `{"User":"root","Age":"20","Pass":"123456789"}`
var data = Data{}
// 第一个参数是传入byte类型的json字符串,第二个参数需要传入转换的地址
err := json.Unmarshal([]byte(str), &data)
if err != nil {
fmt.Printf("转换失败 \n")
} else {
fmt.Printf("%#v \n", data)
}
}
结构体标签(tag)
tag是结构体的元信息,可以在运行时通过反射的机制读取出来,tag在结构体字段后面定义,用反引号包裹,tag是以键值对的方式组成,不同的tag用空格分隔
type Data struct{
// 定义结构体,并且使用结构体标签
User string `json:"user"`
Age string `json:"age"`
Pass string `json:"pass"`
}
func main() {
var data = Data{
User: "root",
Age: "20",
Pass: "123456789",
}
jsonByte, _ := json.Marshal(data)
// byte类型转string类型
jsonStr := string(jsonByte)
fmt.Printf(jsonStr)
var str = `{"User":"admin","Age":"22","Pass":"abc12345"}`
var datajson = Data{}
err := json.Unmarshal([]byte(str), &datajson)
if err != nil {
fmt.Printf("转换失败 \n")
} else {
fmt.Printf("%#v \n", datajson)
}
}
包(package)是多个源码的集合,是一种代码复用方案,像fmt,time,encoding/json都是go的内置包
go中的包分为3种,内置包(go提供的内置包,可以直接引入使用),自定义包(自己写的包),第三方包(也是自定义包,不过不是自己写的,需要下载到本地才能使用)
包管理器(go mod)(在1.11版本之前需要使用自定义包的话,需要将项目放在GOPATH环境变量中,1.13之后将彻底不需要GOPATH)
初始化项目(生成go.mod来管理项目的依赖(包括go版本和要使用到的包))
go mod init go_test
如果要引入go_test项目的包,需要import “go_test/包名”,包名根据package设置
package 包名
注意:包名不能和文件夹的名字相同,包名不能出现-符号,一个文件夹中直接包含的文件只能归1个package,同一个package的文件不能在多个文件夹中,而且只有引入了包名为main的程序,编译后会得到可执行文件,如果没有包含main包的程序编译不会得到可执行文件
init()初始化函数:导入包,自动触发包内部的init()函数的调用
go会先从main包开始检查其导入的所有包,每个包又可能导入了其他包,因此形成了一个树状包引入关系,根据引入的顺序来决定编译的顺序
最后导入的包最先初始化并且调用其init()函数
golang第三包仓库https://pkg.go.dev/
go install 编译并安装包(当存在GOBIN环境变量时,编译完成的二进制文件放到$GOBIN下,如果不存在默认放到GOPATH/bin下,源码默认在$HOME/sdk下)
go install github.com/tal-tech/go-zero@1.4.1
go get 全局安装包(Go 1.17版本中已被弃用,推荐使用go install )
go get github.com/tal-tech/go-zero
go mod download 全局安装包
依赖自动下载到$GOPATH/pkg/mod目录,多个项目可共享缓存的mod,使用该命令之前需要在项目引入第三方包
go mod vendor 将依赖复制到当前项目的vendor中,需要在项目引入第三方包
go mod vendor
其他命令
go mod edit 编辑go.mod文件
go mod tidy 自动处理go.mod中多引入和少引入的包(没有使用的module移除,缺少引入的module将自动引入构建,确保go.mod与模块中的源代码一致)
go mod graph 打印模块依赖图
go mod verify 校验依赖,检查下载的第三方库是否本地修改,如果没有修改则返回0(校验成功),否则返回非0(校验失败)
go mod why 解释为啥需要包
使用Modules
GO111MODULE:1.12版本之前的,要设置环境变量GO111MODULE,之后就不需要了通过设置GO111MODULE来开启或者关闭go module
GO111MODULE = off 禁用go module,编译时在GOPATH和vendor中查找包 GO111MODULE = on 启用go module,编译时忽略GOPATH和vendor,只根据go.mod下载依赖 GO111MODULE=auto 默认值,当项目在GOPATH/src之外,并且项目的根目录有go.mod文件时启用go module
windows设置GO111MODULE
set GO111MODULE=on|off|auto
MacOS或者Linux设置GO111MODULE
export GO111MODULE=on|off|auto
off和auto,下载的包安装在GOPATH/src目录下
no,下载的包安装在GOPATH/pkg/mod/下,也在这个目录下查找包(不在GOPATH/src查找)
也可以手动修改环境变量,GO111MODULE变量,值为on|off|auto
GOPROXY:GO代理服务器,是Go官方提供的中间代理的方式来包下载,需要设置GOPROXY环境变量
常见的代理服务器地址:
goproxy.io; goproxy.cn:由国内的七牛云提供
一键设置GOPROXY:
windows:go env -w GOPROXY=https://goproxy.cn,direct
Linux或者macOS:export GOPROXY=https://goproxy.cn
注意:go语言在1.13版本后,GOPROXY默认为https://proxy.golang.org,如果下载缓慢或者无法访问请设置为https://goproxy.cn
也可以手动修改环境变量,GOPROXY变量,值为https://goproxy.cn
依赖的安装(注意:需要移除把项目从GOPATH移除(GOPATH下不允许有go.mod),否则报错$GOPATH/go.mod exists but should not)
go get下载指定版本的依赖包
go get -u 升级项目中的包到最新的次要版本或者修订版本 go get -u=patch 升级项目中的包到最新的修订版本 go get 包名@版本号 下载对应包的指定版本或者将对应包升级到指定的版本,版本号可以是v1.x.x之类的,也是可以是git的分支,tag,git提交的哈希值
手动修改go.mod,执行go mod download或者使用go get
go的接口(interface)是一种抽象数据类型,接口定义了对象的行为规范,只负责定义规范,并不实现,接口的规范实现由具体的对象来实现
接口是一组函数method的集合,接口不能存在任何变量,接口中的全部方法都没有方法体
type TestData interface{
// 定义一个TestData接口
test()
data()
}
type Data struct {
// 实现接口
User string
}
func (d Data) test() {
fmt.Println(d.User, "Test")
}
func (d Data) data() {
fmt.Println(d.User, "Data")
}
func main() {
var datamain TestData = Data{
"root",
}
datamain.test()
datamain.data()
}
空接口(接口允许不定义任何方法,不定义任何方法的接口就是空接口)
type Maxdata interface {
// 定义一个空接口,空接口表示没有约束,任何类型都能实现空接口
}
func main() {
var abc Maxdata
var str = "hallo word"
abc = str
fmt.Println(abc)
}
Go1.18版本已添加any关键字来表示泛型
func main(){
var abc any
abc = 'hallo any'
fmt.Println(abc)
}
实质上any还是空接口interface{}的类型别名,type any = interface{}
空接口也可用来当做类型,表示任意类型(类似于Java中的Object类型)
空接口还可以用来当做函数的参数,表示可以接收任意类型的函数参数
func Data(abc interface{}) {
fmt.println(abc)
}
使用空接口来实现可以保存任意类型的map
var dataInfo = make(map[string]interface{})
dataInfo["uesr"] = "root"
dataInfo["age"] = 20
dataInfo["pass"] = "123456789"
使用空接口来实现一个空接口类型的切片
var dataslice = make([]interface{}, 6, 6)
dataslice[0] = "root"
dataslice[1] = 20
dataslice[3] = "123456789"
类型断言
接口的值是由具体类型和具体类型的值组成,称为接口的动态类型和动态值
判断空接口的值的类型,需要使用类型断言,语法格式为:类型为interface{}的变量.(断言这个变量可能是的类型)
例如:
var abc interface{}
abc = 123
value, isInt := abc.(int)
if isInt {
fmt.Println("int类型, 值为:", value)
} else {
fmt.Println("不是int类型,断言失败")
}
Go并发
还是讲一下进程和线程的区别
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源的分配和调度的基本单位,每个进程都拥有一个自己的地址空间,进程至少有5种基本状态,分别是:初始状态,执行状态,等待状态,就绪状态,终止状态
线程(Thread)是进程的执行实例,是程序执行的最小单位,是操作系统能够进行运算调度的最小单位,一个进程可以创建多个线程,同一个进程的线程共享进程的内存信息,同一个进程的多个可以并发执行,一个线程要执行,必须至少有一个进程
并发和并行的区别
并发:指一个时间段中有多个线程(程序)被快速的轮换执行(处于启动运行到运行完毕之间),在一个时间段中只有一个线程(程序)在执行,在宏观上感觉是多个线程同时被处理,如果多线程操作在一个cpu上,操作系统将cpu运行时间分隔为若干个时间段,将时间段分配给各个线程执行,在一个时间段的线程执行时,其他线程将处于挂起状态,这就叫并发
并行:当系统拥有多个cpu(1个以上)时,一个cpu执行一个线程,另一个线程执行另一个线程,同时进行处理,这就叫并行
并发和并行的区别:并发一个时间段只能执行一个线程(多个线程需要排队执行),并行可以在一个时间段中同时执行多个线程
当多线程程序在单核CPU上执行时,就是并发,在多核CPU执行时,就是并行,当线程数大于CPU核数,那么既有并行又有并发
go的主线程和协程(goroutine):主线程可调起多个协程,而多协程就是可以实现并行或者并发了
每个goroutine (协程) 默认占用内存远比 Java 、C的线程少(goroutine是2kb,加上协程调度内存开销也比线程少,因此goroutine是轻量级线程)
通过go关键字开启goroutine即可实现协程功能,goroutine的调度由golang运行时进行管理,例如
var wg sync.WaitGroup // 协程计数器
func data1() {
for i := 0; i < 5; i++ {
fmt.Println("hallo word")
time.Sleep(time.Millisecond * 100)
}
wg.Done() // 程序结束则协程计数器减1
}
func data2() {
for a := 0; a < 5; a++ {
fmt.Println("hallo golang")
time.Sleep(time.Millisecond * 100)
}
wg.Done() // 程序结束则协程计数器减1
}
func main() {
wg.Add(1) // 协程计数器加1
go data1()
wg.Add(1) // 协程计数器加1
go data2()
for i := 0; i < 5; i++ {
fmt.Println("hallo hahaha")
time.Sleep(time.Millisecond * 100)
}
wg.Wait() // 等待所有的协程执行完毕
fmt.Println("主线程结束")
}
通过time.Sleep设置100毫秒定时,可以看到并没有顺序输出,因为这里使用了多协程
go运行时的调度器通过GOMAXPROCS参数来确定使用多少个OS线程来执行,默认值为计算机的cpu核心数,例如32核的计数器,调度器将程序同时调度到32个OS线程上,通过runtime.GOMAXPROCS()设置当前程序发时的CPU逻辑核心数,runtime.NumCPU()获取计算机的CPU核心数
注意:go1.5版本之前,默认使用单核心,1.5版本之后默认使用全部核心数
Cpu := runtime.NumCPU() // 获取cpu个数
fmt.Println("cpu核心数:", Cpu)
runtime.GOMAXPROCS(runtime.NumCPU() - 1) // 设置要使用的CPU数量
看看多协程实质执行效果
var vg sync.WaitGroup
func data(num int) {
for i := 0; i <= 10; i++ {
fmt.Printf("协程%v输出的%v条数据 \n", num, i)
}
vg.Done()
}
func main() {
for i := 0; i <= 10; i++ {
go data(i)
vg.Add(1)
}
vg.Wait()
fmt.Println("主线程结束")
}
通道(channel)是传输数据的一种数据结构(类型),被用来多个goroutine之间传递信息通讯,可以让goroutine发送特定值给另一个goroutine
go语言的并发模型是CSP(Communicating Sequential Processes),goroutine是并发体,channel是通信
channel遵循先入先出(First In First Out)的规则,保证数据的顺序,channel类型是引用类型
声明管道(通过chan关键字声明)
var ch1 chan int // 声明传递整型的管道
var ch2 chan string // 声明传递字符串的管道
因为其是引用类型,需要使用make()声明内存空间才能使用
ch1 = make(chan int, 10) // 创建一个可以存储10个int类型的管道
ch2 = make(chan string, 10) // 创建一个可以存储10个string类型的管道
管道具备发送数据,接收数据和关闭管道功能,其中发送和接收都使用<-符号表示,例如:
ch2 <- "hall word" // 将hall word发送到ch2管道里
data := <- ch2 // 接收ch2管道的数据
close(ch2) // 通过内置的close函数关闭管道
管道也有容量,长度,用cap(), len()获取
管道阻塞:当管道没有数据,还进行接收,就会出现阻塞,同样当管道容量不足了,还进行发送数据,也会导致阻塞
注意:当管道被关闭,还继续给管道添加数据或者接收数据将导致panic: send on closed channel报错,如果goroutine执行完毕,管道不关闭,将抛出fatal error: all goroutines are asleep - deadlock!错误
另外使用for range循环在管道取值,在使用for range之前一定要关闭管道,使用for循环遍历管道就不需要关闭管道了
goroutine和channel协作
func data(ch1 chan int) {
for i := 0; i <= 5; i++ {
fmt.Println("写入:", i)
ch1 <- i
time.Sleep(time.Millisecond * 100)
}
wg.Done()
}
func dataGet(ch1 chan int) {
for a := 0; a <= 5; a++ {
fmt.Println("接收:", <-ch1)
time.Sleep(time.Millisecond * 100)
}
wg.Done()
}
func main() {
ch1 := make(chan int, 5)
wg.Add(1)
go data(ch1)
wg.Add(1)
go dataGet(ch1)
wg.Wait()
fmt.Println("主线程结束")
}
data函数写入数据到ch1,dataGet函数接收数据,并且可以看到管道写入数据后,会等待接收数据
单向管道(限制管道在函数中只能接收数据或者只能发送数据,管道默认可接收可发送)
声明只可发送的管道(不能接收)
var ch = make(chan<- int, 5)
ch <- 10
声明只可读的管道(不能发送)
var ch1 = make(<-chan int, 5)
<- ch1
使用select关键字实现多路复用,来从多个管道接收数据,因为管道的特性,没有数据可以接收会导致阻塞
select关键字类似于switch语句,有case分支和default分支,一个case负责一个管道的发送和接收,select会等待某个case通信操作完成,再执行case分支的语句
例如:
var ch1 = make(chan int, 10)
ch1 <- 1
ch1 <- 2
ch1 <- 10
ch1 <- 8
ch1 <- 0
ch1 <- 23
var ch2 = make(chan string, 10)
ch2 <- "hallo word"
ch2 <- "hallo golang"
for {
select {
case data:= <- ch1:
fmt.Println("读取ch1的数据:", data)
case data:= <- ch2:
fmt.Println("读取ch2的数据:", data)
default:
fmt.Println("所有的数据获取完毕")
return
}
}
可以看到管道发送数据和接收数据是按照顺序的,另外使用select获取数据时,不要关闭管道(当使用完再关闭)
并发安全和锁
在并发环境中,可能会出现并发访问的问题,需要使用互斥锁
互斥锁是并发时对共享资源进行控制访问的手段,使用sync标准库的Mutex结构体定义,sync.Mutex有两个指针方法,分别是Lock()和Unlock()
var mutex sync.Mutex // 定义锁
mutex.Lock() // 上锁
mutex.Unlock() // 解锁
当禁止访问公共资源时上锁,当需要访问公共资源时解锁
互斥锁实质上就是当一个goroutine访问时,其他goroutine不能访问,避免竞争,如果只读不写的话,也不会出现资源竞争的情况,因为写数据,需要保证数据同步,当写数据,又不能保证数据同步就是会出现资源竞争了
读取数据和读取数据之间不会资源竞争的特性衍生出另外一种锁,叫做读写锁
读写锁可以将多个读取并发,同时读取,并且对修改数据是完全互斥的,当一个goroutine修改数据(写)时,其他goroutine不能读取数据也是不能写数据
在go语言中,读写锁用sync.RWMutex定义
var mu sync.RWMutex // 定义读写锁
mu.RLock() // 上锁
mu.RUnlock() // 解锁
操作文件和目录
使用os.Open读取文件
file, err := os.Open("./data.txt") // 读取文件
defer file.Close() // 关闭文件流
if err != nil {
fmt.Println("打开文件出错")
}
var bytedata = make([]byte, 1024)
for {
n, err := file.Read(bytedata) // n为字节数, err是判断是否读取到末尾,值为nil没读取到末尾,值为io.EOF读取到末尾
if err == io.EOF {
fmt.Printf("读取完毕")
break
}
fmt.Printf("读取到了%v 个字节 \n", n)
var strdata []byte
strdata := append(strdata, bytedata...)
fmt.Println(string(strdata))
}
另一种读取方式(bufio,读取大文件推荐使用这个)
file, err := os.Open("./data.txt") // 读取文件
defer file.Close() // 关闭文件流
if err != nil {
fmt.Println("打开文件出错")
}
reader := bufio.NewReader(file)
var fileStr string
var count int = 0
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
fileStr += str
fmt.Println("读取结束", count)
break
}
if err != nil {
fmt.Println(err)
break
}
count ++
fileStr += str
}
fmt.Println(fileStr)
读取小文件(ioutil)
Strdata, err:= ioutil.ReadFile("./data.txt")
if err != nil {
fmt.Println("打开文件出错")
}
fmt.Println(string(Strdata))
写入文件(os.OpenFile)
file, err := os.OpenFile("./data.txt", os.O_RDWR | os.O_APPEND, 777) // 打开文件
defer file.Close() // 关闭文件流
if err != nil {
fmt.Println("打开文件出错")
}
str := "hallo word"
file.WriteString(str) // 写入文件
这里os.OpenFile接收3个参数,分别是要打开的文件的路径,打开文件的模式(多个模式用|隔开),文件的权限(和Linux文件权限一样,用八进制表示,读(04),写(02),执行(01),一般为777或者755)
os.O_WRONLY:只读 os.O_CREATE:创建 os.O_RDONLY:只读 os.O_RDWR:读写 os.O_TRUNC:清空 os.O_APPEND:追加
bufio写入
file, err := os.OpenFile("./main/test.txt", os.O_RDWR | os.O_APPEND, 777) // 打开文件
defer file.Close()
if err != nil {
fmt.Println("打开文件出错")
}
writer := bufio.NewWriter(file)
writer.WriteString("hallo word")
writer.Flush()
ioutil写入
str := "hallo word"
ioutil.WriteFile("./data.txt", []byte(str), 777)
复制内容到文件
Str, err := ioutil.ReadFile("./data.txt")
if err != nil {
fmt.Println("读取文件出错")
return
}
ioutil.WriteFile("./data1.txt", Str, 777)
创建目录
os.Mkdir("./test", 777)
删除文件和目录
os.Remove("data1.txt") // 删除文件
os.Remove("./test) // 删除目录
os.RemoveAll("./test1") // 和Remove一样,不过这个会递归删除所有子目录和文件
重命名
file := "./data.txt"
err1 := os.Rename(file,"test.txt")
if err1 != nil {
panic(err1)
} else {
fmt.Println("文件重命名成功")
}
folder := "./demo"
err2 := os.Rename(folder, "demo1")
if err2 != nil {
panic(err1)
} else {
fmt.Println("目录重命名成功")
}