非完全原创,部分内容来自于学习其他人的理论和B站视频。如果有侵权,请联系我,可以立即删除掉。
0、Go数据类型分类引用类型包括:指针,slice,map,channel,接口,函数类型等,其本质是一个指向对象的指针
值类型有:数组、结构体、基本数据类型
指针是一个代表着某个内存地址的值。这个内存地址往往是在内存中存储的另一个变量的值的起始位置
1、特点(1)默认值 nil,没有 NULL 和null常量
(2)操作符 & 取变量地址,* 通过指针访问目标对象
(3)不支持指针运算,不支持 “->” 运算符,直接⽤ . 访问目标成员
n := 66
var p *int = nil //声明一个变量p, 类型为 *int
p = &n
*p = 888
fmt.Printf("n = %d, &n = 0x%x, *p = %d, p = 0x%xn", n, &n, *p, p)
var p1 *int //声明方式1
p1 = new(int) //p1指向匿名的int变量
p2 := new(int) //声明方式2
*p2 = 111
output:
n = 888, &n = 0xc0000aa0c0, *p = 888, p = 0xc0000aa0c0
3、指针作为参数传递
func swap(x, y *int) {
*x, *y = *y, *x
}
func main() {
a := 6666
b := 8888
swap02(&a, &b) //变量值交换
fmt.Printf("a = %d, b = %dn", a, b)
二、数组
1、概念
数组是存储同一种数据类型多个元素的集合。它既可以存储基本数据类型,也可以存储引用数据类型。数组每个数据被称为数组元素,其包含的元素个数被称为数组的长度。
2、数组定义、遍历声明明语法如下:
var 数组变量名 [数组长度]Type
注意:数组长度不能是变量
var n int = 6
var a [n]int //err, non-constant array bound n
var b [10]int //方式1
c := [10]int{} //方式2:10个元素均为0
d := [...]int{1, 2, 3} //方式3:数组长度为3
e := [...]int{2: 6, 4: 8} //方式4:数组长度为5
//二位数组
f := [...][2]int{{0, 1}, {2, 3}, {4, 5}} //方式1:第2维不能用...
g := [7][2]int{1: {0: 0}, 5: {1: 1}} //方式2
h := [2][2]int{{0, 0}}
for i := 0; i < len[d]; i++ {
fmt.Printf("d[%d] = %dn", i, d[i])
}
//range具有两个返回值,第一个返回值是元素的数组下标,第二个返回值是元素的值
for i, v := range e {
fmt.Println("e[", i, "]=", v)
}
数组的特点是:
(1)长度是固定的,因此定义时长度不能为变量,且数组定义后其长度不可修改
(2)数组是随机访问的,因为其地址是连续的,可以直接通过索引来计算访问的地址,时间为O(1)
func main(){
a := [3][3]int8{}
for i, val := range a {
for j := 0; j < len(val); j++ {
fmt.Printf("0x%xt", &a[i][j])
}
fmt.Println()
}
}
output:
0xc0000140b0 0xc0000140b1 0xc0000140b2
0xc0000140b3 0xc0000140b4 0xc0000140b5
0xc0000140b6 0xc0000140b7 0xc0000140b8
3、数组作为参数
go语言中,数组作为函数的参数时,默认以值的方式传递的,因此开销会比较大。
数组指针作为参数时,则是地址传递
func val_func(array [3]int) {
array[0] = 6 // 值传递,此处修改不影响原数组
}
func addr_func(array *[3]int) {
(*array)[0] = 6 //引用传递,此处修改会影响原数组
}
func main() {
arr := [3]int{1, 2, 3}
val_func(arr)
fmt.Println(arr)
addr_func(&arr)
fmt.Println(arr)
}
output:
[1 2 3]
[6 2 3]
三、切片slice
由于数组的长度是确定的,且数组在定义后其长度无法再次修改,因此在遇到不确定长度的场景时,需要用到切片slice
切片并不是数组或数组指针,它通过内部指针和相关属性引⽤数组⽚段,以实现变⻓⽅案。它是一个引用类型
1、切片的声明var s1 []int //和数组声明差不多,只是不需要指定长度
//make([]T, length, capacity) //capacity省略,则和length的值相同
var s2 []int = make([]int, 1, 2)
s3 := make([]int, 0, 0)
s4 := []int{1, 2} //创建切片并初始化
2、切片的操作
| 操作 | 含义 |
|---|---|
| s[n] | 切片s中索引位置为n的项 |
| s[:] | 从切片s的索引位置0到len(s)-1处所获得的切片 |
| s[low:] | 从切片s的索引位置low到len(s)-1处所获得的切片 |
| s[:high] | 从切片s的索引位置0到high处所获得的切片,len=high |
| s[low:high] | 从切片s的索引位置low到high处所获得的切片,len=high-low |
| s[low:high:max] | 从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low |
| len(s) | 切片s的长度,总是<=cap(s) |
| cap(s) | 切片s的容量,总是>=len(s) |
var s1 []int
//追加元素
s1 = append(s1, 1, 2) //追加2个元素
fmt.Printf("s2 len = %d, cap = %dn", len(s2), cap(s2))
//复制元素:部分覆盖
data := [...]int{0, 1, 2, 3, 4, 5}
s2 := data[3:]
copy(data, s2)
fmt.Println(data) //[3, 4, 5, 3, 4, 5]
3、切片作为函数参数
func slice_test(s []int){
s[0] = 6
s[1] = 8
}
func main() {
s:= []int{0, 1, 2, 3, 4, 5}
slice_test(s)
fmt.Println(s) //[6 8 2 3 4 5]
}
四、map
Go语言中的map(映射、字典)是一种内置的数据结构,它是一个无序的key—value对的集合
map里所有的键都是唯一的,而且必须是支持==和!= 操作符的类型;map值可以是任意类型,没有限制,包括nil
map的格式:
map[keyType]valueType
var map1 map[int]string //声明一个map但没有初始化, map1 = nil
map2 := map[int]string{1: "jack", 2: "tom"}
map[1] = 'x' //将'jack'修改为`x`
map[3] = 'y' //追加一个元素,3:"y"
map3 := make(map[int]string)
map4 := make(map[int]map[int]string, 66) //指定容量
for k, v := range map2 {
fmt.Printf("%d: %sn", k, v)
}
for k := range map2 {
fmt.Printf("%d: %sn", k, map2[k])
}
//第一个返回value,第二个返回是否存在
val, ok := map[3] //ok = true
val2, ok2 := map[5] //val2 = "", ok2 = false
delete(map2, 1) //删除key = 1的数据
五、结构体struct
在开发中,有时需要将不同类型的数据组合成一个整体。比如人含有姓名、年龄、性别等属性,单独定义不太方便,因此可以使用结构体struct
1、结构体的基本操作由于结构体是值类型,因此两个结构体用==比较时,实际上是逐个属性进行比较;同时,作为参数是值传递,函数中的操作不会影响原结构体
type Student struct {
name string
sex byte
age int
}
func main() {
//1、顺序初始化,必须每个成员都初始化
var s1 Student = Student{"jack", 'm', 18}
//2、指定初始化某个成员,没有初始化的成员为零值
s2 := Student{name: "tom"}
s2.sex= "f"
s2.age= 20
p := &s2
p.name = 'hello' //p.name 和 (*p).name等价,go底层做了优化
(*p).age = 29
}
func test1(s Student){
s.name = "go"
}
func test2(s *Student){
s.name = "hello"
}
func main(){
s := Student{"jack", 'm', 18}
test1(s)
fmt.Println(s)
test2(&s)
fmt.Println(s)
}
output:
{jack 109 18}
{hello 109 18}
2、结构体的继承
type Study struct {
subjects []string
}
type Person struct {
name string
sex byte
age int
}
type Student struct {
Person //此处表示继承
Study //多重继承
id int
name string
course [9]int
}
func main() {
subjects := []string{"english", "math"}
st := Study{subjects}
p := Person{"jack", 'm', 20}
s := Student{p, st, 2018, "tom", [9]int{0}}
fmt.Printf("s.name = %s, s.Person.name = %s", s.name, s.Person.name)
}
output:
s.name = tom, s.Person.name = jack
六、接口
接口(interface)是一个抽象的数据类型,接口类型具体描述了一系列方法的集合。接口定义的是某种规范,不涉及实现,因此接口类型不能实例化
接口只有方法声明,没有实现,没有数据字段
格式:
type Person interface {
sleep()
eat()
}
接口是用来定义行为的类型,但行为的实现需要结构体的类型。
注意:只有实现了接口中的所有方法才算实现了接口,因此它接耦合了接口和类之间的联系
type Person interface{
sleep()
eat()
}
//Student实现了Person的所有接口,因此它实现了Person
type Student struct{
id int
name string
}
//Student实现sleep()方法
func (s *Student) sleep() {
fmt.Printf("Student[%d, %s] sleep!!n", s.id, s.name)
}
func (s *Student) eat() {
fmt.Printf("Student[%d, %s] eat!!n", s.id, s.name)
}
//Teacher 实现了Person的所有接口,因此它实现了Person
type Teacher struct { //老师
name, addr string
}
//Teacher实现sleep()方法
func (t *Teacher) sleep() {
fmt.Printf("Teacher[%s, %s] sleep!!n", t.name, t.addr)
}
func (t *Teacher) eat() {
fmt.Printf("Teacher[%s, %s] eat!!n", t.name, t.addr)
}
//由于两个结构体都实现了接口,因此参数为接口但实际可传结构体
//父类的引用去调用方法,实际是根据子类的类型去执行子类中的方法,即相同的调用方式调出不同的结果,即多态
func test(p Person){
p.sleep()
p.eat()
}
func main(){
s := Student{1020, "jack"}
t := Teacher{"mary", "Beijing"}
test(&s)
test(&t)
}
output:
Student[1020, jack] sleep!!
Student[1020, jack] eat!!
Teacher[mary, Beijing] sleep!!
Teacher[mary, Beijing] eat!!
七、json标签及序列化
1、json简介
json 是一种数据交换格式,它以key-value 键值对的形式来保存数据,键和值(int类型不需要双引号)都用双引号""包裹,使用冒号:分隔。广泛用于网络传输中
查看json和结构体对应关系的网址:https://www.json.cn/
注意点:
(1)结构体的标签tag对应的字段必须是大写,否则序列化出来的结果为空字符串。原因:json的方法在json包中实现的,获取结构体字段时必然要跨包访问,如果小写则获取不到字段
(2)Marshal和Unmarshal方法中形参是interface{},因此实参必须是结构体指针。另一个原因在于,反序列化时,需要给实参结构体变量赋值,因此需要传地址
(3)序列化后字符串的顺序和结构体字段的顺序可能会不一样,json序列化后默认按照键的字母排序
type Student struct {
Id int `json:"id"`
Name string `json:"name"`
Sex byte `json:"sex"`
Age int `json:"age"`
Course []int `json:"course"`
}
//此处重写String()方法,直接打印结构体时会自动调用,注意此处用的是(s Student),而不是指针
func (s Student) String() string {
return fmt.Sprintf("[%T] Id = %d, Name = %s, Sex = %c, Age = %d, Course = (%T)%v",
s, s.Id, s.Name, s.Sex, s.Age, s.Course, s.Course)
}
func main() {
s := Student{20220730, "tom", 'f', 30, []int{90, 95, 80}}
//序列化
data, err := json.Marshal(&s)
if err != nil {
fmt.Println("序列化失败, err = ", err)
}
fmt.Println("序列化后的结果:", string(data))
//反序列化
var s2 Student
str := "{"id":19970606,"name":"json","sex":78,"age":32,"course":[60, 100]}"
err = json.Unmarshal([]byte(str), &s2) //传地址才能改变原结构体
if err != nil {
fmt.Println("反序列化失败, err = ", err)
}
fmt.Println("反序列化后的结果:", s2)
}
output:
序列化后的结果: {"id":20220730,"name":"tom","sex":102,"age":30,"course":[90,95,80]}
反序列化后的结果: [main.Student] Id = 19970606, Name = json, Sex = N, Age = 32, Course = ([]int)[60 100]
3、Map序列化和反序列化
func main(){
m := make(map[string]interface{})
m["name"] = "Go"
m["age"] = 13
m["creator"] = "google"
m["similar"] = []string{"C", "Java", "Python"}
//序列化
data, err := json.Marshal(&m)
if err != nil {
fmt.Println("序列化失败, err = ", err)
}
fmt.Println("序列化后的结果:", string(data))
//反序列化
var m2 map[string]interface{}
mapstr := "{"name":"Go","age":13,"creator":"google","similar":["C","Java","Python"]}"
err = json.Unmarshal([]byte(mapstr), &m2) //传地址才能改变原结构体
if err != nil {
fmt.Println("反序列化失败, err = ", err)
}
fmt.Printf("反序列化后的结果:(%T) %v", m2, m2)
}
output:
序列化后的结果: {"age":13,"creator":"google","name":"Go","similar":["C","Java","Python"]}
反序列化后的结果:(map[string]interface {}) map[age:13 creator:google name:Go similar:[C Java Python]]
八、面向对象和面向过程
1、比较
1. 1、面向过程编程(POP)
它是直接将解决问题的步骤分析出来,然后用函数把步骤一步一步实现,然后再依次调用就可以了;它一种以过程为中心的编程思想,面向过程编程是基于“冯·诺依曼”模型
- 优点:流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析
- 缺点:需要深入的思考,耗费精力,代码重用性低,扩展能力差,后期维护难度比较大
它是将构成问题的事物,分解成若干个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在解决问题过程中的行为。
- 优点:易维护、易扩展、易复用,有封装、继承、多态的特性、更容易设计出耦合的系统
- 缺点:性能比面向过程低。
- 封装:封装是将过程和数据包围起来,数据只能通过定义的接口访问。面向对象计算从一个基本概念开始,即现实世界可以表示为一系列完全自治的、封装的对象,这些对象通过受保护的接口访问其他对象。
- 继承:继承是一种层次模型,它连接类,允许并鼓励类的重用,提供了一种明确表达共性的方法。对象的新类可以从现有类派生,这个过程称为类继承。新类继承原类的属性。新类被称为原类的派生类(子类),原类被称为新类的基类(父类)。
- 多态:多态允许不同类的对象响应相同的消息。例如,同样的加法,两次相加和两个整数相加,一定是完全不同的。多态语言具有灵活性、抽象性、行为共享性和代码共享性等优点,较好地解决了应用程序功能的同名问题。
在定义一个结构体或者类时,实际上就是把一类事物共有的属性(字段)和行为(方法)提取出来形成一个物理模型。go语言通过结构体来实现抽象,其他语言都是通过类
1.4.2、封装把抽象出的字段和对字段操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。其他语言将字段和方法封装在类里面,而go语言的结构体中只定义字段,在结构体外定义方法
1.4.3、多态指相同的消息给予不同的对象会引发不同的动作
变量/函数多态
- 变量多态是指:基类型的变量(对于C++是引用或指针)可以被赋值基类型对象,也可以被赋值派生类型的对象。
- 函数多态是指,相同的函数调用界面(函数名与实参表),传送给一个对象变量,可以有不同的行为,这视该对象变量所指向的对象类型而定
静态/动态多态
- 动态多态(dynamic polymorphism):通过类继承机制和虚函数机制生效于运行期。可以优雅地处理异质对象集合,只要其共同的基类定义了虚函数的接口。也被称为子类型多态(Subtype polymorphism)或包含多态(inclusion polymorphism)。在面向对象程序设计中,这被直接称为多态。
- 静态多态(static polymorphism):模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为“静态”。可以用来实现类型安全、运行高效的同质对象集合操作。C++ STL不采用动态多态来实现就是个例子。C++中实现的方式有:函数重载、运算符重载、带变量的宏多态、非参数化多态或译作特设多态、参数化多态(把类型作为参数的多态。在面向对象程序设计中,这被称作泛型编程)。Go语言没有这种多态
为防止代码冗余/复用,更利于代码的维护和功能拓展,需要继承方式。其他语言中继承需要用关键字extends来实现,go语言在结构体中第一行加上父结构体名即可
//C++实现继承
class Person{
public:
void show();
protected:
char *m_name;
int m_age;
}
void People::show() {
cout << m_name << "的年龄是" << m_age << endl;
}
class Student : public Person{
int id;
}
//Java实现继承
public class Person{
String name;
int age;
public show() {
System.out.prinf("name = %s, age = %dn", name, age);
}
}
public class Student extends Person{
int id;
void sleep(){
System.out.println(name+"sleep");
}
}
2、实现家庭收支记账软件
2.1、需求
软件显示的界面如下:
-------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):1 -------------当前收支明细记录------------- 收支 收支金额 剩余金额 说明 收入 10000.00 10000.00 工资 支出 100.00 9900.00 买菜
要求:
(1)软件刚进入时,需要显示主界面,并等到用户输入数字进行功能选择
(2)支持4个功能:查看明细、登记收入、等级支出、退出
因为程序的退出需要用户输入数字选择,因此主页面及这4个功能都需要放在一个循环中,同时添加一个变量来控制循环是否退出
实现的思路:
(1)接收用户输入,需要用到多个变量:功能选择的数字、记录收入/支出、收支的金额、收支的说明
(2)用于统计的变量:统计剩余的金额、统计每一次收支的log
package main
import "fmt"
func main() {
var num int
var total, money float32
var explain, log string
flag := false
//记录当前是否有交易
for loop := true; loop; {
fmt.Println("-------------家庭收支记账软件-------------")
fmt.Println(" 1 查看收支明细")
fmt.Println(" 2 登记收入记录")
fmt.Println(" 3 登记支出记录")
fmt.Println(" 4 退出")
fmt.Printf(" 请选择(1~4):")
fmt.Scan(&num)
switch num {
case 1:
fmt.Println("-------------当前收支明细记录-------------")
if flag {
fmt.Println("收支t收支金额t剩余金额t说明")
fmt.Println(log)
} else {
fmt.Println("当前没有收支记录,请添加一笔收入n")
}
case 2:
fmt.Printf("请输入收入金额:")
fmt.Scan(&money)
fmt.Printf("请输入收入说明:")
fmt.Scan(&explain)
total += money
log += fmt.Sprintf("收入t%-10.2ft%-10.2ft%sn", money, total, explain)
flag = true
case 3:
fmt.Printf("请输入支出金额:")
fmt.Scan(&money)
fmt.Printf("请输入支出说明:")
fmt.Scan(&explain)
if total < money {
fmt.Println("支出失败,当前余额不足!!!")
} else {
total -= money
log += fmt.Sprintf("支出t%-10.2ft%-10.2ft%sn", money, total, explain)
}
case 4:
loop = false
default:
fmt.Println("输入序号有误,请重新输入")
}
fmt.Println()
}
}
PS D:WorkSpaceGolangsrcTest01pop> go run .main.go -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):1 -------------当前收支明细记录------------- 当前没有收支记录,请添加一笔收入 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):2 请输入收入金额:10000 请输入收入说明:工资 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):1 -------------当前收支明细记录------------- 收支 收支金额 剩余金额 说明 收入 10000.00 10000.00 工资 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):3 请输入支出金额:100 请输入支出说明:买菜 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):1 -------------当前收支明细记录------------- 收支 收支金额 剩余金额 说明 收入 10000.00 10000.00 工资 支出 100.00 9900.00 买菜 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):42.4、面向对象的实现
package main
import "fmt"
type Account struct {
total, money float32
explain, log string
flag bool
}
func (a *Account) ShowDealInfo() {
fmt.Println("-------------当前收支明细记录-------------")
if a.flag {
fmt.Println("收支t收支金额t剩余金额t说明")
fmt.Println(a.log)
} else {
fmt.Println("当前没有收支记录,请添加一笔收入n")
}
}
func (a *Account) ManageIncoming() {
fmt.Printf("请输入收入金额:")
fmt.Scan(&a.money)
fmt.Printf("请输入收入说明:")
fmt.Scan(&a.explain)
a.total += a.money
a.log += fmt.Sprintf("收入t%-10.2ft%-10.2ft%sn", a.money, a.total, a.explain)
a.flag = true
}
func (a *Account) ManageSpend() {
fmt.Printf("请输入支出金额:")
fmt.Scan(&a.money)
fmt.Printf("请输入支出说明:")
fmt.Scan(&a.explain)
if a.total < a.money {
fmt.Println("支出失败,当前余额不足!!!")
} else {
a.total -= a.money
a.log += fmt.Sprintf("支出t%-10.2ft%-10.2ft%sn", a.money, a.total, a.explain)
}
}
func (a *Account) ShowMainUI() {
fmt.Println("-------------家庭收支记账软件-------------")
fmt.Println(" 1 查看收支明细")
fmt.Println(" 2 登记收入记录")
fmt.Println(" 3 登记支出记录")
fmt.Println(" 4 退出")
fmt.Printf(" 请选择(1~4):")
}
func GetAccount() *Account {
return &Account{0, 0, "", "", false}
}
func main() {
acc := GetAccount()
num := 0
ans := "Y"
for loop := true; loop; {
acc.ShowMainUI()
fmt.Scan(&num)
switch num {
case 1:
acc.ShowDealInfo()
case 2:
acc.ManageIncoming()
case 3:
acc.ManageSpend()
case 4:
for {
fmt.Printf("你确定要退出吗?Y/N:")
fmt.Scan(&ans)
if ans == "Y" || ans == "y" {
loop = false
break
} else if ans == "N" || ans == "n" {
break
} else {
fmt.Println("输入错误,请重新输入!")
}
}
default:
fmt.Println("输入序号有误,请重新输入")
}
fmt.Println()
}
}
PS D:WorkSpaceGolangsrcTest01oop> go run .main.go -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):1 -------------当前收支明细记录------------- 当前没有收支记录,请添加一笔收入 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):2 请输入收入金额:1300 请输入收入说明:补贴 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):1 -------------当前收支明细记录------------- 收支 收支金额 剩余金额 说明 收入 1300.00 1300.00 补贴 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):3 请输入支出金额:20 请输入支出说明:买饮料 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):1 -------------当前收支明细记录------------- 收支 收支金额 剩余金额 说明 收入 1300.00 1300.00 补贴 支出 20.00 1280.00 买饮料 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):3 请输入支出金额:10000 请输入支出说明:付款 支出失败,当前余额不足!!! -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):1 -------------当前收支明细记录------------- 收支 收支金额 剩余金额 说明 收入 1300.00 1300.00 补贴 支出 20.00 1280.00 买饮料 -------------家庭收支记账软件------------- 1 查看收支明细 2 登记收入记录 3 登记支出记录 4 退出 请选择(1~4):4 你确定要退出吗?Y/N:Y



