go 没有原生的 “类”的概念,也不支持继承等OOP的概念;Go通过结构体内嵌再配合接口比OOP有更好的灵活性和扩展性
go 中有string int float bool
等基本类型,可以使用type
来自定义类型。自定义类型就是定义了一个新的类型。比如 type MyInt int
就是定义了一个新的类型MyInt
,它具有int
的特性
其实就是给一个类型又取了一个名字,格式:
type TypeAlias = Type
比如 rune
和byte
就是别名:
type rune = int32
type byte=uint8
基础数据类型可以用来描述简单属性,比如var color string
,类型string
描述了属性color
。当我们需要描述复杂属性(多个属性)时,基础数据类型就会不够用了。在java中,我们使用类 class
去封装一组属性(和行为),在go中,也有接近的概念struct
。
结构体的定义:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
举个栗子【这和 java 的类何其相似!】:
type person struct {
name string
city string
age int8
}
这里还要特别注意一点:结构体是值类型.比如看下面这个例子:
这倒是和Java对象 传递引用不太相同。可以记一下:GO语言中传参总是副本。
type person struct {
name string
age int
}
func fn4(){
p := person{
name: "air",
age: 1,
}
fmt.Println(p) // {air 1}
changeName(p) // 相当于只是修改了 副本
fmt.Println(p) // {air 1}
}
func changeName(s person){
s.name= "bee"
}
make 或 new
去实例化一个struct
。var p Person
这样声明结构体类型。func fn6(){
var p Person
p.age=10
p.name="justin"
p.city="GZ"
fmt.Printf("%v \n", p)
p.city = "BJ"
fmt.Printf("%v \n", p)
}
func fn7(){
// 匿名结构体
var d struct{color string;age uint8}
d.color = "yellow"
d.age = 1
fmt.Println(d)
}
可以通过 new
来初始化一个结构体,返回的是一个 指针
func fn8() {
// p2 is a struct pointer
var p2 = new(Person)
fmt.Printf("%T \n", p2)
fmt.Printf("%#v ", p2)
}
通过 &
来实例化一个结构体
func fn9(){
var p3 = &Person{} // 对一个结构体 取址操作,相当于对这个 结构体进行了实例化,但是并没有初始化, p3 中的所有属性都是 零值
fmt.Printf("%T \n", p3)
fmt.Printf("%#v \n", p3)
p3.age=10
p3.city="GZ"
p3.name="justin"
fmt.Printf("%#v \n", p3)
}
有人可能对 p3.age=10
的表达感到困惑: p3 是一个 指针,那为啥可以直接执行p3.age
呢,正常逻辑不应该是*p3.age=10
吗?
这其实 这是Go的一个语法糖,本质上其实是 : (*p3).age = 10
这里,要稍微注意一下,实例化 和 初始化 还是两个分开的概念。
1、使用 KV对对结构体进行初始化, k – 结构体的属性 ,v – 字段的值
func fn10(){
var p4 = Person{
name:"jim",
age:10,
city:"BZ",
}
fmt.Printf("%#v ", p4)
}
2、使用&
取址来对一个结构体进行初始化。假如一些字段没有初始值,没有初始值的字段的值就是 零值。&
相当于对该结构体进行了一次new 实例化操作。
func fn11() {
var p5 = &Person{ // 使用取址对一个结构体进行初始化
name: "pat", //这其实是一个语法糖 ,底层是 (*p5).name="pat"
age: 11,
}
fmt.Printf("%#v \n", p5) //&main.Person{name:"pat", age:11, city:""}
fmt.Printf("%T \n", p5) // *main.Person
fmt.Prinfg("%v \n",p5.name)
}
3、使用值的列表初始化
这种方式:
go中的结构体占用一块连续的内存,而空结构体是不占用内存的
比如,这个 test
结构体,其初始化后,字段占用的内存都是对齐的。
type test struct{
a int8;
b int8;
c int8;
d int8
}
func fn12(){
var t = test{
a:1,
b:1,
c:1,
d:1,
}
//t.a 0xc0000a2058
//t.b 0xc0000a2059
//t.b 0xc0000a205a
//t.b 0xc0000a205b
fmt.Printf("t.a %p \n", &t.a)
fmt.Printf("t.b %p \n", &t.b)
fmt.Printf("t.b %p \n", &t.c)
fmt.Printf("t.b %p \n", &t.d)
}
看这个case:空结构体是不占用空间的。
func fn13() {
a:= empty{}
fmt.Println(unsafe.Sizeof(a)) //0 【这里的 unsafe.Sizeof()?】
}
type empty struct{
}
在Java中,类初始化是需要执行构造函数的。但是在 go 中并没有原生的构造函数概念,不过,我们可以自己动身定义一个 结构体的构造函数。这样做的最大好处在于:struct类型是值传递,传参struct会引发拷贝,当struct 较大时,性能有损。
func fn15(){
i := newStudent("dog", 1)
fmt.Println(*i) //{dog 1}
fmt.Println(i) //&{dog 1}
}
// 这是一个 构造函数(自定义的),返回的是 指针
func newStudent(name string, age int) *student{
return &student{
name:name,
age: age,
}
}
type student struct {
name string
age int
}
this 或 self
func fn16() {
s := newStudent("ace", 10) // 把 s 当成一个java对象
e := s.learn("english") // learn()像是一个 java 方法
fmt.Println(e)
}
// 方法的声明
// 按照官方的推荐,接收者 s 应该是 类型的首字母小写
// (s student) 中的 student 是说 接收者类型为 student
// 接收者类型,既可以是 指针,也可以是非指针类型
func (s student) learn(name string) string {
fmt.Println(s.name, " --> ", name)
return name
}
func newStudent(name string, age int) *student {
return &student{
name: name,
age: age,
}
}
type student struct {
name string
age int
}
指针类型的receiver
由一个结构体的指针组成;通过指针调用方法,可以修改指针对应的实例的属性值。这种场景下, 越发显得 receiver
和 java中的this
接近。
func fn17() {
s := newStudent("amy", 11)
s.setAge(2)
fmt.Println(s.age)
}
// receiver的类型是一个 指针
func (s *student) setAge(age int) {
s.age = age //和java的 setter 何其相似,s *student 和 java 的this 多像
}
//结构体的构造函数,返回的是 一个 指针
func newStudent(name string, age int) *student {
return &student{
name: name,
age: age,
}
}
type student struct {
name string
age int
}
当方法作用于值类型receiver
时,Go语言会在代码运行时将receiver
值复制一份。在值类型receiver
的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
func fn18(){
s:=newStudent("bob",12)
fmt.Println(s.age) //12
s.setAge2(20) // here it does not work
fmt.Println(s.age) //12
}
func (s student) setAge2(age int) {
s.age = age
}
//结构体的构造函数,返回的是 一个 指针
func newStudent(name string, age int) *student {
return &student{
name: name,
age: age,
}
}
type student struct {
name string
age int
}
方法不是 结构体类型的专利,go中任意类型都可以添加方法。 值得注意的是: 非本地类型不能定义方法,即:不能给别的包的类型定义方法。
// MyInt 是 自定义类型
// 对 基础类型 添加方法,感觉有点像是 Java中的 包装类
type MyInt int
func (m MyInt)foo(){
fmt.Println("foo...")
fmt.Printf("%v \n", m)
}
func fn19(){
var i MyInt
i = 8
i.foo() //打印了 8
}
声明结构体时,我们可以只提供 字段类型,不提供字段名。本质上,go提供了和字段类型同名的字段名。不过个人不是很建议采用这种做法,这样会造代码不是很清晰。
type Dog struct{
string
int
}
func fn20(){
d := Dog{
"boy",11,
}
fmt.Println(d) //{boy 11}
}
学到这里,就会发现,go的结构体和java的类 十分类似了,结构体就像是go提供的OOP 解决方案。
所谓的嵌套结构体,就是 一个结构体的字段类型并不是一个基础类型,而是另一个结构体类型。
如果再仔细点,就会发现下面示例中的 User
实例和 JSon
数据结构有着很大的相似性。实际会不会有啥关联呢?我们且看。
type Address struct {
Province string
City string
County string
}
type User struct{
Name string
Gender string
Address Address
}
func fn22(){
u := User{
Name:"ht",
Gender:"male",
Address:Address{ // 注意这里的语法的 第二个的 Address
Province:"GD",
City:"GZ",
County:"HZ",
},
}
fmt.Println(u)
}
嵌套匿名字段:只有字段类型,没有字段名(实际上将字段类型作为了字段名)
当嵌套结构体内部存在重名字段时,为了避免歧义,需要指定具体的内嵌结构体字段名。
go 使用了“组合”的方式来继承,而不是Java那样使用了 extend 关键字。
type Animal struct{
name string
}
func (a *Animal) move(){
fmt.Printf("%v moves \n", a.name)
}
type Cat struct{
Color string
*Animal // 匿名字段 ,注意这里是指针.同时注意,匿名字段,匿掉的是 字段,不是类型
}
func (c *Cat) showColor(){
fmt.Printf("the cat is of color:%v",c.Color)
}
func fn23(){
c := &Cat{
Color: "yellow",
Animal: &Animal{ // 注意这里要 传入指针变量
name: "lily",
},
}
fmt.Printf("cat is:%v \n",c.name)
c.move() //这是个继承来的方法
c.showColor() // 这是 原本自身的方法
}
结构体字段首字母是大写开头,表示可以公开访问;小写访问,则仅包内可见。坦白说,这种设计有点囧。大写还是小写,其实是很容易笔误的。
func fn1(){
c:= &Class{
Title: "class-1",
Students: make([]*Student, 0, 30), // 初始化一个切片
}
for i := 0; i < 2; i++ {
s := Student{
ID:c.Title + strconv.Itoa(i),
Gender: "male",
Name: fmt.Sprintf("stu%02d",i),
}
c.Students = append(c.Students, &s)
}
fmt.Printf("%T \n", c) //*main.Class
data,ex := json.Marshal(c)
if ex !=nil {
fmt.Println(ex)
}
fmt.Printf("json值类型 %T \n", data) // []uint8
fmt.Printf("json内容:%s \n",data)
jsonStr:= `{"Title":"class-1","Students":[{"ID":"class-10","Gender":"male","Name":"stu00"},{"ID":"class-11","Gender":"male","Name":"stu01"}]}`
c1 := &Class{}
ex1 := json.Unmarshal([]byte(jsonStr), c1) // []byte(jsonStr) 这样去取字符串对应的byte数组
if ex1 !=nil {
fmt.Println(ex1)
}
fmt.Printf("反序列化结果%#v\n", *c1)
}
type Student struct{
ID string
Gender string
Name string
}
type Class struct{
Title string
Students []*Student // 这是一个切片,元素类型是 指针
}
结构体标签tag
是结构体的元信息,运行时可以反射拿出来。tag 在结构体字段后面定义,如 【 key1:"value1" key2:"value2"
】
注意:
func fn2(){
c := Cat{
name: "amy",
Color: "pink",
ID: 1024,
}
data,error := json.Marshal(c)
// {"Color":"pink","id":1024}
// 1. id 变成了 小写
// 2. name 没被序列化
// 3. 默认的情况下,字段名(Color)变成了json的key
if error != nil {
fmt.Println("json parsed failed",error)
return
}
fmt.Printf("json result:%s \n",data)
}
type Cat struct{
name string // 这个是私有字段,因为字段的首字母是 小写的。私有字段仅当前包内可见,因此json包不能访问
Color string // json 的序列化默认是使用字段名作为key
ID int `json:"id"` // 通过指定tag实现json序列化该字段时 的 key
}
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此复制时要特别注意。
type Pig struct {
name string
age int8
dreams []string
}
func (p *Pig) setDreams(dreams []string) {
p.dreams =dreams
}
func fn3() {
p := Pig{
name: "bob",
age: 1,
}
dreams:=[]string{"sleep", "eat"}
p.setDreams(dreams)
fmt.Println("pig:",p) // {bob 1 [sleep eat]}
// 这里我们修改了 切片的(底层数组)的值,导致数组元素变化了,但这 未必是预期的
// 正确的做法:在方法中传入slice的拷贝对结构体进行赋值
dreams[0] = "sleep more"
fmt.Println("pig2:", p) // {bob 1 [sleep more eat]}
}
正确的写法:
type Pig struct {
name string
age int8
dreams []string
}
func (p *Pig) setDreams(dreams []string) {
p.dreams = make([]string,len(dreams))
copy(p.dreams,dreams)
}
func fn3() {
p := Pig{
name: "bob",
age: 1,
}
dreams:=[]string{"sleep", "eat"}
p.setDreams(dreams)
fmt.Println("pig:",p) // {bob 1 [sleep eat]}
dreams[0] = "sleep more" // 即使这里我们修改了 切片的(底层数组)的值,也不会导致数组元素变化
fmt.Println("pig2:", p) // {bob 1 [sleep eat]}
}
Go中的 接口是一组 method的集合,是duck type programming
的一种实现。接口不关心属性(数据),只关心行为(方法)
Go的接口本质上也是一种类型。
func fn5(){
c:=Cat{}
d:=Dog{}
fmt.Printf("cat :%v \n",c.Say())
fmt.Printf("dog :%v \n",d.Say())
}
type Cat struct{}
func (c Cat) Say() string{
return "meow"
}
type Dog struct{}
func (d Dog) Say() string{
return "www"
}
这个demo,Dog
和 Cat
都是有 Say()
的行为(方法),代码重复。如何把这种Say
的行为抽象出来?这就是接口的意义了。
这种场景随处可以:
接口区别于Go中的其他类型,接口是一种抽象类型。看到一个接口的值,我们不知道接口它是啥,只知道它能做啥
Go提倡 面向接口编程。
接口定义如:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
duck type programming
的体现。er
表示 某种行为的实施者。看例子中的writer
接口,说明对象只要实现了Write
方法,那就是Write
行为的实施者举个栗子:
type writer interface{
Write([]byte) error
}
当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。 Go的这种设计,具有超大的自由度。可以看到,对象和接口其实并没有语法上的关联。在Java中,一个类需要实现接口,这个类的实例(对象)才和接口发生关系。
type sayer interface{
say()
}
type dog struct{}
type cat struct{}
func (d dog) say(){ //dog 有 say()方法,相当于实现了sayer接口
fmt.Println("dogggg")
}
func (c cat) say(){ //cat 有 say()方法,相当于实现了sayer接口
fmt.Println("catttt")
}
如果只是看上一节的例子,其实并不感觉到接口有啥了不起的。。接口的作用在于:接口类型变量能够接收所有实现这个接口的实例。
func fn6(){
var x sayer
a:=cat{}
b:=dog{}
x = a
x.say()
x = b
x.say()
}
上个例子:
type Mover interface{
move()
}
type dog struct{}
func (d dog)move(){
fmt.Println("dog moves")
}
func fn7(){
d1:=dog{} // 值
d2:=&dog{} //指针
var x Mover //x 既可以接收 值,也可以接收指针
x = d1
x.move()
x = d2 // 对指针变量求值有语法糖,dog指针内部会自动求值 *dog
x.move()
}
func (d *dog)move(){
fmt.Println("dog moves..")
}
func fn7(){
d1:=dog{}
d2:=&dog{}
var x Mover
x = d1 //d1 是dog 类型, 这里会报错。x 不能接收 dog 类型
x.move()
x = d2 // d2 是 dog指针类型,x 能接收 dog指针类型
}
总结一下就是:
func fn8(){
d:= dryer{}
h:= haier{d}
var x WashingMachine
x= h
x.dry() // WashingMachine并不直接实现 dry() 方法
x.wash()
}
type WashingMachine interface {
wash()
dry()
}
type dryer struct{}
func (d dryer) dry() {
fmt.Println("drying...")
}
type haier struct {
dryer
}
func (h haier) wash() {
fmt.Println("hair wash...")
}
接口嵌套可以创造出新的接口。
demo略
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量.
空接口有啥作用?
1、 空接口作为函数的参数(空接口实现可以接收任意类型的的函数参数)
// 空接口能接收所有类型的参数
func show(a interface{}){
fmt.Printf("type:%T value:%v \n", a,a)
}
2、空接口作为map的值 (使用空接口实现可以保存任意值的字典)
func fn9(){
// 空接口能接收任意类型的 map 值
var m =make(map[string]interface{})
m["name"]="amy"
m["age"]=10
m["female"]=true
fmt.Println(m)
}
instance
关键字类似。func fn11() {
var w interface{} // w 是一个接口值
w = "amy"
//利用空接口进行类型推断。需要记住这种语法的返回参数,第一个是值,第二个是推断结果
v, ok := w.(string)
if ok{
fmt.Println(v)
}else {
fmt.Println("failed")
}
}
接口值 = 具体类型(动态类型)+ 具体类型的值(动态值)组成。在为接口值赋不同的具体类型的值时,动态类型和动态值会变化