go基础第三篇:结构体、函数、方法、接口

struct

结构体struct就相当于java中的类class,用于定义属性和方法。

定义一个Person,有string型的Name和int型的Age两个属性:

type Person struct {
    Name string
    Age  int
}

创建一个Person实例:

第一种方式:

var p Person = Person{}

这时,所有属性值都是零值。

直接打点给属性赋值:

p.Name = "zhangsan"

p.Age = 18

var p Person = Person{"zhangsan", 18}

一个个把属性值列出来,要求一个都不能少。

第二种方式:

var p Person = Person{Name: "zhansan", Age: 18}

这时,Name属性值为zhangsan,Age属性值为18

把属性名与值一对对列出来,可以只列需要的属性,其他的属性值为零值。注意,属性名没有用双引号括住。

第三种方式:

var pptr *Person = new(Person)

注意,与上面两种方式不同,用new函数生成的是指向实例的指针,注意,不是new关键字,而是new函数,java中是new关键字。new(Person)相当于&Person{},返回都是指针类型。

我们可以用reflect.TypeOf()函数来查看变量类型

func main() {
        a := Person{"张三", 90}
        log.Println(reflect.TypeOf(a))

        var pptr *Person = new(Person)
        log.Println(reflect.TypeOf(pptr))
}

会打印出

main.Person

*main.Person

可以看出,a是Person类型,pptr是指针类型,类型名以星号开头。

var p Person = new(Person)会报编译错误,提示Cannot use 'new(Person)' (type *Person) as type Person。

我们既可以通过实例访问其成员变量,又可以直接通过指向该实例的指针访问其成员变量

如pptr.Name = "lisi"

fmt.Println(pptr.Name)

定义一个Employee,有Person型的p和int型的salary两个属性:

type Employee struct {
    p      Person
    salary int
}

创建一个Employee实例:

var e Employee = Employee{}

var e Employee = Employee{p: Person{}, salary: 10000}

var eptr *Employee = new(Employee)

如果属性类型是自定义的struct的话,属性名可以省略,如下:

type Employee struct {
    Person
    salary int
}

此时创建一个Employee实例:

var e Employee = Employee{}

var e Employee = Employee{Person: Person{}, salary: 10000}

这里由于属性名省略了,所以花括号中的key只能是我们自定义的struct了。

var eptr *Employee = new(Employee)

通过e访问Name、Age属性:

在属性名不省略时,只能通过e打点获取Person实例,然后Person实例再打点操作Name、Age属性,示例如下:

func main() {
    var e Employee = Employee{p: Person{}, salary: 10000}
    e.p.Name = "zhangsan"
    fmt.Println(e)
}

在属性名省略时,我们既可以通过e打点获取Person实例,然后Person实例再打点操作Name、Age属性,也可以直接e打点操作Name、Age属性,这就有点像java的继承了。示例如下:

func main() {
    var e Employee = Employee{Person: Person{}, salary: 10000}
    e.Person.Name = "zhangsan"
    e.Age = 30
    fmt.Println(e)
}

函数

在go中,函数是一等公民。

函数可以有不定长入参,可以有多个返回值,可以赋值给变量,可以作为函数的入参和出参。

函数定义:func func_name(i int, s string) int {}

定义函数f0,有一个int型入参、无出参:

func f0(i int) {

}

定义函数f1,有一个int型入参、一个string型入参,一个int型出参:

func f1(i int, s string) int {
    return 0
}

定义函数f2,有两个int型入参,一个string型出参:

func f2(i, j int) string {
    return ""
}

如果多个入参类型一样的话,可以省略前面几个参数的类型关键字,而只保留最后一个参数的类型关键字。

定义函数f3,有两个int型入参,一个string型出参,一个int型出参:

func f3(i, j int) (string, int) {
    return "", 0
}

多个出参的话,要用括号包起来。

定义函数f4,有不定长个int型入参,一个int型出参:

func f4(p ...int) int {
    return len(p)
}

定义函数f5,有一个string型入参和不定长个int型入参,一个int型出参:

func f5(s string, sl ...int) int {
    return len(s) + len(sl)
}

函数赋值给变量

func main() {
    var f func(int, string) int = f1
    f(1, "a")
}

变量的类型可以通过fmt.Println(reflect.TypeOf(f))打印出来看。

函数作为函数的入参和出参

func S(f func(i int) int) func(s string) string {
    return func(s string) string {
        return strconv.Itoa(f(len(s)))
    }
}

func main() {
    var p = S(func(i int) int {
        return i + 10
    })("100")
    fmt.Println(p)
}

我们在定义函数时,如果入参是一个struct实例,则底层会复制一个struct实例作为入参,函数对实例的改动不会影响原来的struct实例。所以,入参最好是指向struct实例的指针,这样就不用复制了,函数对实例的改动也会体现到原来的struct实例上。示例如下:

func changeName(e Person) {
    e.Name = "zhangsan"
}

func changeNamePtr(e *Person) {
    e.Name = "zhangsan"
}

func main() {
    var p Person = Person{}
    changeName(p)
    fmt.Println(p)

    var ptr *Person = new(Person)
    changeNamePtr(ptr)
    fmt.Println(ptr)
}

方法

方法和函数长得差不多,区别是方法定义时func关键字后面紧跟的是括号,括号里面是调用者形参及调用者类型,之后才是func_name,再之后是括号,括号里面是入参形参及入参类型(没有入参的情况下括号不可以省略),最后是出参类型,如果有多个出参,则出参类型要用括号括住。

从上面描述可以看出,在方法定义时,方法的调用者类型就已经是确定了的,只有这个调用者类型的实例或者指针才能调用这个方法。方法定义时的调用者类型既可以是普通类型,也可以是指针类型,方法最终的调用者也是既可以是普通类型,也可以是指针类型。

示例如下:

type JavaProgrammer struct {
}

func (jp *JavaProgrammer) helloWorld() {
    fmt.Println("System.out.println(\"Hello World!\");")
}

type GoProgrammer struct {
}

func (gp GoProgrammer) helloWorld() {
    fmt.Println("fmt.Println(\"Hello World!\")")
}

func main() {
    new(JavaProgrammer).helloWorld()
    javaProgrammer := JavaProgrammer{}
    javaProgrammer.helloWorld()

    new(GoProgrammer).helloWorld()
    goProgrammer := GoProgrammer{}
    goProgrammer.helloWorld()

}

JavaProgrammer的helloWorld方法是调用者类型定义成指针类型的方法,第一次调用是用指向JavaProgrammer实例的指针调用,第二次调用是用JavaProgrammer实例调用。

GoPrammer的helloWorld方法是调用者类型定义成普通类型的方法,第一次调用是用指向GoProgrammer实例的指针调用,第二次调用是用GoProgrammer实例调用。

相比来说,调用者类型定义成指针类型的方法要比定义成普通类型的方法好,因为在调用时可以避免内存复制。

func main() {
    a := Person{"张三", 90}
    log.Println(1, unsafe.Pointer(&a.Name))
    a.Ps()
    a.Ps2()
}

func (e Person) Ps() {
    log.Println(0, unsafe.Pointer(&e.Name))
}

func (e *Person) Ps2() {
    log.Println(0, unsafe.Pointer(&e.Name))
}

在Ps方法内部打印的入参a的Name属性地址和直接打印实例a的Name属性地址不一样,而在Ps2方法内部打印的入参a的Name属性地址和直接打印实例a的Name属性地址一样,由此可以证明调用者类型定义成指针类型的方法在调用时,不会有内存复制,而调用者类型定义成普通类型的方法在调用时,会有内存复制。

func main() {
    a := Person{"张三", 90}
    log.Println(1, unsafe.Pointer(&a))
    a.Ps()
    a.Ps2()
}

func (e Person) Ps() {
    log.Println(0, unsafe.Pointer(&e))
}

func (e *Person) Ps2() {
    log.Println(0, unsafe.Pointer(&e))
}

打印出来的三个值各不相同,难道说形参是指针类型的方法在调用时,也会有内存复制?

func (p *Person) exchange(p0 *Person) {}

第一个括号中的p *Person表示本方法调用者只能是Person实例或者指向Person实例的指针,且是引用传递,如果在方法中改变了调用者的属性,会在方法外体现。

第二个括号中的p0 *Person表示方法入参是引用传递,如果在方法中改变了入参的属性,会在方法外体现。

方法的继承

还是以上面的Person、Employee举例。

假设Person有个changeName方法,定义如下:

func (pptr *Person) changeName(name string) {
    pptr.Name = name
}

假如在定义Employee时省略了Person类型属性的名称,则我们可以通过e直接打点调用changeName方法,示例1如下:

func main() {
    var e Employee = Employee{Person: Person{Name: "wanglaoji"}}
    e.changeName("lisi")
    fmt.Printf("%+v\n", e)
}

假设Employee也有个changeName方法,则直接通过e打点调用changeName方法的话,调用的其实是Employee的changeName方法,而不是Person的changeName方法。示例2如下:

func (pptr *Person) changeName(name string) {
    pptr.Name = name
}

func (eptr *Employee) changeName(name string) {
    eptr.Name = name + name
}

func main() {
    var e Employee = Employee{Person: Person{Name: "wanglaoji"}}
    fmt.Printf("%+v\n", e)
}

假如Person有个change方法,在change方法中调用了changeName方法,那么e调用change方法时,执行的是Employee的changeName方法呢,还是Person的changeName方法呢?示例3如下:

func (pptr *Person) changeName(name string) {
    pptr.Name = name
}

func (pptr *Person) change(name string) {
    pptr.changeName(name)
}

func (eptr *Employee) changeName(name string) {
    eptr.Name = name + name
}

func main() {
    var e Employee = Employee{Person: Person{Name: "wanglaoji"}}
    e.change("lisi")
    fmt.Printf("%+v\n", e)
}

实测执行的是Person的changeName方法。

假如Employee也有个change方法,在change方法中调用了changeName方法,那么e调用change方法时,执行的是Employee的changeName方法呢,还是Person的changeName方法呢?示例4如下:

func (pptr *Person) changeName(name string) {
    pptr.Name = name
}

func (pptr *Person) change(name string) {
    pptr.changeName(name)
}

func (eptr *Employee) changeName(name string) {
    eptr.Name = name + name
}

func (eptr *Employee) change(name string) {
    eptr.changeName(name)
}

func main() {
    var e Employee = Employee{Person: Person{Name: "wanglaoji"}}
    e.change("lisi")
    fmt.Printf("%+v\n", e)
}

实测执行的是Employee的change方法和Employee的changeName方法。

总结:

在go中没有继承,不论是属性还是方法。都是太任性的省略搞的鬼。

在go中没有静态方法的概念。

接口

定义格式:

type RedPacketService interface {
    add(s string)
    delete(s string)
    update(s string)
    query(s string) string
}

go中没有implements或者相同作用的关键字。要实现某个接口,不是在定义struct的时候显式声明要实现某个接口,而是采用duck typing的方式,即只要实现了接口的所有方法,就认为这个struct实现了这个接口,就可以向上转型。示例如下:

type Programmer interface {
    helloWorld()
}

type JavaProgrammer struct {
}

func (jp *JavaProgrammer) helloWorld() {
    fmt.Println("System.out.println(\"Hello World!\");")
}

type GoProgrammer struct {
}

func (gp GoProgrammer) helloWorld() {
    fmt.Println("fmt.Println(\"Hello World!\")")
}

func main() {
    var p Programmer = new(JavaProgrammer)
    p.helloWorld()

    p = new(GoProgrammer)
    p.helloWorld()
}

Programmer接口只有一个helloWorld方法,JavaProgrammer实现了这个方法,所以JavaProgrammer实现了Programmer接口,同理,GoProgrammer也实现了Programmer接口。

这里有一点需要注意,JavaProgrammer是pointer receiver实现,GoProgrammer是value receiver实现,而实现接口的方法时,pointer receiver和value receiver是不一样的,参考go官方文档https://golang.org/doc/effective_go.html#pointers_vs_values

The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers.

也就是说,value receiver方式实现的方法可以被实例和指向实例的指针调用,而pointer receiver方式实现的方法只能被指向实例的指针调用。

上例中,直接用JavaProgrammer{}生成一个实例,然后赋值给p会报编译错误,提示Cannot use 'JavaProgrammer{}' (type JavaProgrammer) as type Programmer. Type does not implement 'Programmer' as 'helloWorld' method has a pointer receiver.

假如再给Programmer接口添加一个hi方法,由于JavaProgrammer和GoProgrammer都没有实现这个方法,所以这两个struct都没有实现Programmer接口,所以把指向JavaProgrammer实例的指针或指向GoProgrammer实例的指针赋值给Programmer类型变量时会报编译错误。

接口最佳实践:

使用小的接口定义,接口中方法数不要太多;

较大的接口定义,由多个小接口定义组合而成;

只依赖于含必要功能的最小接口

type Reader interface {
    read() string
}

type Writer interface {
    write()
}

type ReaderWriter interface {
    Reader
    Writer
}func getMessage(reader Reader) string {
    return reader.read()
}

func getMessage2(readerWriter ReaderWriter) string {
    return readerWriter.read()
}

如上例,Reader接口提供read()方法,Writer接口提供write()方法,而ReaderWriter接口是由Reader接口和Writer接口组合起来的。函数getMessage内部只需调用Reader的read()方法,那么形参类型只需是Reader,而不应该是比Reader接口功能更多的ReaderWriter接口。

话说ReaderWriter接口和Reader接口是啥关系呢?是ReaderWriter接口继承了Reader接口,还是说Reader接口是ReaderWriter接口的成员变量?

空接口 interface{}

interface{}可以用作任意类型的形参,示例如下:

func PX(s interface{}) {
    if s0, ok := s.(int); ok {
        fmt.Println("int=", s0)
    } else if s0, ok := s.(string); ok {
        fmt.Println("string=", s0)
    } else if s0, ok := s.(bool); ok {
        fmt.Println("bool=", s0)
    } else {
        fmt.Println("unknown type")
    }
}

func main() {
    PX("1")
    PX(1)
    PX(false)
    PX(new(Programmer))
}

以上,s可以是任意类型变量。s.(int)有2个返回值,第一个返回值是s,第二个返回值是true或者false,如果s是int型变量,就是true,否则就是false。所以通过判断第二个返回值是否是true,就能判定s是否是int型变量。

以上写法还可以通过switch来简化判断,如下:

func PX(s interface{}) {
    switch s.(type) {
    case int:
        fmt.Println("int=", s)
    case string:
        fmt.Println("string=", s)
    case bool:
        fmt.Println("bool=", s)
    default:
        fmt.Println("unknown type")
    }
}

func main() {
    PX("1")
    PX(1)
    PX(false)
    PX(new(Programmer))
}

s.(type) 必须跟在switch后面,否则会报编译错误。

error

error是go自带的一个接口,只有一个Error()方法,没有入参,有一个string类型的出参。

go自带了很多实现了error接口的struct,如errorString、TimeoutError等。

errorString只有一个string型的属性,我们可以通过errors包的New(text string)函数获取指向errorString实例的指针。示例如下:

func Check(i int) (int, error) {
    if i < 0 {
        return -1, errors.New("negative")
    } else {
        return 1, nil
    }
}

func main() {
    i, err := Check(-1)
    if err != nil {
        fmt.Println("something wrong")
    } else {
        fmt.Println("going on,i=", i)
    }
}

error实例能否用==比较?

github.com/pkg/errors是一个比较好用的error相关的包。