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代码的示例:
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())
}
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)
}
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 vs os.Exit
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 掉,这样守护进程就会重新把服务进程提起来(说的有点高大上,其实就是重启),重启是恢复不确定性错误的最好方法。
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))
}
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
}
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")))
}
mac环境,使用 brew 安装 glide
brew install glide
初始化 glide
glide init
glide init 执行完毕后,生成了一个 yaml
文件,并把依赖的包和版本号定义在了里面
在之前的目录下执行glide install
然后就会在我们的指定的文件下面生成一个 vender
目录和 glide.lock
文件。
到此为止,Go 就能 搜索到 vender 目录下面的 package 了,我们就通过 vender 来指定了包的路径和版本号,即实现了在同一环境下使用同一个包的不同版本依赖了。
笔记整理自极客时间视频教程:Go语言从入门到实战