面向对象编程
Go语言的面向对象编程和其他语言有非常大的差别。
Go 是一种面向对象的语言吗?
是和不是。虽然 Go 有类型和方法,并允许面向对象的编程风格,但没有类型层次结构(继承)。Go 中的“接口”概念提供了一种不同的方法,我们认为这种方法易于使用,并且在某些方面更通用。还有一些方法可以将类型嵌入到其他类型中,以提供类似于(但不完全相同)子类化的东西。此外,Go 中的方法比 C++ 或 Java 中的方法更通用:它们可以为任何类型的数据定义,甚至是内置类型,例如普通的“未装箱”整数。它们不限于结构(类)。
此外,缺少类型层次结构使得 Go 中的“对象”感觉比 C++ 或 Java 等语言中的“对象”轻量级得多。
封装数据和行为
结构体定义
实例创建及初始化
type Employee struct {
Id string
Name string
Age int
}
func TestCreateEmployeeObj(t *testing.T) {
e := Employee{"0", "Bob", 20}
e1 := Employee{Name: "Mike", Age: 30}
e2 := new(Employee) // 返回指针
e2.Id = "2"
e2.Name = "Rose"
e2.Age = 22
t.Log(e)
t.Log(e1)
t.Log(e1.Id)
t.Log(e2)
t.Logf("e is %T", e)
t.Logf("e2 is %T", e2)
}
行为(方法)定义
-
第一种
func (e Employee) String() string { fmt.Printf("Address is %x", unsafe.Pointer(&e.Name)) fmt.Println() return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age) } func TestStructOperations(t *testing.T) { e := Employee{"0", "Bob", 20} fmt.Printf("Address is %x", unsafe.Pointer(&e.Name)) fmt.Println() t.Log(e.String()) }
所以这种写法会有复制的开销。
-
第二种
func (e *Employee) String() string { fmt.Printf("Address is %x", unsafe.Pointer(&e.Name)) fmt.Println() return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age) } func TestStructOperations(t *testing.T) { e := &Employee{"0", "Bob", 20} // 传递引用 fmt.Printf("Address is %x", unsafe.Pointer(&e.Name)) fmt.Println() t.Log(e.String()) }
更推荐这种。
接口(定义交互协议)
Go的接口和很多主流编程语言的接口有很大的区别。
Java代码的示例:
Duck Type式接口
Go语言的interface:
type Programmer interface {
WriteHelloWorld() string
}
type GoProgrammer struct {
}
func (g *GoProgrammer) WriteHelloWorld() string { // duck type 鸭子类型
return "Hello World"
}
func TestClient(t *testing.T) {
var p Programmer
p = new(GoProgrammer)
t.Log(p.WriteHelloWorld())
}
Go接口
接口变量
自定义类型
type IntConv func(op int) int
// 计算函数操作的时长
func timeSpend(inner IntConv) IntConv { // 以前的方法特别的长 我们可以用自定义类型做替换
// 类似装饰者模式,对原来的函数进行了一层包装
return func(n int) int {
start := time.Now()
ret := inner(n)
fmt.Println("time spend:", time.Since(start).Seconds())
return ret
}
}
扩展和复用(类似继承)
Go语言无法天然支持继承,但是又想要实现面向对象的特性。
即父类对象 使用子类对象初始化,那么该父类对象调用的函数就是子类实现的函数 ,从而满足LSP(子类交换原则)。
案例一: Go语言 支持扩展父类的功能,如下代码:
package oriented_test
import (
"fmt"
"testing"
)
// Pet 类
type Pet struct {
}
func (p *Pet) Speak(){ // Pet类的函数成员
fmt.Print("Pet speak.\n")
}
func (p *Pet) SpeakTo(host string) { // Pet类的函数成员
p.Speak()
fmt.Println("Pet SpeakTo ", host)
}
// Dog 扩展Pet的功能
type Dog struct {
p *Pet
}
// 扩展父类的方法
func (d *Dog) Speak(){
d.p.Speak()
}
// 扩展父类的方法
func (d *Dog) SpeakTo(host string) {
d.Speak()
fmt.Println("Dog Speakto ", host)
}
func TestDog(t *testing.T) {
dog := new(Dog)
dog.SpeakTo("Test dog")
}
以上测试代码的输出如下:
dog 的 SpeakTo 中调用了 dog 的 Speak,其中调用了 Pet 的 Speak,所以输出正常。
Pet 和 Dog 调用不会相互影响,完全由用户决定。
但是这和我们所想需要的不同,Pet 类有自己的方法,Dog 类有自己的方法,两者作用域完全不同。
这里Go语言推出了匿名嵌套类型,即 Dog 类不用实现自己的和 Pet 类同名的方法即可,通过在 Dog 类的声明中变更 Pet 成 员。
匿名嵌套类型
案例二: Go语言支持匿名函数类型
// Dog 扩展Pet的功能
type Dog struct {
Pet
}
这样即不需要 Dog 声明自己的同名函数成员,默认的调用即为 Pet 成员函数的调用。
package oriented_test
import (
"fmt"
"testing"
)
type Pet struct {
}
func (p *Pet) Speak(){
fmt.Print("Pet speak.\n")
}
func (p *Pet) SpeakTo(host string) {
p.Speak()
fmt.Println("Pet SpeakTo ", host)
}
// Dog 扩展Pet的功能
type Dog struct {
Pet // 支持匿名嵌套类型
}
func TestDog(t *testing.T) {
var dog Dog
dog.Speak()
dog.SpeakTo("Test dog")
}
最终的输出如下:
调用的都是 Pet 的成员函数,感觉像是继承了,因为继承默认就是子类能够使用父类的公有成员。
在匿名嵌套类型下,我们想要完整尝试一下Go语言是否真正支持继承,可以像之前的代码一样在 Dog 中实现 Pet 的同名函数,且能够通过父类对象调用子类的成员方法,像 C++/Java 这样进行向上类型转换(本身是不可能的,Go语言不支持显式类型转换)。
案例三: Go语言不支持继承,如下代码:
package oriented_test
import (
"fmt"
"testing"
)
type Pet struct {
}
func (p *Pet) Speak(){
fmt.Print("Pet speak.\n")
}
func (p *Pet) SpeakTo(host string) {
p.Speak()
fmt.Println("Pet SpeakTo ", host)
}
// Dog 扩展Pet的功能
type Dog struct {
//p *Pet
Pet // 支持匿名嵌套类型
}
// 重载父类的方法
func (d *Dog) Speak(){
fmt.Print("Dog speak.\n")
}
// 重载父类的方法
func (d *Dog) SpeakTo(host string) {
d.Speak()
fmt.Println("Dog Speakto ", host)
}
func TestDog(t *testing.T) {
var dog Pet = new(Dog) // 这里就会编译错误
dog.Speak()
dog.SpeakTo("Test dog")
}
cannot use new(Dog) (value of type *Dog) as Pet value in variable declaration
不支持将Pet类型转换为Dog类型
总结一下,Go语言并不支持继承,能够支持接口的扩展 和 复用**(匿名嵌套类型)**,内嵌这种方式是完全不能当成继承来用的,因为它不支持访问子类的方法数据(重载),不支持LSP原则。
其中扩展 就是不同类实现相同的成员函数,能够实现类似于案例一中的扩展接口形态。
复用则是通过匿名嵌套类型实现 类似于重载的功能,可以看看案例二的代码。
多态
type Programmer interface {
WriteHelloWorld() string
}
type GoProgrammer struct {
}
func (g *GoProgrammer) WriteHelloWorld() string {
return "fmt.Println(\"Hello World\")"
}
type JavaProgrammer struct {
}
func (j *JavaProgrammer) WriteHelloWorld() string {
return "System.out.println(\"Hello World\")"
}
// 多态
func writeFirstProgram(p Programmer) {
fmt.Printf("%T %v\n", p, p.WriteHelloWorld())
}
func TestClient(t *testing.T) {
goProgrammer := new(GoProgrammer)
javaProgrammer := new(JavaProgrammer)
writeFirstProgram(goProgrammer)
writeFirstProgram(javaProgrammer)
}
空接口与断言
func DoSomething(p interface{}) {
// '.' 断言,p.(int),p断言为int类型
if i, ok := p.(int); ok {
fmt.Println("Integer", i)
return
}
if s, ok := p.(string); ok {
fmt.Println("string", s)
return
}
fmt.Println("UnKnow type")
}
func TestEmptyInterface(t *testing.T) {
DoSomething(10)
DoSomething("10")
DoSomething(10.00)
}
Go接口最佳实践
错误机制
error
package error
import (
"errors"
"testing"
)
func GetFibonacci(n int) ([]int, error) {
if n < 2 || n > 100 {
return nil, errors.New("n should be in [2, 100]")
}
fibList := []int{1, 1}
for i := 2; i < n; i++ {
fibList = append(fibList, fibList[i-1]+fibList[i-2])
}
return fibList, nil
}
func TestGetFibonacci(t *testing.T) {
if v, err := GetFibonacci(-10); err != nil {
t.Error(err)
} else {
t.Log(v)
}
}
最佳实践
尽早失败,避免嵌套!
例:
func GetFibonacci2(str string) {
var (
i int
err error
list []int
)
if i, err = strconv.Atoi(str); err != nil {
fmt.Println("Error", err)
return
}
if list, err = GetFibonacci(i); err != nil {
fmt.Println("Error", err)
return
}
fmt.Println(list)
}
panic
panic vs os.Exit
revocer
recover
类似于 java 的 catch
。
func TestRecover(t *testing.T) {
defer func() {
if err := recover(); err != nil {
fmt.Println("recovered from", err)
}
}()
fmt.Println("Start")
panic(errors.New("something wrong"))
}
其实上面这种修复方式是非常危险的。
我们一定要当心自己 revocer
在做的事,因为我们 revocer 并不检测到底发生了什么错误,而只是记录了一下或者直接忽略掉了,这时可能系统某些核心资源消耗完了,但我们把他强制恢复之后系统依然是不能正常工作的,还会导致我们的健康检查程序 health check
检查不出当前系统的问题,因为很多 health check 只是检查当前系统的进程在还是不在, 因为我们的进程是在的,所以就会形成僵尸进程
,它还活着,但它不能提供服务。
如果出现了这种问题,我们可以用 “Let is Crash”
可恢复的设计模式,我们直接 crash 掉,这样守护进程就会重新把服务进程提起来(说的有点高大上,其实就是重启),重启是恢复不确定性错误的最好方法。
包 package
构建可复用的模块(包)
my_series.go
package series
func GetFibonacci(n int) ([]int, error) {
ret := []int{1, 1}
for i := 2; i < n; i++ {
ret = append(ret, ret[i-1]+ret[i-2])
}
return ret, nil
}
package_test.go
引用另一个包中的方法:
package client
import (
"mygoland/geekvideo/ch13/series" // 包路径要从自己的gopath开始写起
"testing"
)
func TestPackage(t *testing.T) {
t.Log(series.GetFibonacci(10))
}
init方法
func init() {
fmt.Println("init1")
}
func init() {
fmt.Println("init2")
}
func GetFibonacci(n int) []int {
ret := []int{1, 1}
for i := 2; i < n; i++ {
ret = append(ret, ret[i-1]+ret[i-2])
}
return ret
}
如何使用远程的package
ConcurrentMap for GO
https://github.com/easierway/concurrent_map
使用 go get
命令导入
go get -u github.com/easierway/concurrent_map
package remote
import (
cm "github.com/easierway/concurrent_map" // 导入远程包
"testing"
)
func TestConcurrentMap(t *testing.T) {
m := cm.CreateConcurrentMap(99)
m.Set(cm.StrKey("key"), 10)
t.Log(m.Get(cm.StrKey("key")))
}
依赖管理
Go未解决的依赖问题
vendor路径
常用的依赖管理工具
- godep:https://github.com/tools/godep
- glide:https://github.com/Masterminds/glide
- dep:https://github.com/golang/dep
安装glide
-
mac环境,使用 brew 安装 glide
brew install glide
安装成功
-
初始化 glide
glide init
-
glide init 执行完毕后,生成了一个
yaml
文件,并把依赖的包和版本号定义在了里面 -
在之前的目录下执行
glide install
然后就会在我们的指定的文件下面生成一个
vender
目录和glide.lock
文件。 -
到此为止,Go 就能 搜索到 vender 目录下面的 package 了,我们就通过 vender 来指定了包的路径和版本号,即实现了在同一环境下使用同一个包的不同版本依赖了。
笔记整理自极客时间视频教程:Go语言从入门到实战