视频来源:B站《golang入门到项目实战 [2022最新Go语言教程,没有废话,纯干货!]》
文章为自己整理的学习笔记,侵权即删,谢谢支持!
值类型:基本数据类型 int
系列, float
系列, bool
, string
、数组和结构体 struct
引用类型:指针、slice
切片、map
、管道 chan
、interface 等都是引用类型
使用特点:
值类型:变量直接存储值,内存通常在栈中分配[外链图片转存失败,源站可能有防盗链机制,建议将
引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由 GC 来回收
内存的栈区和堆区示意图
Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。若传递数据使用指针,则无需拷贝数据。
类型指针不能进行偏移和运算。
Go语言中的指针操作非常简单,只需要记住两个符号:&
(取地址)和*
(根据地址取值)。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&
字符放在变量前面对变量进行取地址操作。Go语言中的值类型(int
、float
、bool
、string
、array
、struct
)都有对应的指针类型,如:*int
、*int64
、*string
等。
一个指针变量指向了一个值的内存地址。
也就是我们声明了一个指针之后,可以像变量赋值一样,把一个值的内存地址放入到指针当中。
类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:
var var_name *var-type
var-type
:为指针类型
var_name
:为指针变量名
*
:用于指定变量是作为一个指针。
package main
import "fmt"
func main() {
var a int = 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a变量的地址是: %x\n", &a)
/* 指针变量的存储地址 */
fmt.Printf("ip变量储存的指针地址: %x\n", ip)
/* 使用指针访问之 */
fmt.Printf("*ip变量的值: %d\n", *ip)
var sp *string
var s string = "hello"
sp = &s
fmt.Printf("sp的数据类型: %T\n", sp)
fmt.Printf("sp的值: %v\n", *sp)
var bp *bool
var b bool = true
fmt.Printf("bp的数据类型: %T\n", bp)
bp = &b
fmt.Printf("bp的值: %v\n", *bp)
}
运行结果:
a变量的地址是: c000014098
ip变量储存的指针地址: c000014098
*ip变量的值: 20
sp的数据类型: *string
sp的值: hello
bp的数据类型: *bool
bp的值: true
Go语言中指向数组的指针与C语言不同
C语言指向数组的指针是指向数组的首地址
Go语言中指向数组的指针是指向数组里面的每个元素
var ptr [MAX]*int;
表示数据里面的元素的类型是指针类型
package main
import "fmt"
const MAX int = 3
func main() {
a := []int{1, 3, 5}
var i int
var ptr [MAX]*int
fmt.Println(ptr) //这个打印出来的是[<nil> <nil> <nil>]
for i := 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i, *ptr[i]) //*ptr[i]就是打印出相关指针的值了
}
}
运行结果:
[<nil> <nil> <nil>]
a[0] = 1
a[1] = 3
a[2] = 5
语法:
type NewType Type
NewType
:定义的新类型
Type
:原类型
实例:
package main
import "fmt"
func main() {
// 类型定义
type MyInt int
// i为MyInt类型
var i MyInt
i = 100
fmt.Printf("i: %v i: %T\n", i, i)
}
运行结果:
i: 100 i: main.MyInt
语法:
type NewType = Type
NewType
:定义的类型别名
Type
:原类型
实例:
package main
import "fmt"
func main() {
// 类型别名定义
type MyInt2 = int
// i其实还是int类型
var i MyInt2
i = 100
fmt.Printf("i: %v i: %T\n", i, i)
}
运行结果:
i: 100 i: int
有两只猫:一只名字叫小白,今年3岁,白色;另一只锚叫小花,今年100岁,花色。请编写一个程序当用户输入名子时就显示该猫的名字、年龄和颜色;若输入名字错误时则显示没有这只猫
① 单独的定义变量解决
var cat1Name string = "小白"
var cat1Age int = 3
var cat1Color string = "白色"
var cat2Name string = "小花"
var cat2Age int = 100
var cat2Color string = "花色"
② 使用数组解决
var catName [2]string = [...]string{"小白", "小花"}
var catAge [2]int = [...]int{3, 100}
var catColor [2]string = [...]string{"白色", "花色"}
class
),Go 语言的结构体(struct
)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct
来实现 OOP 特性的。this
指针等等extends
关键字,继承是通过匿名字段来实现。type system
)的一部分,通过接口(interface
)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。结构体与结构体变量(实例/对象)的关系示意图:
对上图的说明:
用结构体方式解决5.1问题:
package main
import "fmt"
type Cat struct {
Name string
Age int
Color string
Hobby string
}
func main() {
var cat1 Cat
cat1.Name = "小白"
cat1.Age = 3
cat1.Color = "白色"
cat1.Hobby = "吃河虾"
fmt.Printf("cat1: %v\n", cat1)
fmt.Println("猫猫的信息如下:")
fmt.Printf("Name: %v\n", cat1.Name)
fmt.Printf("Age: %v\n", cat1.Age)
fmt.Printf("Color: %v\n", cat1.Color)
fmt.Printf("Hobby: %v\n", cat1.Hobby)
}
运行结果:
cat1: {小白 3 白色 吃河虾}
猫猫的信息如下:
Name: 小白
Age: 3
Color: 白色
Hobby: 吃河虾
type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}
type
:结构体定义关键字
struct_variable_typ
e:结构体类型名称
struct
:结构体定义关键字
member definition
:成员定义
例如,定义一个人的结构体Person:
type Person struct {
id int
name string
age int
email string
}
以上我们定义一个Person结构体,有四个成员,来描述一个Person的信息。
同类型的可以合并到一行,例如:
type Person struct {
id, age int
name, email string
}
声明一个结构体变量和声明一个普通变量相同,例如:
package main
import "fmt"
type Person struct {
id, age int
name, email string
}
func main() {
// 声明一个结构体变量
// 方式一:
var tom Person
fmt.Printf("tom: %v\n", tom)
// 方式二:
kite := Person{}
fmt.Printf("kite: %v\n", kite)
}
运行结果:
tom: {0 0 }
kite: {0 0 }
结构体成员在没有赋值之前都是零值。
结构体和结构体变量(实例)的区别和联系:
可以使用点运算符 .
,来访问结构体成员,例如:
package main
import "fmt"
func main() {
type Person struct {
id, age int
name, email string
}
var tom Person
tom.id = 1
tom.name = "tom"
tom.age = 20
tom.email = "tom@gmail.com"
fmt.Printf("tom: %v\n", tom)
}
运行结果如下:
tom: {1 20 tom tom@gmail.com}
如果结构体是临时使用,可以不用起名字,直接使用,例如:
package main
import "fmt"
func main() {
// 匿名结构体
var dog struct {
id int
name string
}
dog.id = 1
dog.name = "花花"
fmt.Printf("dog: %v\n", dog)
}
未初始化的结构体,成员都是零值
int 0
float 0.0
bool false
string nil
指针,slice,和 map 的零值都是 nil
例如:
package main
import "fmt"
type Person struct {
id, age int
name, email string
}
func main() {
var tom Person
fmt.Printf("tom: %v\n", tom)
}
运行结果:
tom: {0 0 }
package main
import "fmt"
func main() {
type Person struct {
id, age int
name, email string
}
kite := Person{
id: 1,
name: "kite",
age: 20,
email: "kite@gmail.com",
}
fmt.Printf("kite: %v\n", kite)
}
运行结果:
kite: {1 20 kite kite@gmail.com}
package main
import "fmt"
func main() {
type Person struct {
id, age int
name, email string
}
kite := Person{
1,
20,
"kite",
"kite@gmail.com",
}
fmt.Printf("kite: %v\n", kite)
}
运行结果:
kite: {1 20 kite kite@gmail.com}
注意:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
package main
import "fmt"
func main() {
type Person struct {
id, age int
name, email string
}
kite := Person{
id: 1,
name: "kite",
// 其他未初始化的结构体为零值
}
fmt.Printf("kite: %v\n", kite)
}
运行结果:
kite: {1 0 kite }
结构体指针和普通的变量指针相同,我先来回顾一下普通变量的指针,例如:
package main
import "fmt"
func main() {
var name string
name = "tom"
// 声明一个p_name 指针类型
var p_name *string
// &name 取name地址
p_name = &name
fmt.Printf("name: %v\n", name)
// 输出指针地址
fmt.Printf("p_name: %v\n", p_name)
// 输出指针指向的内容的值
fmt.Printf("*p_name: %v\n", *p_name)
}
运行结果:
name: tom
p_name: 0xc000050230
*p_name: tom
实例演示:
package main
import "fmt"
func main() {
type Person struct {
id int
name string
}
var tom = Person{1, "tom"}
var p_person *Person // 创建结构体指针
p_person = &tom
fmt.Printf("tom: %v\n", tom)
fmt.Printf("p_person: %p\n", p_person)
fmt.Printf("*p_person: %v\n", *p_person)
}
运行结果:
tom: {1 tom}
p_person: 0xc000004078
*p_person: {1 tom}
我们还可以通过使用new
关键字对结构体进行实例化,得到的是结构体的地址
例如:
package main
import "fmt"
func main() {
type Person struct {
id int
name string
}
var p_person = new(Person) // 创建结构体指针
fmt.Printf("p_person: %T\n", p_person)
}
运行结果:
p_person: *main.Person
从运行结果,我们发现p_person为指针类型
访问结构体指针成员,也使用点运算符 .
,例如:
package main
import "fmt"
func main() {
type Person struct {
id int
name string
}
var p_person = new(Person)
fmt.Printf("p_person: %T\n", p_person)
p_person.id = 1 // 访问结构体指针成员
p_person.name = "tom" // 访问结构体指针成员
fmt.Printf("*p_person: %v\n", *p_person)
}
运行结果
p_person: *main.Person
*p_person: {1 tom}
go结构体可以像普通变量一样,作为函数的参数,传递给函数,这里分为两种情况:
package main
import "fmt"
type Person struct {
id int
name string
}
// 拷贝了一份结构体副本
func showPerson(person Person) {
person.id = 1
person.name = "kite"
fmt.Printf("person: %v\n", person)
}
func main() {
person := Person{1, "tom"}
fmt.Printf("person: %v\n", person)
fmt.Println("-------------------")
showPerson(person)
fmt.Println("-------------------")
fmt.Printf("person: %v\n", person)
}
运行结果:
person: {1 tom}
-------------------
person: {1 kite}
-------------------
person: {1 tom}
从运行结果来看,在函数内部不会改变外面结构体内容
实例
package main
import "fmt"
type Person struct {
id int
name string
}
func showPerson(person *Person) {
person.id = 1
person.name = "kite"
fmt.Printf("person: %v\n", person)
}
func main() {
person := Person{1, "tom"}
fmt.Printf("person: %v\n", person)
fmt.Println("-------------------")
showPerson(&person)
fmt.Println("-------------------")
fmt.Printf("person: %v\n", person)
}
运行结果:
person: {1 tom}
-------------------
person: &{1 kite}
-------------------
person: {1 kite}
从运行结果来看,在函数内部改变了外面结构体内容
go语言没有面向对象编程思想,也没有继承关系,但是可以通过结构体嵌套来实现这种效果。
实例演示:假如有一个人Person结构体,这个人还养了一个宠物Dog结构体
Dog结构体:
type Dog struct {
name string
color string
age int
}
Person结构体:
type person struct {
dog Dog
name string
age int
}
访问它们:
package main
import "fmt"
type Dog struct {
name string
color string
age int
}
type person struct {
dog Dog
name string
age int
}
func main() {
var tom person
tom.dog.name = "花花"
tom.dog.color = "黑白花"
tom.dog.age = 2
tom.name = "tom"
tom.age = 20
fmt.Printf("tom: %v\n", tom)
}
运行结果
tom: {{花花 黑白花 2} tom 20}
先看下面一段代码:
package main
import "fmt"
type Person struct {
name string
age int
}
func main() {
var p1 Person
p1.age = 10
p1.name = "小明"
var p2 Person = p1
fmt.Printf("p2.age: %v\n", p2.age)
p2.name = "tom"
fmt.Printf("p2.name: %v\n", p2.name)
fmt.Printf("p1.name: %v\n", p1.name)
}
运行结果:
p2.age: 10
p2.name: tom
p1.name: 小明
我们知道变量总是存在内存中的,那么结构体变量在内存中如何存在?
示意图:
由此可见,p2是拷贝了一份p1的副本,p1和p2的地址并不相同
再看一段代码:
package main
import "fmt"
type Person struct {
name string
age int
}
func main() {
var p1 Person
p1.age = 10
p1.name = "小明"
var p2 *Person = &p1 // 内存如下图分析
fmt.Println((*p2).age)
fmt.Println(p2.age)
fmt.Println("----------")
p2.name = "tom~"
fmt.Printf("p2.name: %v\n", p2.name)
fmt.Printf("p1.name: %v\n", p1.name)
fmt.Println("----------")
fmt.Printf("*p2.name: %v\n", (*p2).name)
fmt.Printf("p1.name: %v\n", p1.name)
fmt.Println("----------")
fmt.Printf("p1的地址是: %p\n", &p1)
fmt.Printf("p2的地址是: %p\n", &p2)
fmt.Printf("p2的值是: %p\n", p2)
}
运行结果:
10
10
----------
p2.name: tom~
p1.name: tom~
----------
*p2.name: tom~
p1.name: tom~
----------
p1的地址是: 0xc0420023e0
p2的地址是: 0xc000004028
p2的值是: 0xc0420023e0
此代码内存图分析:
结构体的所有字段在内存中是连续的
package main
import "fmt"
type Point struct {
x int
y int
}
type Rect1 struct {
leftUp, rightDown Point
}
type Rect2 struct {
leftUp, rightDown *Point
}
func main() {
r1 := Rect1{Point{1, 2}, Point{3, 4}}
// r1有四个int,在内存中是连续分布的
// 打印地址
fmt.Printf("r1.leftUp.x的地址是: %p\nr1.leftUp.y的地址是: %p\nr1.rightDown.x的地址是: %p\nr1.rightDown.y的地址是: %p\n", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)
fmt.Println("----------")
r2 := Rect2{&Point{10, 20}, &Point{30, 40}}
// r2有两个*Point类型,这两个*Point类型的本身地址也是连续的
// 打印地址
fmt.Printf("r2.leftUp的地址是: %p\nr2.rightDown的地址是: %p\n", &r2.leftUp, &r2.rightDown)
fmt.Println("----------")
// 他们指向的地址不一定是连续的,这个要看系统在运行时是如何分配的
fmt.Printf("r2.leftUp指向的地址是: %p\nr2.rightDown指向的地址是: %p", r2.leftUp, r2.rightDown)
}
运行结果:
r1.leftUp.x的地址是: 0xc000012260
r1.leftUp.y的地址是: 0xc000012268
r1.rightDown.x的地址是: 0xc000012270
r1.rightDown.y的地址是: 0xc000012278
----------
r2.leftUp的地址是: 0xc00004e250
r2.rightDown的地址是: 0xc00004e258
----------
r2.leftUp指向的地址是: 0xc0000140c0
r2.rightDown指向的地址是: 0xc0000140d0
结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
package main
import "fmt"
type A struct {
Num int
}
type B struct {
Num int
}
func main() {
var a A
var b B
a = A(b) // 可以转换,但是要求结构体的字段要完全一样(名字、个数和类型)
fmt.Println(a, b)
}
运行结果:
{0} {0}
结构体进行 type
重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
结构体的每个字段上,可以写上一个 tag
, 该 tag
可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
序列化使用场景:实例:
package main
import (
"encoding/json"
"fmt"
)
type Monster struct {
Name string `json:"name"` // `json:"name"`就是一个结构体tag
Age int `json:"age"`
Skill string `json:"skill"`
}
func main() {
//
monster := Monster{"牛魔王", 500, "芭蕉扇~"}
//
//
jsonStr, err := json.Marshal(monster)
if err != nil {
fmt.Printf("json处理错误: %v\n", err)
}
fmt.Printf("jsonStr: %v\n", string(jsonStr))
}
运行结果:
jsonStr: {"name":"牛魔王","age":500,"skill":"芭蕉扇~"}