Golang语言面向对象编程说明:
假设定义结构体是老师,属性:名字,年龄,学校
type Teacher struct {
//变量名字大写外界可以访问这个属性
Name string
Age int
School string
}
下面我们尝试赋值,并打印出来:
type Teacher struct {
//变量名字大写外界可以访问这个属性
Name string
Age int
School string
}
func main() {
var tea1 Teacher
tea1.Name = "张三"
tea1.Age = 28
tea1.School = "深圳大学"
fmt.Println("tea1=", tea1)
fmt.Println("老师的信息如下:")
fmt.Println("name=", tea1.Name)
fmt.Println("age=", tea1.Age)
fmt.Println("school=", tea1.School)
}
输出结果:
tea1= {张三 28 深圳大学}
老师的信息如下:
name= 张三
age= 28
school= 深圳大学
通过上面的案例和讲解可以看出:
在Go语言中,结构体的存储是在堆上。
当我们创建一个结构体实例时,它的内存将被分配在堆上。这意味着结构体的生命周期可以超出创建它的函数的生命周期。
例如,当我们使用new
关键字或make
函数创建一个结构体实例时,它将被分配在堆上。
type Person struct {
Name string
Age int
}
func main() {
// 使用 new 关键字创建结构体实例
p := new(Person)
p.Name = "Alice"
p.Age = 25
// 使用 make 函数创建结构体实例(只适用于为某些类型分配内存,如 map、slice 和 channel)
m := make(map[string]int)
m["key"] = 42
// 结构体实例被分配在堆上,可以在其他函数中继续使用
anotherFunc(p)
yetAnotherFunc(m)
}
这是因为在Go语言中,所有的变量都是通过传值而不是通过引用传递。在堆上分配结构体实例可以确保结构体数据的持久性和可用性。
Go语言中结构体的内存布局
Go语言中的结构体是一组值的集合
,这些值被存储在内存中的一段连续的区域
。结构体的内存布局取决于结构体中的成员变量顺序和类型
,以及对齐方式。
结构体的对齐方式
Go语言中使用的是一种称为Packing的方式
进行对齐。这种方式默认对齐到最多8字节的倍数,即8字节对齐。可以通过在结构体成员变量的后面添加逗号
和数字
的形式手动调节对齐方式。
type Person struct {
Name string
Age int
Height float64
}
func main() {
var p Person
// 获取结构体的大小
size := unsafe.Sizeof(p)
fmt.Printf("结构体大小:%d 字节\n", size)
// 获取结构体字段的偏移量
nameOffset := unsafe.Offsetof(p.Name)
ageOffset := unsafe.Offsetof(p.Age)
heightOffset := unsafe.Offsetof(p.Height)
fmt.Printf("Name 字段的偏移量:%d 字节\n", nameOffset)
fmt.Printf("Age 字段的偏移量:%d 字节\n", ageOffset)
fmt.Printf("Height 字段的偏移量:%d 字节\n", heightOffset)
// 结构体的对齐方式
packed := unsafe.Alignof(p)
fmt.Printf("结构体的对齐方式:%d 字节\n", packed)
}
输出结果:
结构体大小:20 字节
Name 字段的偏移量:0 字节
Age 字段的偏移量:8 字节
Height 字段的偏移量:12 字节
结构体的对齐方式:4 字节
在这个示例中,我们定义了一个Person
结构体,它包括名字、年龄和身高三个字段。我们通过unsafe
包中的函数来获取结构体的大小、字段的偏移量以及对齐方式。结构体的大小为20字节,字段的偏移量分别为0字节、8字节和12字节,结构体的对齐方式为4字节。
基本介绍
方式一:直接声明
案例:var person Person
方式二:{}
案例:var person Person = Person{“Tom”, 18} => person := Person{“Tom”, 18}
方式三:&
案例:var person *Person = new (Person)
type Person struct {
Name string
Age int
}
func main() {
var p *Person = new(Person)
// (*p).Name = "smith" 标准写法
// go设计者,为了程序使用方便,底层对下面这个做了优化,实现了这种简单的写法
// 会给 p 加上 取值运算 =》 (*p).Name = "smith"
p.Name = "smith"
p.Age = 18
fmt.Println(p)
fmt.Println(*p)
}
输出结果:
&{smith 18}
{smith 18}
方式四:{}
案例:var person *Person = &Person{}
type Person struct {
Name string
Age int
}
func main() {
var p *Person = &Person{}
// 标准方式:(*person).Name = "scott"
p.Name = "scott"
p.Age = 18
fmt.Println(p)
}
输出结果:&{scott 18}
//结构体
type Point struct {
x int
y int
}
//结构体
type Rect struct {
leftUp, rightDown Point
}
//结构体
type Rect2 struct {
leftUp, rightDown *Point
}
func main() {
r1 := Rect{Point{1,2}, Point{3,4}}
//r1有四个int, 在内存中是连续分布
//打印地址
fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p \n",
&r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)
//r2有两个 *Point类型,这个两个*Point类型的本身地址也是连续的,
//但是他们指向的地址不一定是连续
r2 := Rect2{&Point{10,20}, &Point{30,40}}
//打印地址
fmt.Printf("r2.leftUp 本身地址=%p r2.rightDown 本身地址=%p \n",
&r2.leftUp, &r2.rightDown)
//他们指向的地址不一定是连续..., 这个要看系统在运行时是如何分配
fmt.Printf("r2.leftUp 指向地址=%p r2.rightDown 指向地址=%p \n",
r2.leftUp, r2.rightDown)
}
输出结果:
r1.leftUp.x 地址=0x9496080 r1.leftUp.y 地址=0x9496084 r1.rightDown.x 地址=0x9496088 r1.rightDown.y 地址=0x949608c
r2.leftUp 本身地址=0x948a038 r2.rightDown 本身地址=0x948a03c
r2.leftUp 指向地址=0x9496068 r2.rightDown 指向地址=0x94960a0
结构体之间可以转换,但是有要求,就是结构体的字段要完全一样(包括:名字、类型,个数)
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)
}
结构体进行type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转。
type Student struct {
Name string
Age int
}
type Stu Student
func main() {
var stu1 Student
var stu2 Stu
// stu2 = stu1 // 错误,系统认为这是两个不一样的类型
stu2 = Stu(stu1)
fmt.Println(stu1, stu2)
}
struct的每个字段上,可以写上一个标签tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
type Student struct {
Name string `json:"name"` // 这里就是结构体的标签tag
Age int `json:"age"`
}
func main() {
// 1.创建一个student变量
student := Student{"张三", 18}
// 2.将monster变量序列化为json格式字符串
jsonStr, err := json.Marshal(student) // 这里json.Marshal底层用到了反射
if err != nil {
fmt.Println("jsonStr报错")
}
fmt.Println("jsonStr:", jsonStr)
fmt.Println("string(jsonStr):", string(jsonStr))
}
如果这里不加上标签,生成的json格式,就变成:string(jsonStr): {"Name":"张三","Age":18}
会发现,这里的Name的首字母是大写,这又是不可避免,因为小写就调用不了
所以这里通过标签,设置一个别名,底层用了反射解决这个问题~~~
输出结果:string(jsonStr): {"name":"张三","age":18}
在某些情况下,我们需要声明(定义)方法。比如Person结构体:除了有一些字段外(年龄,姓名…),Person结构体还有一些行为。比如:可以说好,跑步,学习等,还可以做算术题。这时就要用方法才能完成
Golang中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅只是struct。
type A struct {
Num int
}
func (a A) test() {
fmt.Println(a.Num)
}
对上面的语法说明:
type Person struct {
Name string
}
func (p Person) test() {
p.Name = "Tom"
fmt.Println("test():", p.Name)
}
func main() {
person := Person{Name: "张三"}
person.test() // 调用方法
fmt.Println("main p.Name=", person.Name)
}
输出结果:
test(): Tom
main p.Name= 张三
对上面的总结:
test方法和Person类型绑定
test方法只能通过Person类型的遍历来调用,而不能直接调用,也不嫩更实用其他类型变量来调用。
func (p Person) test() {} 这个p是它的副本,进行的是值传递,与函数很像。
给Person结构体添加speak 方法,输出 xxx是一个好人
type Person struct{
Name string
}
//给Person结构体添加speak 方法,输出 xxx是一个好人
func (p Person) speak() {
fmt.Println(p.Name, "是一个goodman~")
}
给Person结构体添加jisuan 方法,可以计算从 1+…+1000的结果,
type Person struct {
Name string
}
func (p Person) jisuan() {
res := 0
for i := 1; i <= 1000; i++ {
res += i
}
fmt.Println(p.Name, "计算的结果是=", res)
}
func main() {
p := Person{Name: "张三"}
p.jisuan() // 输出:张三 计算的结果是= 500500
}
给Person结构体jisuan2 方法,该方法可以接收一个参数n,计算从 1+…+n 的结果
type Person struct {
Name string
}
// 给Person结构体jisuan2 方法,该方法可以接收一个参数n,计算从 1+..+n 的结果
func (p Person) jisuan2(n int) {
res := 0
for i := 1; i <= n; i++ {
res += i
}
fmt.Println(p.Name, "计算的结果是=", res)
}
func main() {
p := Person{Name: "张三"}
p.jisuan2(10) // 输出:张三 计算的结果是= 55
}
给Person结构体添加getSum方法,可以计算两个数的和,并返回结果
type Person struct {
Name string
}
// 给Person结构体添加getSum方法,可以计算两个数的和,并返回结果
func (p Person) getSum(n1 int, n2 int) int {
return n1 + n2
}
func main() {
p := Person{Name: "张三"}
sum := p.getSum(1, 1)
fmt.Printf("%v sum=%v", p.Name, sum) //输出:张三 sum=2
}
说明:方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当作实参也传递给方法。下面举例说明:
案例:
type Person struct {
Name string
}
// 给Person结构体添加getSum方法,可以计算两个数的和,并返回结果
func (p Person) getSum(n1 int, n2 int) int {
return n1 + n2
}
func main() {
p := Person{Name: "张三"}
n1 := 10
n2 := 20
res := p.getSum(n1, n2)
fmt.Println("res=", res)
}
上面这些注意事项都比较简单,就代码展示一下最后一条:
type Student struct {
Name string
Age int
}
// 给*Student实现方法String()
func (stu *Student) String() string {
str := fmt.Sprintf("Name=[%v] Age=[%v]", stu.Name, stu.Age)
return str
}
func main() {
//定义一个Student变量
stu := Student{
Name: "tom",
Age: 20,
}
//如果你实现了 *Student 类型的 String方法,就会自动调用
fmt.Println(stu)
fmt.Println(&stu)
}
输出结果:
{tom 20}
Name=[tom] Age=[20]
这一点很容易迷糊,下面用段代码解释一下:
type Person struct {
Name string
}
//函数
//对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
func test01(p Person) {
fmt.Println(p.Name)
}
func test02(p *Person) {
fmt.Println(p.Name)
}
//对于方法(如struct的方法),
//接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
func (p Person) test03() {
p.Name = "jack"
fmt.Println("test03() =", p.Name) // jack
}
func (p *Person) test04() {
p.Name = "mary"
fmt.Println("test03() =", p.Name) // mary
}
func main() {
p := Person{"tom"}
test01(p)
test02(&p)
p.test03()
fmt.Println("main() p.name=", p.Name) // tom
(&p).test03() // 从形式上是传入地址,但是本质仍然是值拷贝
fmt.Println("main() p.name=", p.Name) // tom
(&p).test04()
fmt.Println("main() p.name=", p.Name) // mary
p.test04() // 等价 (&p).test04 , 从形式上是传入值类型,但是本质仍然是地址拷贝
}
输出结果:
tom
tom
test03() = jack
main() p.name= tom
test03() = jack
main() p.name= tom
test03() = mary
main() p.name= mary
test03() = mary
从代码会发现,仅管传递的是一个地址,但是编译器进行了内部优化,实际上还是值传递,只是支持这种写法,但并不是进行一个地址值的修改。
说明:Golang的结构体没有构造函数,通常可以使用工厂模式来解决这个问题
相当于,这个工厂模式,就是以前构造函数的功能。
看个需求:
一个结构体的声明如下:
package model
type Student struct {
Name string
}
因为这里的 Student 的首字母S是大写的,如果我们想在其他包串接Student的实例(比如:main包),引入model包后,就可以直接创建Student结构体的变量(实例)。
但是问题来了,如果搜字母是小写的,比如是: type student struct {…}就不行了,咋办?-》工厂模式来解决,
Model包
package model
type student struct {
name string
score float64
}
// 因为student结构体首字母是小写,因此是只能在model使用
// 通过工厂模式来解决
func NewStudent(n string, s float64) *student {
return &student{
name: n,
score: s,
}
}
// 如果score字段首字母小写,则,在其他包不可以直接方法,我们可以提供一个方法
func (s *student) GetScore() float64 {
return s.score
}
// 如果score字段首字母小写,则,在其他包不可以直接方法,我们可以提供一个方法
func (n *student) GetName() string {
return n.name
}
main包
import (
"GoStudy_Day1/model"
"fmt"
)
func main() {
// 定student结构体是首字母小写,我们可以通过工厂模式来解决
stu := model.NewStudent("Tom", 88.8)
fmt.Println(stu)
fmt.Println(*stu)
// fmt.Println("name=", stu.name) // 报错,因为是私密的
fmt.Println("name=", stu.GetName())
fmt.Println("score=", stu.GetScore())
}
输出结果:
&{Tom 88.8}
{Tom 88.8}
name= Tom
score= 88.8
其实,这就看出来,就是Java的GetSet,绕了一圈,又回来啦~~~!!!!!