引言:本文综合了几个比较主流的go语言博客,取长补短,较为详细的总结了变量常量,基本数据类型,流程控制,数组,切片,指针,map,函数方法,结构体等语言基础内容,在总结的过程中,我也更加熟练的使用go语言语法并粗浅的了解其原理。后面的章节内容需要更加深入的了解方能做好归纳总结并加以自己的观点,后续会做好总结,就先发前面的基础🥰 GO语言学习笔记详谈 GO基础相关学习:
Go语言基础–Printf格式化输出、Scanf格式化输入详解:https://blog.csdn.net/qq_34777600/article/details/81266453
1.Go语言基础之变量常量
1.Go语言的变量声明格式为:
var 变量名 变量类型
var 变量名 类型 = 表达式
0空字符串falsenil__:=
2.Go语言的常量声明格式为:
const pi = 3.1415
const e = 2.7182
//多个常量也可以一起声明。
const (
pi = 3.1415
e = 2.7182
)
//const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。
const (
n1 = 100
n2
n3
)
iotaiotaiota
2.Go语言基础之基本数据类型
Go语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。
- 值类型
bool
int(32 or 64), int8, int16, int32, int64
uint(32 or 64), uint8(byte), uint16, uint32, uint64
float32, float64
string
complex64, complex128
array -- 固定长度的数组
- 引用类型(指针类型)
slice -- 序列数组(最常用)
map -- 映射
chan -- 管道
1.1整型
uint8byteint16shortint64long
1.2.浮点型
float32float64IEEE 754float323.4e38math.MaxFloat32float641.8e308math.MaxFloat64
1.3复数
complex64complex128complex64complex128
1.4布尔值
booltrue(真)false(假)
1.5字符串
(int、bool、float32、float64 等)ASCII
1.6字符串的常用操作
- len(str) 求长度
- +或fmt.Sprintf 拼接字符串
- strings.Split 分割
- strings.contains 判断是否包含
- strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
- strings.Index(),strings.LastIndex() 子串出现的位置
- strings.Join(a[]string, sep string) join操作
1.7byte和rune类型
var b = 'x'
// 遍历字符串
func traversalString() {
s := "hello沙河"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}
1.8修改字符串
[]rune[]bytestring
func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}
1.9类型转换
- Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。
//举例:
func sqrtDemo() {
var a, b = 3, 4
var c int
// math.Sqrt()接收的参数是float64类型,需要强制转换
c = int(math.Sqrt(float64(a*a + b*b)))
fmt.Println(c)
}
3.Go语言基础之流程控制
一.if else(分支语句)
1.if条件判断基本写法
if 表达式1 { //判断语句没有括号
分支1
} else if 表达式2 {
分支2
} else{
分支3
}
例子:
score := 65
if score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
2.if条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断。
func ifDemo2() {
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}
}
二.for(循环结构)
for
//for循环的基本格式如下:
for 初始语句;条件表达式;结束语句{
循环体语句
}
1.for循环
truefalse
func forDemo() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
2.忽略for循环的初始语句
for循环的初始语句可以被忽略,但是初始语句后的分号必须要写
func forDemo2() {
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}
}
3.省略初始语句和结束语句
s := "abc"
n := len(s)
for n > 0 { // 替代 while (n > 0) {}
println(s[n]) // 替代 for (; n > 0;) {}
n--
}
//这种写法类似于其他编程语言中的while,在while后添加一个条件表达式,满足条件表达式时持续循环,否则结束循环。
4无限循环
for {
循环体语句
}
breakgotoreturnpanic
5.for range(键值循环)
for range*
三.switch case
switch
func switchDemo1() {
finger := 3
switch finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
default:
fmt.Println("无效的输入!")
}
}
//Go语言规定每个switch只能有一个default分支。
1.1一个分支可以有多个值,多个case值中间使用英文逗号分隔。
switch n := 7; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
1.2分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量。
case age < 25:
fmt.Println("好好学习吧")
fallthrough
2.Type Switch
- switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
switch x.(type){
case type:
statement(s)
case type:
statement(s)
/* 你可以定义任意个数的case */
default: /* 可选 */
statement(s)
}
//实例:
var x interface{}
//写法一:
switch i := x.(type) { // 带初始化语句
case nil:
fmt.Printf(" x 的类型 :%T\r\n", i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型")
default:
fmt.Printf("未知型")
}
3.select 语句
select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。select 是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。 select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
1.Go 编程语言中 select 语句的语法如下:
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
每个case都必须是一个通信
所有channel表达式都会被求值
所有被发送的表达式都会被求值
如果任意某个通信可以进行,它就执行;其他被忽略。
如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
否则:
如果有default子句,则执行该语句。
如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
实例:
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
结果: no communication
具体使用:https://www.topgoer.com/%E6%B5%81%E7%A8%8B%E6%8E%A7%E5%88%B6/%E6%9D%A1%E4%BB%B6%E8%AF%AD%E5%8F%A5select.html
四.goto,break,continue
1.goto(跳转到指定标签)
gotogotogoto
func gotoDemo2() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
// 设置退出标签
goto breakTag
}
fmt.Printf("%v-%v\n", i, j)
}
}
return
// 标签
breakTag:
fmt.Println("结束for循环")
}
2.break(跳出循环)
breakforswitchselectbreakforswitchselect
func breakDemo1() {
BREAKDEMO1:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
break BREAKDEMO1
}
fmt.Printf("%v-%v\n", i, j)
}
}
fmt.Println("...")
}
3.continue(继续下次循环)
continueforcontinue
4.Go语言基础之数组(重点)
引言:
- 数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。
// 定义一个长度为3元素类型为int的数组a
var a [3]int
1.数组的定义
var 数组变量名 [元素数量]T
var a [5]int[5]int[10]int0len-1
2.数组的初始化
2.方法一:
- 初始化数组时可以使用初始化列表来设置数组元素的值。
var testArray [3]int //数组会初始化为int类型的零值
var numArray = [3]int{1, 2} //使用指定的初始值完成初始化
var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
2.2方法二:
- 按照上面的方法每次都要确保提供的初始值和数组长度一致,一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度,
var testArray [3]int
var numArray = [...]int{1, 2}
var cityArray = [...]string{"北京", "上海", "深圳"}
2.3方法三:
- 我们还可以使用指定索引值的方式来初始化数组,
3.数组的遍历
- 遍历数组有以下两种方法:
func main() {
var a = [...]string{"北京", "上海", "深圳"}
// 方法1:for循环遍历
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
// 方法2:for range遍历
for index, value := range a {
fmt.Println(index, value)
}
}
4.多维数组
- Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。
4.1二维数组的定义
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
//遍历这个二维数组
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}
...
5.数组是值类型(重要‼️)
[n]*T*[n]T
5.Go语言基础之切片(重要‼️)
地址长度容量
1切片的定义
var name []T
//name:表示变量名
//T:表示切片中的元素类型
例子:
// 声明切片类型
var a []string //声明一个字符串切片
var b = []int{} //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
注意:切片是引用类型,不支持直接比较,只能和nil比较
2.切片的长度和容量
len()cap()
3.切片表达式
- 切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。
3.1简单切片表达式
lowhigh1<=索引值<3长度=high-low
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // s := a[low:high]
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
}
//结果:s:[2 3] len(s):2 cap(s):4
lowhigh
a[2:] // 等同于 a[2:len(a)]
a[:3] // 等同于 a[0:3]
a[:] // 等同于 a[0:len(a)]
0 <= low <= high <= len(a)
highcap(a)lowhighlow <= highpanic
func main() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // s := a[low:high]
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
s2 := s[3:4] // 索引的上限是cap(s)而不是len(s)
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
}
//结果
s:[2 3] len(s):2 cap(s):4
s2:[5] len(s2):1 cap(s2):1
3.2完整切片表达式
- 对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:
a[low : high : max]
a[low: high]max-low
func main() {
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))
}
//结果
t:[2 3] len(t):2 cap(t):4
0 <= low <= high <= max <= cap(a)
3.3使用make()函数构造切片
make()
make([]T, size, cap)
T:切片的元素类型
size:切片中元素的数量
cap:切片的容量
例子:
func main() {
a := make([]int, 2, 10)
fmt.Println(a) //[0 0]
fmt.Println(len(a)) //2
fmt.Println(cap(a)) //10
}
alen(a)cap(a)
4.切片的本质
- 切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)
a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}s1 := a[:5]
s2 := a[3:6]
len(s) == 0s == nil
5.切片不能直接比较
==nilnilnilnil
var s1 []int //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
len(s) == 0s == nil
6.切片的赋值拷贝
- 下面的代码中演示了拷贝前后两个变量**共享底层数组**,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。
func main() {
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
s2[0] = 100
fmt.Println(s1) //[100 0 0]
fmt.Println(s2) //[100 0 0]
}
7.切片遍历
for range
func main() {
s := []int{1, 3, 5}
//索引遍历
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}
//for range遍历
for index, value := range s {
fmt.Println(index, value)
}
}
8.append()方法为切片添加元素
append()
func main(){
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
}
注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。
var s []int
s = append(s, 1, 2, 3)
append()
func main() {
//append()添加元素和切片扩容
var numSlice []int
for i := 0; i < 10; i++ {
numSlice = append(numSlice, i)
fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
}
}
//结果
[0] len:1 cap:1 ptr:0xc0000a8000
[0 1] len:2 cap:2 ptr:0xc0000a8040
[0 1 2] len:3 cap:4 ptr:0xc0000b2020
[0 1 2 3] len:4 cap:4 ptr:0xc0000b2020
[0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000
从上面的结果可以看出:
append()
9.切片的扩容策略
$GOROOT/src/runtime/slice.go
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
1.首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
2.否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
3.否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
4.如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。
需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。
10.使用copy()函数复制切片
- 首先我们来看一个问题:
func main() {
a := []int{1, 2, 3, 4, 5}
b := a
fmt.Println(a) //[1 2 3 4 5]
fmt.Println(b) //[1 2 3 4 5]
b[0] = 1000
fmt.Println(a) //[1000 2 3 4 5]
fmt.Println(b) //[1000 2 3 4 5]
}
由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化。
copy()copy()
copy(destSlice, srcSlice []T
1.srcSlice: 数据来源切片
2.destSlice: 目标切片
11.从切片中删除元素
- Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。
func main() {
// 从切片中删除元素
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]
}
indexa = append(a[:index], a[index+1:]...)
12.字符串和切片(string and slice)
- string底层就是一个byte的数组,因此,也可以进行切片操作。
func main() {
str := "hello world"
s1 := str[0:5]
fmt.Println(s1)
s2 := str[6:]
fmt.Println(s2)
}
//结果
hello
world
- string本身是不可变的,因此要改变string中字符。需要如下操作: 英文字符串:
func main() {
str := "Hello world"
s := []byte(str) //中文字符需要用[]rune(str) 强转
s[6] = 'G'
s = s[:8]
s = append(s, '!')
str = string(s)
fmt.Println(str)
}
- 含有中文字符串
func main() {
str := "你好,世界!hello world!"
s := []rune(str) //强转
s[3] = '够'
s[4] = '浪'
s[12] = 'g'
s = s[:14]
str = string(s)
fmt.Println(str)
}
//结果:你好,够浪!hello go
Slice底层实现:https://www.topgoer.com/go%E5%9F%BA%E7%A1%80/Slice%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0.html#nil-%E5%92%8C%E7%A9%BA%E5%88%87%E7%89%87
6.Go语言基础之指针
1.Go语言中的指针
&*
2.指针地址和指针类型
(int、float、bool、string、array、struct)*int、*int64、*string
ptr := &v // v的类型为T
v:代表被取地址的变量,类型为T
ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}
b := &a
2.1指针取值
*
func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
结果:
type of b:*int
type of c:int
value of c:10
*&*
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
1.对变量进行取地址(&)操作,可以获得这个变量的指针变量。
2.指针变量的值是指针地址。
3.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
实例:
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
2.2空指针
- 当一个指针被定义后没有分配到任何变量时,它的值为 nil
- 空指针的判断
func main() {
var p *string
fmt.Println(p)
fmt.Printf("p的值是%v\n", p)
if p != nil {
fmt.Println("非空")
} else {
fmt.Println("空值")
}
}
3.new和make
我们先来看一个例子:
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["测试"] = 100
fmt.Println(b)
}
执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存
3.1.new
- new是一个内置的函数,它的函数签名如下
func new(Type) *Type
1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
- new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}
var a *int
func main() {
var a *int
a = new(int) //初始化
*a = 10
fmt.Println(*a)
3.2.make
- make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
var b map[string]int
func main() {
var b map[string]int
b = make(map[string]int, 10) //初始化
b["测试"] = 100
fmt.Println(b)
}
3.3new与make的区别
1.二者都是用来做内存分配的。
2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
7.Go语言基础之map
key-value
1.map定义:
- Go语言中 map的定义语法如下:
map[KeyType]ValueType
KeyType:表示键的类型。
ValueType:表示键对应的值的类型。
- map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:
make(map[KeyType]ValueType, [cap])
其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。
2.map基本使用
- map中的数据都是成对出现的,map的基本使用如下:
func main() {
scoreMap := make(map[string]int, 8)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
fmt.Println(scoreMap)
fmt.Println(scoreMap["小明"])
fmt.Printf("type of a:%T\n", scoreMap)
}
结果:
map[小明:100 张三:90]
100
type of a:map[string]int
- map也支持在声明的时候填充元素
func main() {
userInfo := map[string]string{
"username": "李政",
"password": "123456",
}
fmt.Println(userInfo)
}
3.判断某一个键是否存在
- Go语言中有个判断map中键是否存在的特殊写法
value, ok := map[key]
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
// 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
v, ok := scoreMap["张三"]
if ok {
fmt.Println(v)
} else {
fmt.Println("查无此人")
}
}
4.map的遍历
Go语言中使用for range遍历map
func main() {
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["李政"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}
}
//但我们只想遍历key的时候,可以按下面的写法:
for k := range scoreMap {
fmt.Println(k)
}
注意: 遍历map时的元素顺序与添加键值对的顺序无关。
5.使用delete()函数删除键值对
delete()
delete(map, key)
map:表示要删除键值对的map
key:表示要删除的键值对的键
6.按照指定的顺序遍历map
func main() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子
var scoreMap = make(map[string]int, 200)
for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
scoreMap[key] = value
}
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}
7.元素为map类型的切片
func main() {
var mapSlice = make([]map[string]string, 3)
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
fmt.Println("after init")
// 对切片中的map元素进行初始化
mapSlice[0] = make(map[string]string, 10)
mapSlice[0]["name"] = "李政"
mapSlice[0]["password"] = "123456"
mapSlice[0]["address"] = "信阳"
for index, value := range mapSlice {
fmt.Printf("index:%d value:%v\n", index, value)
}
}
8.值为切片类型的map
func main() {
var sliceMap = make(map[string][]string, 3)
fmt.Println(sliceMap)
fmt.Println("after init")
key := "中国"
value, ok := sliceMap[key]
if !ok {
value = make([]string, 0, 2)
}
value = append(value, "北京", "上海")
sliceMap[key] = value
fmt.Println(sliceMap)
}
8.Go语言基础之函数
- 函数是组织好的、可重复使用的、用于执行指定任务的代码块
一.函数
- Go语言中支持函数、匿名函数和闭包,并且函数在Go语言中属于“一等公民”🤩。
1.函数的定义
func
func 函数名(参数)(返回值){
函数体
}
函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。
返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。
函数体:实现指定功能的代码块。
2.函数的调用
函数名()
func intSum(x int, y int) int {
return x + y
}
func sayHello() {
fmt.Println("Hello 沙河")
}
func main() {
sayHello()
ret := intSum(10, 20)
fmt.Println(ret)
}
- 注意,调用有返回值的函数时,可以不接收其返回值。
3.参数
3.1类型简写
- 函数的参数中如果相邻变量的类型相同,则可以省略类型
func intSum(x, y int) int {
return x + y
}
上面的代码中,intSum函数有两个参数,这两个参数的类型均为int,因此可以省略x的类型,因为y后面有类型说明,x参数也是该类型。
3.2可变参数
...
func intSum2(x ...int) int {
fmt.Println(x) //x是一个切片
sum := 0
for _, v := range x {
sum = sum + v
}
return sum
}
//调用上面的函数:
ret1 := intSum2()
ret2 := intSum2(10)
ret3 := intSum2(10, 20)
ret4 := intSum2(10, 20, 30)
fmt.Println(ret1, ret2, ret3, ret4) //0 10 30 60
- 固定参数搭配可变参数使用时,可变参数要放在固定参数的后面
func intSum3(x int, y ...int) int {
fmt.Println(x, y)
sum := x
for _, v := range y {
sum = sum + v
}
return sum
}
//调用上面的函数
ret5 := intSum3(100)
ret6 := intSum3(100, 10)
ret7 := intSum3(100, 10, 20)
ret8 := intSum3(100, 10, 20, 30)
fmt.Println(ret5, ret6, ret7, ret8) //100 110 130 160
//本质上,函数的可变参数是通过切片来实现的。
4.返回值
return
4.1多返回值
()
func calc(x, y int) (int, int) {
sum := x + y
sub := x - y
return sum, sub
}
4.2返回值命名
return
func calc(x, y int) (sum, sub int) {
sum = x + y
sub = x - y
return
}
4.3返回值补充
- 当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int {
if x == "" {
return nil // 没必要返回[]int{}
}
...
}
二.函数进阶
1.变量作用域
1.全局变量
- 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。
package main
import "fmt"
//定义全局变量num
var num int64 = 10
func testGlobalVar() {
fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
}
func main() {
testGlobalVar() //num=10
}
2.局部变量
- 局部变量又分为两种: 函数内定义的变量无法在该函数外使用,例如下面的示例代码main函数中无法使用testLocalVar函数中定义的变量x:
func testLocalVar() {
//定义一个函数局部变量x,仅在该函数内生效
var x int64 = 100
fmt.Printf("x=%d\n", x)
}
func main() {
testLocalVar()
fmt.Println(x) // 此时无法使用变量x
}
注意:如果局部变量和全局变量重名,优先访问局部变量。
- 语句块定义的变量,通常我们会在if条件判断、for循环、switch语句上使用这种定义变量的方式。
func testLocalVar2(x, y int) {
fmt.Println(x, y) //函数的参数也是只在本函数中生效
if x > 0 {
z := 100 //变量z只在if语句块生效
fmt.Println(z)
}
//fmt.Println(z)//此处无法使用变量z
}
- for循环语句中定义的变量,也是只在for语句块中生效
func testLocalVar3() {
for i := 0; i < 10; i++ {
fmt.Println(i) //变量i只在当前for语句块中生效
}
//fmt.Println(i) //此处无法使用变量i
}
3.函数类型与变量
3.1定义函数类型
type
type calculation func(int, int) int //定义一个函数了类型
calculation
func add(x, y int) int {
return x + y
}
func sub(x, y int) int {
return x - y
}
//add和sub都能赋值给calculation类型的变量
var c calculation
c = add
3.2函数类型变量
我们可以声明函数类型的变量并且为该变量赋值
func main() {
var c calculation // 声明一个calculation类型的变量c
c = add // 把add赋值给c
fmt.Printf("type of c:%T\n", c) // type of c:main.calculation
fmt.Println(c(1, 2)) // 像调用add一样调用c
f := add // 将函数add赋值给变量f1
fmt.Printf("type of f:%T\n", f) // type of f:func(int, int) int
fmt.Println(f(10, 20)) // 像调用add一样调用f
}
4.高阶函数
- 高阶函数分为函数作为参数和函数作为返回值两部分。
1.函数作为参数
- 函数可以作为参数
func add(x, y int) int {
return x + y
}
func calc(x, y int, op func(int, int) int) int {//函数作为参数
return op(x, y)
}
func main() {
ret2 := calc(10, 20, add)
fmt.Println(ret2) //30
}
2.函数作为返回值
- 函数也阔以作为返回值
func do(s string) (func(int, int) int, error) {
switch s {
case "+":
return add, nil
case "-":
return sub, nil
default:
err := errors.New("无法识别的操作符")
return nil, err
}
}
三.匿名函数和闭包
1.匿名函数
- 函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:
func(参数)(返回值){
函数体
}
- 匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
注:匿名函数多用于实现回调函数和闭包。
2.闭包
闭包=函数+引用环境
func adder() func(int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder()
fmt.Println(f(10)) //10
fmt.Println(f(20)) //30
fmt.Println(f(30)) //60
f1 := adder()
fmt.Println(f1(40)) //40
fmt.Println(f1(50)) //90
}
fxffx
func adder2(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = adder2(10)
fmt.Println(f(10)) //20
fmt.Println(f(20)) //40
fmt.Println(f(30)) //70
f1 := adder2(20)
fmt.Println(f1(40)) //60
fmt.Println(f1(50)) //110
}
闭包=函数+引用环境
⚠️自己需要在找相关的资料看看。
四.defer语句
deferdeferdeferdeferdefer
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
输出:
start
end
3
2
1
deferdefer
1.defer执行时机
returndefer
2.defer经典案例
- 阅读下面的代码,写出最后的打印结果。
func f1() int {
x := 5
defer func() {
x++
}()
return x
}
func f2() (x int) {
defer func() {
x++
}()
return 5
}
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}
func f4() (x int) {
defer func(x int) {
x++
}(x)
return 5
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
fmt.Println(f4())
}
//结果
5
6
5
5
五.内置函数介绍
- close:主要用来关闭channel
- len:用来求长度,比如string、array、slice、map、channel
- new:用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
- make:用来分配内存,主要用来分配引用类型,比如chan、map、slice
- append:用来追加元素到数组、slice中
- panic和recover:用来做错误处理
9.Go语言基础之结构体
- Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
一.类型别名和自定义类型
1.类型别名
- 类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
//我们之前见的rune和byte都是类型别名
type byte = unit8
type rune = int32
2.自定义类型
string整型浮点型布尔type
//将MyInt定义为int类型
type MyInt int
通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
3.类型定义和类型别名的区别
- 类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
main.NewIntNewIntintMyIntMyInt
二.结构体
structstruct
1.结构体的定义
typestruct
type 类型名 struct{
字段名 字段类型
字段名 字段类型
...
}
类型名:标识自定义结构体的名称,在同一个包内不能重复。
字段名:表示结构体字段名。结构体中的字段名必须唯一。
字段类型:表示结构体字段的具体类型。
- 定义一个Person的结构体,
type person struct {
name string
city string
age int8
}
//同样类型的字段也阔以写在一行
type person1 struct {
name, city string
age int8
}
personnamecityageperson
2.结构体实例化
var
var 结构体实例 结构体类型
2.1基本实例化
.p1.namep1.age
type person struct {
name string
city string
age int8
}
func main() {
var p1 person
p1.name = "李政"
p1.city = "北京"
p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={李政 北京 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"李政", city:"北京", age:18}
}
2.2匿名结构体
- 在定义一些临时数据结构等场景下还可以使用匿名结构体。
func main() {
var user struct{Name string; Age int}
user.Name = "李政"
user.Age = 18
fmt.Printf("%#v\n", user)
}
2.3创建指针类型结构体
new
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
从打印的结果中我们可以看出p2是一个结构体指针
.
var p2 = new(person)
p2.name = "李政"
p2.age = 28
p2.city = "上海"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"李政", city:"上海", age:28}
2.4取结构体的地址实例化
&new
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "李政"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"李政", city:"成都", age:30}
p3.name = "李政"(*p3).name = "李政"
3.结构体初始化
- 没有初始化的结构体,其成员变量都是对应其类型的零值。
3.1使用键值对初始化
- 使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
p5 := person{
name: "李政",
city: "北京",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"李政", city:"北京", age:18}
- 也可以对结构体指针进行键值对初始化.
p6 := &person{
name: "李政",
city: "北京",
age: 18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"李政", city:"北京", age:18}
- 当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
3.2使用值的列表初始化
初始化的时候可以简写,也就是初始化的时候不写键,直接写值。
p8 := &person{
"李政",
"北京",
28,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"李政", city:"北京", age:28}
必须初始化结构体的所有字段。
初始值的填充顺序必须与字段在结构体中的声明顺序一致。
该方式不能和键值初始化方式混用。
4.结构体内存布局
- 结构体占用一块连续的内存。
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
//输出
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063
- 空结构体:空结构体是不占用空间的。
5.构造函数
personstruct
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
//调用构造函数
p9 := newPerson("李政", "固始", 90)
fmt.Printf("%#v\n", p9) //&main.person{name:"张三", city:"沙河", age:90}
6.方法和接收者
方法(Method)接收者(Receiver)thisself
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
方法名、参数列表、返回参数:具体格式与函数定义相同。
- 方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := NewPerson("小王子", 25)
p1.Dream()
}
6.1指针类型的接收者
thisselfPersonSetAge
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
//调用该方法
func main() {
p1 := NewPerson("李政", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}
6.2值类型的接受者
- 当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("李政", 25)
p1.Dream()
fmt.Println(p1.age) // 25
p1.SetAge2(30) // (*p1).SetAge2(30)
fmt.Println(p1.age) // 25
}
6.3什么时候使用指针类型接受者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
7.结构体的匿名字段
- 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
//Person 结构体Person类型
type Person struct {
string
int
}
func main() {
p1 := Person{
"李政",
18,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"北京", int:18}
fmt.Println(p1.string, p1.int) //北京 18
}
注意🧐:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
8.嵌套结构体
- 一个结构体中可以嵌套包含另一个结构体或结构体指针
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "李政",
Gender: "男",
Address: Address{
Province: "信阳",
City: "固始",
},
}
fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"李政", Gender:"男", Address:main.Address{Province:"信阳", City:"固始"}}
}
8.1嵌套匿名字段
Address
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名字段
}
func main() {
var user2 User
user2.Name = "李政"
user2.Gender = "男"
user2.Address.Province = "信阳" // 匿名字段默认使用类型名作为字段名
user2.City = "固始" // 匿名字段可以省略
fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"李政", Gender:"男", Address:main.Address{Province:"信阳", City:"固始"}}
}
8.2嵌套结构体的字段名冲突
- 嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
/Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}
//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user3 User
user3.Name = "李政"
user3.Gender = "男"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2001" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2001" //指定Email结构体中的CreateTime
}
8.3结构体的"继承"
- Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "李政",
},
}
d1.wang() //李政会汪汪汪~
d1.move() //李政会动!
}
9.结构体字段的可见性
🫣:结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
10.结构体标签(Tag)
TagTag
`key1:"value1" key2:"value2"`
Tag
Student
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "男",
name: "李政",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
}
11.结构体与JSON序列化
"":,
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
12.结构体使用map和slice时注意事项
- 因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意。
type Person struct {
name string
age int8
dreams []string
}
//错误做法
func (p *Person) SetDreams(dreams []string) {
p.dreams = dreams
}
//正确做法
func (p *Person) SetDreams(dreams []string) {
p.dreams = make([]string, len(dreams))
copy(p.dreams, dreams)
}
func main() {
p1 := Person{name: "李政", age: 18}
data := []string{"吃饭", "睡觉", "打豆豆"}
p1.SetDreams(data)
// 你真的想要修改 p1.dreams 吗?
data[1] = "不睡觉"
fmt.Println(p1.dreams) // ?
}
⚠️同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题。