Golang学习笔记
Golang的优势
- 极简单的部署方式
- 可直接编译成机器码
- 不依赖其他库
- 直接运行即可部署
- 静态类型语言
- 编译的时候能够直接检查出隐藏的错误
- 语言层面的并发
- 语言特点支持
- 充分利用多核
- 强大的标准库支撑
- runtime系统调度机制
- 高效的GC垃圾回收机制
- 丰富的标准库
- 简单、容易上手
- 只有25个关键字
- 语法跟C语言有相似点
- 有面向对象特征(继承、多态、封装)
- 支持跨平台开发
- 大厂
- Google (k8s)
- 腾讯(docker)百度 京东 小米 七牛云 滴滴
- 美团 阿里巴巴 字节跳动 抖音 哔站
Golang适合做什么
优点
- 云计算基础设施领域:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储
- 基础后端软件:tidb、influxdb、cockroachdb
- 微服务:go-kit、micro、monzo bank的typhon、bilibili
- 互联网基础设施:以太坊、hyperledger
不足
- 大部分包管理在github上面
- 无泛化类型,有些相同功能的代码需要重复写(go 1.18已加上)
- 对于所有的Exception都用Error来处理
- 对C的降级处理并非无缝,没有C降级到asm那么完美
Golang文件
bin:编译好的代码文件存放处
pkg:依赖的存放位置
src:源码的存放位置
- 语法规范
花括号要跟函数定义处在同一行 - 运行.go文件:
- go run main.go 直接运行main.go文件,文件存储在临时目录
- go build main.go 生成可执行main.exe,再通过./main.go执行
Golang变量声明
go局部变量声明有四种方式,全局变量声明只有三种方式,:=
符号不能声明全局变量。对于多变量的声明跟单变量声明类似,多一种多行声明的方式。
package main
import "fmt"
//全局变量声明
//方法一到三都适用
var A int
var B int = 100
var C = 100
func main() {
//局部变量声明
//方法一
var a int
a = 10
fmt.Println(a)
//方法二
var b int = 10
fmt.Println(b)
//方法三
var c = 10
fmt.Println(c)
//方法四
d := 10
fmt.Println(d)
fmt.Println("下面是全局变量的输出结果:")
//方法一
A = 100
fmt.Println(A)
//方法二
fmt.Println(B)
//方法三
fmt.Println(C)
fmt.Println("下面是多变量的声明和输出结果")
//单行写法
var Q1, Q2 int = 1000, 2000
fmt.Println(Q1, Q2)
//多行写法
var ( //注意此处是圆括号
S1 int = 1000
S2 string = "hello go"
)
fmt.Println(S1, S2)
}
在进行多变量多行声明时,要特别注意var关键字后的括号为()。
Golang常量声明
使用const
关键字进行常量的声明:
//常量声明 const a = 10
const (
b = 15
c = 20
)
在const
声明中使用iota
关键字进行逐行+1。
//iota在const中的使用
//iota初始值=0,下面每一行会默认+1,只能在const里使用
const (
A = 2 * iota
B
C
)
上述代码的运行结果如下:
0 2 4
iota第一行初始值为0,每多一行常量值会+1,可以对iota
关键字进行运算操作。
Golang函数多个返回值
Golang的函数可以有多个返回值,可以直接
return a,b
来返回a和b的值,也可以直接使用return
,后面不需要跟变量。
Golang关键字
defer
defer
关键字在Golang中常常用于代码最后执行的部分,如果有多个defer
语句,则执行过程依照压栈、出栈的形式进行运行。
func main{
defer fun1()
defer fun2()
defer fun3()
}
参考上述代码,将fun1到fun3依次压栈,执行时从fun3到fun1依次出栈,所以打印顺序为fun3到fun1。
如果一个函数体里有返回值,那么先执行return还是defer呢?
我们来看下面的代码:
package main
import "fmt"
func printdefer() int {//defer输出函数
fmt.Println("defer running!")
return 0
}
func printreturn() int {//return输出函数
fmt.Println("return running!")
return 0
}
func goget() int {//同时调用
defer printdefer()
return printreturn()
return 0
}
func main() {
goget()
}
通过这个代码,我们可以得到defer和return的执行顺序,程序执行结果如下:
return running!
defer running!
可知defer永远是在最后执行。
range
Golang数组
- 数组的声明
//方法一
var Array1 [10] int
//方法二
array1 := [10] int {0,1,2,3}
//方法三
array1 := [4] {0,1,2,3}
程序运行结果如下:
[0 0 0 0 0 0 0 0 0 0]
[0 1 2 3 0 0 0 0 0 0]
[0 1 2 3]
由程序运行的结果可知,当只声明数组时,数组内的数值会自动初始化为0。我们也可以对数组的一部分进行赋值。
func arrayDemo(array1 [4]int){//值拷贝
}
当数组作为函数参数时,进行的是值拷贝,不会改变原函数中数组的值。
- 动态数组
slice(动态数组)的声明结构有以下几种
//声明slice是一个切片,并且默认值为1,2,3,长度为3
slice1 := []int {1,2,3}
%v可以查看所有信息
//声明slice是一个切片,无默认值
var slice1 [] int
slice1 =make ([]int ,3 )//通过make给slice分配地址空间
slice1:=make([]int ,3)
//判断slice是否为空
if slice == nil
在作为函数参数时,进行的是引用传递
func arrayDemo(array1 [] int){//引用传递
}
slice的长度的追加
//make(type,len,cap) type为类型,len为长度,cap为切片容量
var character = make([] int , 3, 5)
//通过append(slice,element)slice为操作的切片,element为添加的元素来进行元素的追加
character = append(character,2)
来看下面这段代码及输出:
var character = make([]int, 3, 5)
fmt.Printf("slice = %v,len = %d,cap = %d\n", character, len(character), cap(character))
//通过append(slice,element)slice为操作的切片,element为添加的元素来进行元素的追加
character = append(character, 2)
fmt.Printf("slice = %v,len = %d,cap = %d\n", character, len(character), cap(character))
输出结果:
slice = [0 0 0],len = 3,cap = 5
slice = [0 0 0 2],len = 4,cap = 5
当len==cap时,若想继续追加元素,slice会自动扩容,增加当前cap长度的内存空间,即当前内存空间为2*cap长度。
character = append(character, 4)
fmt.Printf("slice = %v,len = %d,cap = %d\n", character, len(character), cap(character))
输出结果:
slice = [0 0 0 2 3 4],len = 6,cap = 10
slice的截取
1、采用左闭右开
var slice = []int{1, 2, 3}
fmt.Printf("slice = %v\n", slice)
s := slice[0:2]
fmt.Printf("s = %v\n", s)
输出结果为:
slice = [1 2 3]
s = [1 2]
2、通过copy函数
var slice = []int{1, 2, 3}
fmt.Printf("slice = %v\n", slice)
s := slice[0:2]
fmt.Printf("s = %v\n", s)
s1 := make([]int, 3)
copy(s1, s)
fmt.Printf("s1 = %v\n", s1)
输出结果为:
slice = [1 2 3]
s = [1 2]
s1 = [1 2 0]
第一种方式可以看成是浅拷贝,采用copy函数则为深拷贝。
map
- map的声明
map的声明需要给map分配地址空间,一般为:make(map[int]string)
的方式,即通过make进行分配。
下面提供几种声明方式:
func main() {
//第一种声明
var map1 = make(map[int]string, 10)//声明开辟的空间个数
map1[1] = "h"
map1[2] = "he"
map1[3] = "hel"
fmt.Println(map1)
//第二种声明
var map2 = make(map[int]string)//不声明空间个数,根据定义开辟
map2[1] = "wanzi"
map2[2] = "yongzai"
map2[3] = "halo"
map2[4] = "readme"
fmt.Println(map2)
//第三种声明
map3 := make(map[int]string)
map3[1] = "re"
map3[2] = "er"
fmt.Println(map3)
}
- map的使用
下面演示对map进行赋值、遍历、删除和函数参数的传递
func change(map_class_add map[string]string) {
//进行的是引用传递,即传入参数的指针地址
map_class_add["qianrushi"] = "qianrushi1"
}
//赋值
map_class := make(map[string]string)
map_class["ruanjian"] = "runajian1"
map_class["jisuanji"] = "jisuanji1"
fmt.Println(map_class)
//遍历
for key, value := range map_class {//用range遍历map的每一组键值对
fmt.Println(key + ":" + value)
}
//引用函数进行传参
fmt.Println("——————————————————")
change(map_class)
for key, value := range map_class {
fmt.Println(key + ":" + value)
}
//删除
fmt.Println("——————————————————")
delete(map_class, "ruanjian")
for key, value := range map_class {
fmt.Println(key + ":" + value)
}
输出结果如下:
map[jisuanji:jisuanji1 ruanjian:runajian1]
jisuanji:jisuanji1
ruanjian:runajian1
——————————————————
ruanjian:runajian1
jisuanji:jisuanji1
qianrushi:qianrushi1
——————————————————
jisuanji:jisuanji1
qianrushi:qianrushi1
面向对象的表示
go语言中类的定义采用的是下面这种格式
type student struct {
Name string
Age int
Class int
}
其中student为类名,{}内的为属性值
类方法格式如下:
func (s *student) Setstuname(newname string) string {
s.Name = newname
return s.Name
}
func 后面跟的()内的值意为该方法是student类的方法,s即为一个对象,后面的内容与go语言函数的定义相同。
在此需要注意,类方法描述捆绑的类有两种形式,一种为(s student)
,另一种为(s *student)
。在语法上两种形式都是可行的,但是在实际应用中会有很大的不同。我们通过下面的例子来讲解:
`Example1`
package main
import "fmt"
type student struct {
Name string
Age int
Class int
}
func (stu student) Setstuname(newname string) {
stu.Name = newname
stu.Age = 17
stu.Class = 5
}
func (stu student) show() {
fmt.Println("姓名:" + stu.Name)
fmt.Println("年龄:", stu.Age)
fmt.Println("班级:", stu.Class)
}
func main() {
s := student{}
s.show()
name := "李四"
s.Setstuname(name)
s.show()
}
`Example2`
package main
import "fmt"
type student struct {
Name string
Age int
Class int
}
func (stu *student) Setstuname(newname string) {
stu.Name = newname
stu.Age = 17
stu.Class = 5
}
func (stu *student) show() {
fmt.Println("姓名:" + stu.Name)
fmt.Println("年龄:", stu.Age)
fmt.Println("班级:", stu.Class)
}
func main() {
s := student{}
s.show()
name := "李四"
s.Setstuname(name)
s.show()
}
看了上面两个例子,大家可以自己运行一下代码看看有什么区别,并思考一下原因
在Example1中,当我们进行第一次show操作时,打印的是初始化之后的s对象。在进行修改name操作后,s对象被拷贝到Setstuname方法中,stu的变量生命周期也只限于这一个方法内,所以我们在方法内的操作只会随着方法调用完而消失。
而在Example2中,进行修改name操作后,传入方法的是主函数中s的地址,赋值给stu对象,所以进行的所有修改操作都会更改s的值。
两个例子的打印结果如下:
`Example1`
姓名:
年龄: 0
班级: 0
姓名:
年龄: 0
班级: 0
`Example2`
姓名:
年龄: 0
班级: 0
姓名:李四
年龄: 17
班级: 5
总的来说,根本区别就是选择值的拷贝还是地址的传递。在进行读操作时,其实两种方法都可以;但是在进行写操作时,只有地址的传递才能够进行修改。
面向对象的封装
类名、属性名、方法名如果是大写,则其他包可以对其进行调用。如果是小写,只能在本包内进行访问。