前言
在一次偶然中,接触到了Go语言,这门语言是一个非常强大的编译型语言,从我接触起,便一直在记录,但愿这些能够帮助到想学习Go语言的人。
1.编译
使用go build
1.在项目目录下执行go build
2.在其他路径下执行go build
,需要在后面加上项目的路径(项目路径从GOPATH/src后开始写起,编译之后的可执行文件就保存在当前目录下)
3.go build -o ***.exe
生成编译文件时执行名字
go run
像执行脚本文件一样执行go代码
go install
go install
分为两步:
1.先编译得到一个可执行文件
2.将可执行文件拷贝到GOPATH/bin
交叉编译
Go支持跨平台编译
例如:在windows平添编译一个能在linux平台执行的可执行文件
SET CGO_ENABLED=0 //禁用CGO
SET GOOS=linux //目标平台是linux
SET GOARCH=amd64 //目标处理器架构是amd64
如果要编译可执行文件,必须要有main包和main函数(入口函数,无参数无返回值)
2.变量和常量
package main
//导入语句
import "fmt"
//函数外只能放标识符(变量,常量,函数,类型)的声明
//程序的入口函数
func main() {
fmt.Println("hello world")
}
标识符与关键字
在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名、常量名、函数名等等。 Go语言中标识符由字母数字和_
(下划线)组成,并且只能以字母和_
开头。 举几个例子:abc
, _
, _123
, a123
。
如果标识符的首字母是大写的,就表示对外部可见,可以通过包导入
关键字
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
nil
类似python的null
变量
Go语言中的变量必须先声明再使用
注意,函数外的每个语句都必须以关键字开始(var,const,func等)
声明的变量必须使用,不使用就无法编译
同一个作用域中,不能重复声明同一个变量
var s1 string
:声明了一个s1变量为字符串类型
批量声明:
var(
a string
b int
c bool
)
输入
fmt.Scan()
代表用户输入,输入需要传指针来修改内存地址上的数据
var s string
fmt.Scan(&s)
fmt.Println("用户输入的内容:", s)
fmt.Sanf
格式化输入
var s string
var d string
fmt.Scanf(&s &d)
fmt.Println("用户输入的内容:", s d)
输出
Printf(“name:%v”, “天听”)无论是什么类型,都能用%v来打印数值
func main() {
fmt.Print("AAA") //输出打印内容
fmt.Println("BBB") //打印完内容之后会加一个换行符
fmt.Printf("name:%s", "天听") //%s:占位符,等于格式化输出
}
变量类型
占位符%T可以查看类型
fmt.Printf("%T\n", 111)
fmt.Printf("%d\n", i1)
fmt.Printf("%b\n", i1) //转换为二进制
fmt.Printf("%o\n", i1) // 转换为八进制
fmt.Printf("%x\n", i1) //转换为十六进制
// %d 十进制
// %c 字符
// %s 字符串
// %p 指针
// %v 值
// %f 浮点数
// %t 布尔值
%%代表的才是%,不能用\转义
类型推导
有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化
var name = "Q1mi"
var age = 18 //根据值判断该变量是什么类型
短变量声明
在函数内部,可以使用更简略的 :=
方式声明并初始化变量。
package main
import (
"fmt"
)
// 全局变量m
var m = 100
func main() {
n := 10
m := 200 // 此处声明局部变量m
fmt.Println(m, n)
}
匿名变量
在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)
。 匿名变量用一个下划线_
表示,(_
多用于占位,表示忽略值。)例如:
func foo() (int, string) {
return 10, "Q1mi"
}
func main() {
x, _ := foo()
_, y := foo()
fmt.Println("x=", x)
fmt.Println("y=", y)
}
常量 const
声明常量的关键字const
const PI = 3.1415926
也可以批量声明常量,和批量声明变量一致,只不过关键字从var变为const
批量声明常量时,如果某一行声明后没有赋值,默认就和上一行一样
const(
n1 = 100
n2
n3
)
iota
iota
是go语言的常量计数器,只能在常量的表达式中使用。
iota
在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota
计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
使用_
跳过某些值
const (
n1 = iota //0
n2 //1
_
n4 //3
)
iota
声明中间插队
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0
定义数量级 (这里的<<
表示左移操作,1<<10
表示将1的二进制表示向左移10位,也就是由1
变成了10000000000
,也就是十进制的1024。同理2<<2
表示将2的二进制表示向左移2位,也就是由10
变成了1000
,也就是十进制的8。)
const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)
多个iota
定义在一行
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)
3.数据类型
整型
整型分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64
其中,uint8
就是我们熟知的byte
型
类型 | 描述 |
---|---|
uint8 | 无符号 8位整型 (0 到 255) |
uint16 | 无符号 16位整型 (0 到 65535) |
uint32 | 无符号 32位整型 (0 到 4294967295) |
uint64 | 无符号 64位整型 (0 到 18446744073709551615) |
int8 | 有符号 8位整型 (-128 到 127) |
int16 | 有符号 16位整型 (-32768 到 32767) |
int32 | 有符号 32位整型 (-2147483648 到 2147483647) |
int64 | 有符号 64位整型 (-9223372036854775808 到 9223372036854775807) |
特殊整型
类型 | 描述 |
---|---|
uint | 32位操作系统上就是uint32 ,64位操作系统上就是uint64 |
int | 32位操作系统上就是int32 ,64位操作系统上就是int64 |
uintptr | 无符号整型,用于存放一个指针 |
注意: 在使用int
和 uint
类型时,不能假定它是32位或64位的整型,而是考虑int
和uint
可能在不同平台上的差异。
注意事项 获取对象的长度的内建len()
函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int
来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用int
和 uint
。
浮点型
Go语言支持两种浮点型数:float32
和float64
打印浮点数时,可以使用fmt
包配合动词%f
,代码如下:
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi)
fmt.Printf("%.2f\n", math.Pi)
}
布尔
Go语言中以bool
类型进行声明布尔型数据,布尔型数据只有true(真)
和false(假)
两个值。
注意:
- 布尔类型变量的默认值为
false
。 - Go 语言中不允许将整型强制转换为布尔型.
- 布尔型无法参与数值运算,也无法与其他类型进行转换。
字符串
Go语言中字符串是用双引号包裹的
单引号包裹的是字符,字符是单独的字母、汉字、符号
字符串操作
转义:
\代表转义,如果单纯的想打印一个\,需要在前面再加一个\
转义符 | 含义 |
---|---|
\r |
回车符(返回行首) |
\n |
换行符(直接跳到下一行的同列位置) |
\t |
制表符 |
\' |
单引号 |
\" |
双引号 |
\\ |
反斜杠 |
多行字符串: `,使用此符号时,无需用\转义
s1 := `
aaa
bbb
ccc
`
方法 | 介绍 |
---|---|
len(str) | 求长度 |
+或fmt.Sprintf | 拼接字符串 |
strings.Split | 分割 |
strings.contains | 判断是否包含 |
strings.HasPrefix,strings.HasSuffix | 前缀/后缀判断 |
strings.Index(),strings.LastIndex() | 子串出现的位置(下标) |
strings.Join(a[]string, sep string) | join操作 |
byte和rune类型
组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:
var a := '中'
var b := 'x'
当需要处理中文、日文或者其他复合字符时,则需要用到rune
类型。rune
类型实际是一个int32
修改字符串:
要修改字符串,需要先将其转换成[]rune
或[]byte
,完成后再转换为string
。无论哪种转换,都会重新分配内存,并复制字节数组。
func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}
类型转换:
Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
强制类型转换的基本语法如下:
T(表达式)
其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等
数组
数组的长度是数组类型的一部分,例如var al [3]bool
,代表了al是一个长度为3元素类型为布尔的数组
数组的初始化
var a1 [3]bool
//初始化
a1 = [3]bool{true, true, false}
//根据初始值自动推断数组的长度是多少
a2 := [...]int{1, 2, 3, 4, 5, 6, 7}
fmt.Println(a2)
//根据索引初始化
a3 := [5]int{0:2,4:3}
fmt.Println(a3)
数组的遍历
citys := [...]string{"北京", "上海", "深圳"}
for i := 0; i < len(citys); i++ {
fmt.Println(citys[i])
}
//range遍历
for i, v := range citys {
fmt.Println(i, v)
}
多维数组
多维数组就是数组的嵌套,如[[1,2],[2,3],[3,4]]
var a4 [3][2]int
//[3]是外层数组,[2]int是该数组的数据类型,元素为两个int
a4 = [3][2]int{
[2]int{1, 2},
[2]int{3, 4},
[2]int{5, 6},
}
fmt.Println(a4)
多维数组的遍历
var a4 [3][2]int
a4 = [3][2]int{
[2]int{1, 2},
[2]int{3, 4},
[2]int{5, 6},
}
for _, v := range a4 {
for _, i := range v {
fmt.Println(i)
}
}
4.if与for
if判断
age := 16
if age >= 18 {
fmt.Println("成年")
} else {
fmt.Println("未成年")
}
也可以将变量定义到if的作用域中,这样,该变量只会在if作用域中
if age := 16; age >= 18 {
fmt.Println("成年")
} else {
fmt.Println("未成年")
}
else if
if age := 16; age >= 18 {
fmt.Println("成年")
} else if age >= 16 {
fmt.Println("小成年")
} else {
fmt.Println("未成年")
}
for循环
基本格式
for 初始语句;条件;结束语句{}
for i := 0; i < 10; i++ {
fmt.Println(i)
}
省略初始语句的写法 **;**不能少
var i=5
for ;i<10;i++{
fmt.Println(i)
}
省略结束语句的写法,如果不加条件,则是无限循环,由于go语言运行速度极快,不要轻易尝试
var i=5
for ;i<10;{
fmt.Println(i)
i++
}
for range循环,有两个变量接收,第一个是索引,第二个是值
s:= "hello天听"
for i,v := range s{
fmt.Printf("%d,%c\n",i,v)
}
break:跳出for循环
contiune:跳过本次循环,继续下一次
switch
简化大量的判断
var a = 3
switch a {
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
case 3:
fmt.Println("3")
case 4:
fmt.Println("4")
default:
fmt.Println("无效")
}
5.运算符
Go语言是强类型
,相同类型的变量才能比较
算数运算符
运算符 | 描述 |
---|---|
+ | 相加 |
- | 相减 |
* | 相乘 |
/ | 相除 |
% | 求余 |
关系运算符
运算符 | 描述 |
---|---|
== | 检查两个值是否相等,如果相等返回 True 否则返回 False。 |
!= | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 |
> | 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 |
>= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 |
< | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 |
<= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 |
逻辑运算符
运算符 | 描述 |
---|---|
|| | 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。 |
&& | 逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。 |
! | 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。 |
位运算符
位运算符对整数在内存中的二进制
位进行操作。
运算符 | 描述 |
---|---|
& | 参与运算的两数各对应的二进位相与。 (两位均为1才为1) |
| | 参与运算的两数各对应的二进位相或。 (两位有一个为1就为1) |
^ | 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (两位不一样则为1) |
<< | 左移n位就是乘以2的n次方。 “a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。 |
>> | 右移n位就是除以2的n次方。 “a>>b”是把a的各二进位全部右移b位。 |
赋值运算符
= | 简单的赋值运算符,将一个表达式的值赋给一个左值 |
---|---|
+= | 相加后再赋值 |
-= | 相减后再赋值 |
*= | 相乘后再赋值 |
/= | 相除后再赋值 |
%= | 求余后再赋值 |
<<= | 左移后赋值 |
>>= | 右移后赋值 |
&= | 按位与后赋值 |
|= | 按位或后赋值 |
^= | 按位异或后赋值 |
6.切片
len
代表长度
cap
代表容量
切片必须初始化分配内存
切片不存值,指向同一个底层数组,所以,怎样赋值切片之后改变,都会使同样的底层数组发生改变,类似浅拷贝
自定义切片
var s1 []int //定义一个存放int类型元素的切片
//初始化
s1 = []int{1,2,3}
由数组得到切片
a1 :=[...]int{1,2,3,4,5}
s3 := a1[0:4]
make函数创建切片
make([]int,元素数量,容量)
s1 := make([]int, 5,10)
fmt.Printf("s1=%v len(s1)=%d cap(s1)=%d\n", s1, len(s1), cap(s1))
切片的本质就是一个框,框住了一块连续的内存,属于引用类型,真正的数据都保存在底层数组里
切片不能直接比较,只能和nil比较
要检查切片是否为空,请始终使用len(s) == 0
来判断,而不应该使用s == nil
来判断。
append函数添加
调用append函数必须使用原来的切片变量接收返回值
s1 := []string{"龙潭", "四海门", "空境"}
s1 = append(s1, "洞天")
fmt.Println(s1)
//多个元素添加,用...进行拆开
s1 := []string{"龙潭", "四海门", "空境"}
s2 := []string{"望苍城", "天下城", "万象城"}
s1 = append(s1, s2...)
fmt.Println(s1)
切片的扩容策略
首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
copy
//此处,将s1的值拷贝给了s3,深拷贝,此时s1如何修改都与s3无关
s1 := []string{"龙潭", "四海门", "空境"}
s2 := s1
var s3 = make([]string, 3, 3)
fmt.Println(s1, s2, s3)
copy(s3, s1)
fmt.Println(s1, s2, s3)
从切片中删除元素
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]
7.指针
在Go语言中对于引用类型的变量,在使用的时候不仅要声明它,还要为它分配内存空间,否则值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。Go语言中new和make是内建的两个函数,主要用来分配内存。
go语言中不存在指针操作,只需要记住两个符号
&
:取地址
*
:根据地址取值
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
//&
a := 20
fmt.Println(&a)
//*
p := &a
m := *p
fmt.Println(m)
new函数
new函数是一个内置的函数,可以申请一个内存地址
new很少用,一般用来给基本数据类型申请内存,int/string等
var a *int //声明一个变量
fmt.Println(a)
var a2 = new(int) // new函数申请内存地址
fmt.Println(*a2)
*a2 = 100 //重新赋值
fmt.Println(*a2)
返回的是指针
make函数
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针。
8.map
Go语言中提供的映射关系容器为
map
,其内部使用散列表(hash)
实现。
map是一种无序的基于
key-value
的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
map[键类型]值类型
var a map[string]int // 声明一个map
a = make(map[string]int, 10) //初始化
a["天听"] = 20//添加数据
a["并轩"] = 30
a["冰冰"] = 16
fmt.Println(a)
v, ok := a["冰冰"] //返回两个值,一个是数据,一个是布尔值,约定成俗是ok
if !ok { //如果ok为false,则查找不到
fmt.Println("查无此人")
} else {
fmt.Println(v)
}
map的遍历
for k, v := range a {
fmt.Println(k, v)
}
如果只用一个参数接收,默认接收k
for k := range a {
fmt.Println(k)
}
如果只是单纯的想拿到值,可以用_接收k
for _, v := range a {
fmt.Println(v)
}
删除
用delete删除
delete(a,键)
9.函数
函数是一段代码的封装
在go语言中,函数格式为
func 函数名(参数)(返回值){}
并且要指定参数类型和返回值类型
func sum(x int, y int) (ret int) {
return x * y
}
func main() {
a := sum(2, 55)
fmt.Println(a)
}
无参数有返回值
func a1() string {
return "jajajaj"
}
返回值可以命名也可以不命名,但是,需要声明返回值
func a2(x int,y int)(ret int){ //声明ret
ret = x+y //直接可以使用
}
//也可以不命名返回值
func a2(x int,y int) int{
ret := x+y //声明
return ret //使用
}
返回值可以有多个,需要用()括起来
func a2(x int,y int)(int,string){
return 1,"天听"
}
参数类型简写
当参数中,连续两个参数类型一致,可以将前边的参数类型省略
func a2(x,y int)(ret int){
ret = x+y
}
可变长参数
func a2(x string,y ...int){ //...int,可变长,但是,都得是int类型
fmt.Println(x)
fmt.Println(y)
}
注意,go语言中函数传参永远是深拷贝
defer语句
defer
语句会将其后边跟随的语句进行延迟处理,在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行也就是说,有多个
defer
语句时,最先写的defer
最后被执行,最后写的defer
最先被执行
defer多用于函数结束之前释放资源
func deferDemo() {
fmt.Println("start")
defer fmt.Println("AAA")
defer fmt.Println("BBB")
defer fmt.Println("CCC")
fmt.Println("end")
}
/*
执行结果为
start
end
CCC
BBB
AAA
*/
Go语言中的return不是原子操作,在底层是分为两步来执行
第一步:返回值赋值
第二步:真正的RET返回
函数中如果存在defer,那么defer执行的时机是在第一步和第二步之间
func f1() (x int) {
x = 5 //返回值赋值
defer func() {
x++ //执行defer
}()
return x //RET返回
}
func f1() int { //此时的返回值没有命名
x := 5
defer func() { //defer修改的是x而不是返回值
x++
}()
return x
}
函数也可以作为参数和返回值
只要是满足参数和返回值的类型要求
func f1(){ //类型为func()
}
func f2() int{ // 类型为func()int
return 100
}
func f3(x func()){ //此处可以将类型为func()的函数传进来
}
func f4(x func()int){//此处可以将类型为func()int的函数传进来
}
匿名函数
因为函数内部无法声明有名字的函数,所以,在匿名函数多用于在函数内部
匿名函数定义:func(){}
func main(){
func(x,y int){
fmt.Println(x+y)
}(1,2)//如果只是调用一次的函数,可以简写成立即执行函数,加个括号
}
闭包
闭包的本质就是一个函数,函数可以作为返回值,因为函数内部查找变量的顺序是由内而外,所以,在自己内部找不到变量就得去外层找,包含了一个外部作用域变量的特殊函数
func f1(a int) (func(int) int, func(int) int) {
//声明一个函数,有参数a,两个有参有返的函数返回值
add := func(i int) int {
a += i
return a
}
//内部声明匿名函数
sub := func(i int) int {
a -= i
return a
}
//内部声明匿名函数
return add, sub
//满足了条件
}
func main() {
q1, q2 := f1(10) // q1,q2的类型为func(int) int
// 参数a是10
fmt.Println(q1(1), q2(2)) //11 9
}
上面的例子中为什么不是8而是9,因为传的参数a并不是内部匿名函数的参数,所以,按照返回值的顺序,先执行了add,再执行sub
10.作用域
和python相同点,全局作用域和局部作用域
不同点为,Go语言的if…else,switch..case等都会产生语句块作用域
11.内置函数
内置函数 | 介绍 |
---|---|
close | 主要用来关闭channel |
len | 用来求长度,比如string、array、slice、map、channel |
new | 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 |
make | 用来分配内存,主要用来分配引用类型,比如chan、map、slice |
append | 用来追加元素到数组、slice中 |
panic和recover | 用来做错误处理 |
panic/recover
Go语言中目前是没有异常机制,但是使用
panic/recover
模式来处理错误。panic
可以在任何地方引发,但recover
只有在defer
调用的函数中有效
func f2() {
defer func() {
err := recover()
fmt.Println(err)
fmt.Println("关闭程序")
}()
fmt.Println("B")
panic("错误")
}
注意,
recover
一定要搭配defer
使用
12.自定义类型和类型别名
自定义类型
type 自定义类型 内置类型
type ssint int
类型别名
type 别名 = 内置类型
type yourInt = int
13.结构体
结构体的内存地址是连续的,并不是每个字段一个内存
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
Go语言中通过struct
来实现面向对象
使用
type
和struct
关键字来定义结构体
type 类型名 struct{
字段名 字段类型
字段名 字段类型
}
type person struct{
name string
age int
gender string
}
//实例化"对象"
var a person
a.name = "天听"
a.age = 18
a.gender = "男"
匿名结构体
var s struct{
name string
age int
}
和定义结构体不同的是,匿名结构体是在声明变量,而不是声明结构体类型
结构体指针
结构体可以当做值传进函数内,通过指针,也可以将某个值的内存地址传过去
func f1(x person) { //此时传的是结构体
x.gender = "女"
}
func f2(x *person) { // 此时,传的是结构体的指针
(*x).gender = "女"
//x.gender = "女" //两种写法都可以
}
func main() {
var a person
a.name = "天听"
a.gender = "男"
f1(a)
fmt.Println(a.gender) //由于传过去的是深拷贝,所以,修改不会成功
f2(&a)
fmt.Println(a.gender)//通过指针修改了内存地址上的值,所以,修改成功了
}
结构体初始化
var b = persson{
name: "并轩",
gender: "女",
}
fmt.Println(b)
var c = persson{
"狮子",
"男",
}
fmt.Println(c)
注意,以上两种方法不能混用
当结构体比较大的时候尽量使用结构体指针,减少程序的内存开销
构造函数
返回一个结构体变量的函数
func newDog(name string) dog {
return dog{
name:name,
}
}
方法和接收者
方法是作用域特定类型的函数
接收者表示的是调用该方法的具体类型变量,多用类型名首字母小写表示,写在函数名前面
type dog struct{
name string
}
func (d dog)wang(){
fmt.Printf("%s:汪汪汪~",d.name)
}
func main(){
d1 := newDog("哈士奇")
d1.wang()
}
接收者
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
什么时候需要用指针接收者:
需要修改接收者的值,需要用指针接收者
保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者
接收者是拷贝代价比较大的大对象
匿名字段
结构体中只有类型没有名字的字段,并且类型不能重复
当字段比较少也比较简单时使用
type person struct{
string
int
//string 此时再写string就会报错,因为类型不能重复
}
嵌套结构体
结构体嵌套结构体
type address struct{ //类型1
name string
age int
}
type ren struct{
gender string
addr address // 嵌套了类型1
}
func main(){
f1 := ren{
gender:"男",
addr:address{ //嵌套结构体构造数据
name:"天听",
age:20,
}
}
}
匿名嵌套结构体
type address struct{
name string
age int
}
type ren struct{
gender string
address // 匿名嵌套
}
func main(){
f1 := ren{
gender:"男",
addr:address{
name:"天听",
age:20,
}
}
// fmt.Println(f1.addr.name)
fmt.Println(f1.name)
}
匿名嵌套结构体的好处是可以直接调用被嵌套结构体中的字段
但是,如果嵌套了两个结构体,而这两个结构体中有一样的字段,这种写法会有字段冲突,解决办法就是写全
结构体模拟“继承”
//动物类
type animal struct {
name string
}
//动物的方法
func (a animal) move() {
fmt.Printf("%s会动\n", a.name)
}
//狗类,继承动物类
type dog struct {
id uint8
animal
}
//狗类的方法
func (d dog) wang() {
fmt.Printf("%s在叫", d.name)
}
func main() {
d1 := dog{
id: 1,
animal: animal{name: "哈士奇"},
}
d1.wang()
d1.move() //动物类的方法,狗类同样也能使用
}
结构体与JSON
序列化:把go语言中的结构体变量转换为json格式的字符串
反序列化:json格式的字符串转换为go语言中能够识别的结构体变量
import "encoding/json"
func main(){
f1 := person{
name:"天听",
age:18,
}
b,err := json.Marshal(f1)
}
14.接口
关键字:interface
注意:接口是一种类型,他规定了数据有哪些方法
例如,结构体Dog可以walk和bark, 如果一个接口声明了walk和bark的方法签名,而Dog实现了walk和bark方法,那么Dog就实现了该接口。
接口的主要工作是仅提供由方法名称,输入参数和返回类型组成的方法签名集合。 由类型(例如struct结构体)来声明方法并实现它们。
//造接口,只要是有speak方法的变量,他就是speaker类型的接口
type speaker interface {
speak()
}
//造两个结构体
type dog struct{}
type cat struct{}
//各有一个speak方法
func (d dog) speak() {
fmt.Println("汪汪汪")
}
func (c cat) speak() {
fmt.Println("喵喵喵")
}
//只需要将接口传进来即可
func da(x speaker) {
x.speak()
}
func main() {
var c cat
var d dog
da(c)
da(d)
}
一个接口中可以有多个方法,但是需要全部实现才算是这个接口类型的变量,只实现其中的某个方法不算这个接口变量.
同一个结构体可以实现多个接口
使用值接收者和指针接收者实现接口区别:
1. 使用**值接收者**实现接口,结构体类型和结构体指针类型的变量都能存
2. **指针接收者**实现接口,只能存结构体指针类型的变量
空接口
interface{} // 空接口
所有的类型都实现了空接口,也就是任意类型的变量都能保存在空接口中
func main(){
var m1 map[string]interface{}//注意加括号,否则只是关键字
m1 = make(map[string]interface{})
m1["name"] = "天听"
m1["age"] = 18
}
在上面的例子中,既可以保存字符串,又能保存整型
类型断言
x.(T)
- x:表示类型为
interface{}
的变量 - T:表示断言,x可能是的类型
func ss(a interface{}){
str,ok := a.(string) //类型断言
if !ok{
fmt.Println("猜错了")
}else{
fmt.Println("字符串")
}
}
func ss(a interface{}){
switch t := a.(type){
case string:
fmt.Println("字符串")
case int:
fmt.Println("整型")
case bool:
fmt.Println("布尔")
}
}
15.包
包中的标识符
首字母大写才可以对外部可见
根据自己的需要创建自己的包。一个包可以简单理解为一个存放
.go
文件的文件夹。 该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。
package 包名 //包的定义
- 一个文件夹下面直接包含的文件只能归属一个
package
,同样一个package
的文件不能在多个文件夹下。 - 包名可以不和文件夹的名字一样,包名不能包含
-
符号。 - 包名为
main
的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main
包的源代码则不会得到可执行文件。
要在代码中引用其他包的内容,需要使用
import
关键字导入使用的包
import "包的路径" // 包的导入
注意事项:
- import导入语句通常放在文件开头包声明语句的下面。
- 导入的包名需要使用双引号包裹起来。
- 包名是从
$GOPATH/src/
后开始计算的,使用/
进行路径分隔。 - Go语言中禁止循环导入包。
匿名导入包
如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包
import _ "包的路径"
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中
init初始化函数
在Go语言程序执行时导入包语句会自动触发包内部
init()
函数的调用。需要注意的是:init()
函数没有参数也没有返回值。init()
函数在程序运行时自动被调用执行,不能在代码中主动调用它
16.文件操作
os.Open()
函数能够打开一个文件,返回一个*File
和一个err
。对得到的文件实例调用,close()
方法能够关闭文件。
为了防止文件忘记关闭,通常使用defer注册文件关闭语句
读取文件
//打开文件,返回两个参数
fileobj, err := os.Open("./main.go")
if err != nil {
fmt.Println("错误为:", err)
return
}
defer fileobj.Close()
var tmp = make([]byte, 128)
//var tmp = [128]byte
for {
n, err := fileobj.Read(tmp[:])
if err != nil {
fmt.Println("读取错误为:", err)
return
}
fmt.Println("读取的字节数为:", n)
fmt.Println(string(tmp[:n]))
if n < 128 {
return
}
}
file.Read()
它接收一个字节切片,返回读取的字节数和可能的具体错误,读到文件末尾时会返回
0
和io.EOF
func (f *File) Read(b []byte) (n int, err error)
//Read方法定义
bufio
bufio是在file的基础上封装了一层API,支持更多的功能
fileobj, err := os.Open("./main.go")
if err != nil {
fmt.Println("错误为:", err)
return
}
defer fileobj.Close()
//创建一个用来从文件中读取内容的对象
reader := bufio.NewReader(fileobj)
for {
line, err := reader.ReadString('\n')
if err == io.EOF {
return
}
if err != nil {
fmt.Println("!!!!", err)
return
}
fmt.Print(line)
}
ioutil
io/ioutil
包的ReadFile
方法能够读取完整的文件,只需要将文件名作为参数传入
content, err := ioutil.ReadFile("./main.go")
if err != nil {
fmt.Println("read file failed, err:", err)
return
}
fmt.Println(string(content))
文件写入
os.OpenFile()
函数能够以指定模式打开文件,从而实现文件写入相关功能
name
:要打开的文件名
flag
:打开文件的模式:
模式 | 含义 |
---|---|
os.O_WRONLY |
只写 |
os.O_CREATE |
创建文件 |
os.O_RDONLY |
只读 |
os.O_RDWR |
读写 |
os.O_TRUNC |
清空 |
os.O_APPEND |
追加 |
Write
写入字节切片数据
WriteString
直接写入字符串数据
fileobj, err := os.OpenFile("./ss.txt", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
fmt.Println(err)
return
}
fileobj.Write([]byte("执子之魂,与子共生\n"))
fileobj.WriteString("那些逃离死亡的人,其生命,早已停滞不前")
defer fileobj.Close()
bufio.NewWriter
fileobj, err := os.OpenFile("./ss.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
fmt.Println(err)
return
}
wr := bufio.NewWriter(fileobj)
//将数据先写入缓存
wr.WriteString("明天,只是一个希望,不是一个承诺")
//将缓存中的内容写入文件
wr.Flush()
defer fileobj.Close()
ioutil.WriteFile
str := "hello 龙潭"
err := ioutil.WriteFile("./xx.txt", []byte(str), 0666)
if err != nil {
fmt.Println("write file failed, err:", err)
return
}
17.时间模块
time
包
time包是时间模块
当前时间
func main(){
now := time.Now()
fmt.Println(now)
fmt.Println(now.Year())
}
时间戳
Unix()
func main(){
now := time.Now()
fmt.Println(now.Unix()) //时间戳
fmt.Println(now.UnixNano()) //纳秒时间戳
}
时间间隔
time.Duration
是time
包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。
time包中定义的时间间隔类型的常量如下:
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
time.Duration
表示1纳秒,time.Second
表示1秒。
时间操作
在日常编码可能会遇到要求时间+时间间隔需求,这时,需要用到
Add
fmt.Println(time.Second)
//当前时间+24小时
fmt.Println(now.Add(24*time.Hour))
时间格式化
通俗的讲,就是把语言中的时间对象转换成字符串类型的时间
格式化时间模板不是常见的
Y-m-d H:M:S
而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)
now.Format
// 注意,格式化是从2006-01-02开始,这个值不可变
fmt.Println(now.Format("2006-01-02"))
fmt.Println(now.Format("2006-01-02 15:04:05"))
Parse
按照对应的格式解析字符串类型的时间
time.Parse("2006-01-02","2020-8-20")
定时器
使用
time.Tick(时间间隔)
来设置定时器,定时器的本质上是一个通道(channel)
func tickDemo() {
ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
for i := range ticker {
fmt.Println(i)//每秒都会执行的任务
}
}
18.log日志服务
Go语言内置的
log
包实现了简单的日志服务。
log.Println("打印的日志")
//输入结果
//2020/08/20 16:28:50 打印的日志
log.SetOutput()
设置输出位置
19.反射
反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
反射类似于ORM
reflect
在Go语言的反射机制中,任何接口值都由是
一个具体类型
和具体类型的值
两部分组成
在Go语言中反射的相关功能由内置的reflect包提供,任意接口值在反射中都可以理解为由
reflect.Type
和reflect.Value
两部分组成,并且reflect包提供了reflect.TypeOf
和reflect.ValueOf
两个函数来获取任意对象的Value和Type。注意,无论是
TypeOf
还是ValueOf
,都有一个kind方法,要注意。
type name 和 type kind
在反射中类型具体划分为两种,一种是类型(Type), 一种是种类(Kind)
type Cat struct{
Name string
}
一个结构体,此时,我们如果要打印他的类型的话,他是一个Cat类型,但是,同时他也属于struct,struct就属于他的种类(Kind),Cat就是他的类型(Type),
20.高并发
都知道,golang天生支持高并发,语言特性使得他是一个并发的杀器。
Goroutine
Go语言的并发通过goroutine
实现。goroutine
类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine
并发工作。goroutine
是由Go语言的运行时(runtime)调度完成,而线程是由操作系统调度完成。
Go语言还提供channel
在多个goroutine
间进行通信。goroutine
和channel
是 Go 语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。
在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutine
,当你需要让某个任务并发执行的时候,你只需要把这个任务包装成一个函数,开启一个goroutine
去执行这个函数就可以了,就是这么简单粗暴。
Go语言中使用goroutine
非常简单,只需要在调用函数的时候在前面加上go
关键字,就可以为一个函数创建一个goroutine
。
一个goroutine
必定对应一个函数,可以创建多个goroutine
去执行相同的函数。
举个例子:
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Println("newTask",i)
time.Sleep(1 * time.Second)
}
}
func main() {
go newTask()
i := 0
for {
i++
fmt.Println("main", i)
time.Sleep(1 * time.Second)
}
}
在程序启动时,Go程序就会为
main()
函数创建一个默认的goroutine
。当main()函数返回的时候该
goroutine
就结束了,所有在main()
函数中启动的goroutine
会一同结束,main
函数所在的goroutine
就像是权利的游戏中的夜王,其他的goroutine
都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。
- Post link: https://www.godhearing.cn/go/
- Copyright Notice: All articles in this blog are licensed under unless otherwise stated.