Go语言中没有class
的概念,只有结构体struct
,但我们可以为结构体定义函数(在Java/C/C++中通常叫方法或是成员函数),因此也可以实现面向对象。
Go语言中结构体写法也是倒过来的,而且要加个type
关键字。
type Vector2 struct {
X int
Y int
}
实际上,Go中的type
有点类似C语言中的typedef
,用于实现自定义类型。上面定义我们可以这样理解:我们声明了一个struct { ... }
结构体类型,并为其起了别名叫Vector2
。此外,结构体成员字段名称的首字母大小写决定了该成员的可见性(访问范围):
首字母大写:公开字段,结构体成员对包外可见。
首字母小写:私有字段,结构体成员对包外不可见,但包内可以访问。
使用如下写法初始化一个结构体,初始化后我们可以直接打印输出。
package main
import "fmt"
type Vector2 struct {
X int
Y int
}
func main() {
var vec Vector2 = Vector2{3, 5}
fmt.Println(vec)
}
输出结果如下。
{3 5}
初始化时,我们也可以指明字段的值。
var vec Vector2 = Vector2{2, 3}
我们也可以不指明任何值,结构体的成员变量会初始化为默认值。
var vec Vector2 = Vector2{}
实际上上面一行代码也可以简写为自动类型推导的var vec = Vector2{}
或是vec := Vector2{}
,这里就不多介绍了。
Go语言提供了new
关键字,用它初始化一个结构体可以返回一个指向空结构体的指针,这里所谓的空结构体就是其成员变量全部按照默认值进行初始化。
var vec *Vector2 = new(Vector2)
如果需要在初始化结构体时直接赋值,我们直接使用取地址符就可以。
var vec *Vector2 = &Vector2{2, 3}
这种写法也可以不指定任何值。
var vec *Vector2 = &Vector2{}
尽管Go语言有new
关键字,但是Go编译器会自动选择为变量分配栈内存还是堆内存,我们无法对其控制,也就是说Vector2{3, 5}
或new(Vector2)
分配的内存我们根本无法确定其在栈上还是在堆上,这显然会让大多数C/C++程序员感到无法理解。
实际上,以现在CPU的性能,我们的应用程序瓶颈根本不会卡在栈内存还是堆内存这种问题上,Go语言这么设计是更加为开发者不要花过多的精力在繁琐的语法细节上考虑,如果你觉得某个模块这样写不好,或不适合当前项目,大可以用C/C++去写。
Go语言中使用点号.
访问结构体对象的成员。
var vec Vector2 = Vector2{2, 3}
fmt.Println(vec.X, vec.Y)
注:即使是指针,也是直接用.
访问。
如下代码为Vector2
结构体定义了一个函数Abs()
,函数声明中v
其实就是具有该成员函数的对象。
type Vector2 struct {
X float64
Y float64
}
func (v *Vector2) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
这里注意v
是一个指针,我们建议结构体的成员函数都使用指针指向结构体,这样能够减小结构体内存拷贝的开销。在其它编程语言中,这个v
其实就是this
指针。
对于一些临时数据结构,在其它强类型语言中我们可能习惯于使用Map结构,Go语言中我们还可以使用匿名结构体,下面是一个例子。
package main
import "fmt"
func main() {
var vec struct {
x int
y int
}
vec.x = 1
vec.y = 2
fmt.Println(vec.x, vec.y)
}
根据Go语言的语法,以上代码也可以简写为如下形式。
package main
import "fmt"
func main() {
vec := struct {
x int
y int
}{1, 2}
fmt.Println(vec.x, vec.y)
}
Go语言中结构体可以嵌套,下面是一个简单的例子。
package main
import "fmt"
type Teacher struct {
TeacherId string
Name string
}
type Classroom struct {
RoomId string
Teacher Teacher
}
func main() {
room := Classroom{RoomId: "1", Teacher: Teacher{TeacherId: "1001", Name: "Tom"}}
fmt.Println(room.RoomId)
fmt.Println(room.Teacher.TeacherId)
}
代码中,Teacher
类型嵌套在Classroom
内,访问嵌套的属性时,我们需要指定嵌套类型。
下面例子中我们实现了结构体匿名嵌套。
package main
import "fmt"
type Dog struct {
Color string
Animal
}
type Animal struct {
Name string
}
func main() {
dog := Dog{Color: "black", Animal: Animal{Name: "dog"}}
fmt.Println(dog.Animal.Name)
fmt.Println(dog.Name)
fmt.Println(dog.Color)
}
匿名嵌套中,我们可以用变量.内嵌结构体类型名.字段
的形式访问内嵌结构体字段,也可以直接用变量.字段
访问,这是匿名嵌套和普通嵌套的一个主要区别。当然,这种访问方式可能存在字段名冲突问题,这个时候为了避免歧义我们就必须指定具体的内嵌结构体的字段。尽管Go语言不支持继承,但使用这种匿名嵌套结构体写法,我们可以实现类似“继承”的功能。
Go语言中结构体支持Tag属性,Tag的内容是一组键值对,它类似注释但又支持通过代码读取。实际上,结构体的Tag可以通过反射读取,下面是一个例子。
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
student := Student{"Tom", 18}
t := reflect.TypeOf(student)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
fmt.Println(jsonTag)
}
}
注意,结构体的Tag必须严格遵循key:"value"
的格式,Tag写错造成一些库的运行逻辑异常是比较难以定位的。
Go语言的很多内置库和第三方库都通过Tag来对结构体字段进行一些额外配置,比如Go语言的Json库就支持通过结构体的Tag配置字段名等信息,下面例子演示了Go语言标准库中json包的用法。
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
str, _ := json.Marshal(Student{"Tom", 18})
fmt.Println(string(str))
}