类型系统

类型系统是指一个语言的类型体系结构,一个典型的类型系统通常包含如下基本内容:

  • 基础类型,如:byte、int、bool、float等
  • 复合类型,如:数组、结构体、指针等
  • 可以指向任意对象的类型(Any类型)
  • 值语义和引用语义
  • 面向对象,即:所有具备面向对象特征(比如成员方法)的类型
  • 接口

为类型添加方法

在Golang中,可以给任意类型(包括内置类型,但不包括指针类型)添加相应的方法。

  1. type Integer int
  2. func (a Integer) Less(b Integer) bool {
  3. return a < b
  4. }

如上示例,定义了一个新的类型Integer,它和int没有本质不同,只是它为内置的int类型增加了个新方法Less()

这样实现了Integer后,就可以让整型像一个普通的类一样使用:

  1. func main() {
  2. var a Integer = 1
  3. if a.Less(2) {
  4. fmt.Println(a, "less 2")
  5. }
  6. }

Golang中的面向对象最为直观,也无需支付额外的成本。如果要求对象必须以指针传递,这有时会是个额外成本,因为对象有时很小(比如4个字节),用指针传递并不划算。

只有在需要修改对象的时候才必须使用指针。

  1. // 需要修改对象的时候才必须使用指针
  2. func (a *Integer) Add(b Integer) {
  3. *a += b
  4. }

值语义和引用语义

值语义和引用语义的差别在于赋值,如下示例:

  1. b = a
  2. b.Modify()

如果b的修改不会影响a的值,那么此类型就是值类型;如果会影响a的值,那么此类型就是引用类型。

Golang中的大多数类型都是值语义,包括:

  • 基本类型,如:byte、int、bool、float32、float64和string等
  • 复合类型,如:数组、结构体、指针等

Golang中的数组与基本类型没有区别,是很纯粹的值类型。

  1. var a = [3]int{1, 2, 3}
  2. var b = a
  3. b[1]++
  4. fmt.Println(a)
  5. fmt.Println(b)

输出:

  1. [1 2 3]
  2. [1 3 3]

b[1]++的结果并没有影响到a[1]的值,这表明b=a赋值语句是数据内容的完整复制。

要想表达引用,需要使用指针:

  1. var a = [3]int{1, 2, 3}
  2. var b = &a
  3. b[1]++
  4. fmt.Println(a)
  5. fmt.Println(*b)

输出:

  1. [1 3 3]
  2. [1 3 3]

b[1]++的结果影响到了a[1]的值,这表明b=&a赋值语句是数组内容的引用。变量b的类型不是[3]int,而是*[3]int

Golang中有4个类型比较特别,看起来像是引用类型,包括:

  • 数组切片:指向数组(array)的一个区间
  • map:极其常见的数据结构,提供键值查询能力
  • channel:执行体(goroutine)间的通信设施
  • 接口(interface):对一组满足某个契约的类型的抽象

结构体

Golang放弃了包括继承在内的大量面向对象特性,只保留了组合这个最基础的特性。

前面已经说过,所有的Golang类型(指针类型除外)都可以有自己的方法。在这个背景下,Golang的结构体只是很普通的复合类型。

  1. // 定义一个矩形类型
  2. type Rect struct {
  3. x, y float64
  4. width, height float64
  5. }

定义一个成员方法来计算矩形的面积:

  1. // 定义一个成员方法来计算面积
  2. func (r *Rect) Area() float64 {
  3. return r.width * r.height
  4. }

初始化

创建并初始化类型的对象实例有多种方式:

  1. // 创建初始化自定义类型对象实例
  2. rect1 := new(Rect)
  3. rect2 := &Rect{}
  4. rect3 := &Rect{0, 0, 100, 200}
  5. rect4 := &Rect{width: 100, height: 200}

在Golang中,未进行显示初始化的变量都会被初始化为该类型的零值(如bool类型的零值为false,int类型的零值为0,string类型的零值为空字符串)。

在Golang中,没有构造函数的概念,对象的创建通常交给一个全局的创建函数来完成,以NewXXX类命名,表示构造函数。

  1. // 创建类型对象的全局函数
  2. func NewRect(x, y, width, height float64) *Rect {
  3. return &Rect{x, y, width, height}
  4. }

匿名组合

确切地说,Golang也提供了继承,但是采用了组合的文法,所以称其为匿名组合。

  1. type Base struct {
  2. Name string
  3. }
  4. func (base *Base) Foo() {
  5. fmt.Println("This is foo in Base")
  6. }
  7. func (base *Base) Bar() {}
  8. type Foo struct {
  9. Base
  10. }
  11. func (foo *Foo) Bar() {
  12. foo.Base.Bar()
  13. }

如上示例代码定义了一个Base类(实现了Foo()和Bar()两个成员方法),然后定义了一个Foo类,该类从Base类“继承”并改写了Bar()方法(该方法实现时先调用了基类的Bar()方法)。

在“派生类”Foo中没有改写“基类”的Base的成员方法时,相应的方法就被“继承”,例如调用foo.Foo()和调用foo.Base.Foo()效果一致。

  1. func main() {
  2. foo := new(Foo)
  3. foo.Foo() // 输出:This is foo in Base
  4. foo.Base.Foo() // 输出:This is foo in Base
  5. }

与其他语言不同,Golang很清晰地告诉了类的内存布局是怎样的。

此外,在Golang中还可以随心所欲地修改内存布局。

  1. type Foo struct {
  2. // ...
  3. Base
  4. }

这段代码从语义上来说,与其他例子并无不同,但是内存布局发生了变化:“基类”Base的数据放在了“派生类”Foo的最后。

另外,在Golang中还可以以指针的方式从一个类型“派生”:

  1. type Foo struct {
  2. *Base
  3. }

这段代码依然有“派生”的效果,只是在创建Foo实例的时候,需要外部提供一个Base类实例的指针。

可见性

Golang没有提供类似privateprotectedpublic这样表示可见性的关键字,要使某个符号对其他包可见,需要将该符号定义为大写字母开头。

  1. type Rect struct {
  2. X, Y float64
  3. Width, Height float64
  4. }

这样Rect类型的全部成员就被导出了,可以被其他引用了Rect所在包的代码访问到。

成员方法的可见性也遵循同样的规则,如:

  1. func (r *Rect) area() float64 {
  2. return r.Width * r.Height
  3. }

这样Rect类型的成员方法area()就只能在该类型所在包内使用。

接口

非侵入式接口

在Golang中,一个类只需要实现了接口要求的所有函数,就可以说这个类实现了该接口。

如下定义一个File类,并实现了ReadWriteSeekClose方法。

  1. // 定义一个File类
  2. type File struct {
  3. }
  4. func (f *File) Read(buf []byte) (n int, err error) {
  5. return 0, nil
  6. }
  7. func (f *File) Write(buf []byte) (n int, err error) {
  8. return 0, nil
  9. }
  10. func (f *File) Seek(off int64, whence int) (pos int64, err error) {
  11. return 0, nil
  12. }
  13. func (f *File) Close() error {
  14. return nil
  15. }

假设有如下接口:

  1. type IFile interface {
  2. Read(buf []byte) (n int, err error)
  3. Write(buf []byte) (n int, err error)
  4. Seek(off int64, whence int) (pos int64, err error)
  5. Close() error
  6. }
  7. type IReader interface {
  8. Read(buf []byte) (n int, err error)
  9. }
  10. type IWriter interface {
  11. Write(buf []byte) (n int, err error)
  12. }
  13. type ICloser interface {
  14. Close() error
  15. }

尽管File类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是File类实现了这些接口(实现了接口中的所有函数),所以如下赋值是正确的:

  1. var file1 IFile = new(File)
  2. var file2 IReader = new(File)
  3. var file3 IWriter = new(File)
  4. var file4 ICloser = new(File)

接口赋值

接口赋值在Golang中分为两种情况:

  • 将对象实例赋值给接口
  • 将一个接口赋值给另一个接口

将某种类型的实例赋值给接口,这要求该对象实例实现了接口要求的所有方法。

  1. // Integer是一个新的类型
  2. type Integer int
  3. func (a Integer) Less(b Integer) bool {
  4. return a < b
  5. }
  6. func (a *Integer) Add(b Integer) {
  7. *a += b
  8. }
  9. // LessAdder是一个接口类型
  10. type LessAdder interface {
  11. Less(b Integer) bool
  12. Add(b Integer)
  13. }

显然,Integer类实现了LessAdder接口的所有方法,所以可以将Integer类实例赋值给LessAdder接口类型。

  1. var a Integer = 1
  2. var b LessAdder = &a

将一个接口赋值给另一个接口,要求两个接口拥有相同的方法列表(方法次序可以不同)。

  1. type ReadWriter interface {
  2. Read(buf []byte) (n int, err error)
  3. Write(buf []byte) (n int, err error)
  4. }
  5. type IStream interface {
  6. Write(buf []byte) (n int, err error)
  7. Read(buf []byte) (n int, err error)
  8. }

如上,两个接口类型ReadWriterIStream拥有相同的方法列表,在Golang中这两个接口并无差别。

如下赋值是完全正确的:

  1. var file1 ReadWriter = new(File) // 创建一个ReadWriter类型
  2. var file2 IStream = file1 // 将ReadWriter类型赋值给IStream类型
  3. var file3 ReadWriter = file2 // 将IStream类型赋值给ReadWriter类型

接口赋值并不要求两个接口必须等价:如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A,反过来并不成立。

  1. type Writer interface {
  2. Write(buf []byte) (n int, err error)
  3. }
  4. var file1 ReadWriter = new(File)
  5. var file2 Writer = file1 // 将“大”接口赋值给小接口可以
  6. var file3 ReadWriter = file2 // 编译报错,不能将“小”接口赋值给大接口

接口查询

接口查询的语法如下:

  1. // 判断file1对象(Writer类型)是否实现了IStream接口,如果实现了则执行特定的代码
  2. var file1 Writer = new(File)
  3. if file2, ok := file1.(IStream); ok {
  4. fmt.Println("file1 implement interface IStream, file2: ", file2)
  5. }

还可以询问接口指向的对象是否是某个类型:

  1. // 判断file1对象(Writer类型)是否属于File类型
  2. var file1 Writer = new(File)
  3. if file3, ok := file1.(*File); ok {
  4. fmt.Println("file1 is File type, file3: ", file3)
  5. }

类型查询

在Golang中可以直接了当地询问接口只想的对象实例的类型:

  1. var v1 interface{} = new(File)
  2. switch v := v1.(type) {
  3. case *File:
  4. fmt.Println(v, "is File type")
  5. default:
  6. fmt.Println(v, "is Unknown type")
  7. }

接口组合

  1. // io包中的ReadWriter接口将基本的Read和Write方法组合起来
  2. type ReadWriter interface {
  3. Reader
  4. Writer
  5. }

ReadWriter接口组合了ReaderWriter两个接口,它完全等同于如下写法:

  1. type ReadWriter interface {
  2. Read(p []byte) (n int, err error)
  3. Write(p []byte) (n int, err error)
  4. }

因为这两种写法的表意完全相同:ReadWriter接口既能做Reader接口的所有事情,又能做Writer接口的所有事情。

可以认为接口组合是类型匿名组合的一个特定场景,只不过接口只包含方法,而不包含任何成员变量。

Any类型

Golang中的任何对象都满足空接口interface{},所以interface{}看起来像是可以指向任何对象的Any类型,如下:

  1. var v1 interface{} = 1 // 将int类型赋值给interface{}
  2. var v2 interface{} = "abc" // 将string类型赋值给interface{}
  3. var v3 interface{} = &v2 // 将*interface{}类型赋值给interface{}
  4. var v4 interface{} = struct{ x int }{1} // 将结构体类型赋值给interface{}
  5. var v5 interface{} = &struct{ x int }{1}

学习go语言编程之面向对象的更多相关文章

  1. 大数据技术之_16_Scala学习_04_函数式编程-基础+面向对象编程-基础

    第五章 函数式编程-基础5.1 函数式编程内容说明5.1.1 函数式编程内容5.1.2 函数式编程授课顺序5.2 函数式编程介绍5.2.1 几个概念的说明5.2.2 方法.函数.函数式编程和面向对象编 ...

  2. 如何轻松学习C语言编程!

    C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构.C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现 ...

  3. 学习go语言编程系列之定义变量

    package main import ( "fmt" "math") func main() { // 1. 定义变量名age,不初始化,使用对应类型的默认值 ...

  4. 学习go语言编程系列之helloworld

    1. 下载https://golang.org/dl/ # Go语言官网地址,在国内下载太慢,甚至都无法访问.通过如下地址下载:https://golangtc.com/download. 2. 安装 ...

  5. JavaSE学习总结第06天_Java语言基础2 & 面向对象1

      06.01 二维数组概述和格式1的讲解 二维数组概述:二维数组其实就是一个元素为一维数组的数组 格式1:数据类型[][] 变量名 = new 数据类型[m][n]; m表示这个二维数组有多少个一维 ...

  6. 怎样在C语言里实现“面向对象编程”

    有人觉得面向对象是C++/Java这样的高级语言的专利,实际不是这样.面向对象作为一种设计方法.是不限制语言的.仅仅能说,用C++/Java这样的语法来实现面向对象会更easy.更自然一些. 在本节中 ...

  7. 适合刚刚学习编程的萌新:C语言编程学习制作超简单又好玩的报数游戏!

    C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构.C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现 ...

  8. 新手学习C语言/C++编程你所必须要了解的知识!从计算机原理开始!

    C语言是面向过程的,而C++是面向对象的 C和C++的区别: C是一个结构化语言,它的重点在于算法和数据结构.C程序的设计首要考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现 ...

  9. python学习第十四天 -面向对象编程基础

    python也是支持面向对象编程的.这一章节主要讲一些python面向对象编程的一些基础. 什么是面向对象的编程? 1.面向对象编程是一种程序设计范式 2.把程序看做不同对象的相互调用 3.对现实世界 ...

  10. Linux C语言编程学习笔记 (1)进程控制入门

    想进行Linux系统开发已经很久了,一直没有付诸实践.今日终于开始学习Linux下的C语言编程,研究一天,终于大概弄明白了Linux系统进程管理的一些基本概念和编程方法,总结下来以方便大家学习和自己实 ...

随机推荐

  1. 参照DefenseGrid在Unity中实现合理的塔防寻路机制

    前言 在一款TD游戏中,最重要的单位就两大类:防御塔(Tower)和敌人单位(Enemy).在处理敌人单位的AI行为时,最基本也是最重要的就是自动寻路.在各式TD游戏中,防御塔的攻击方式以及敌人单位的 ...

  2. SUBMIT指定用户名错误

    1.SUBMIT说明 在ABAP中,SUBMIT关键字用于运行另一个ABAP程序.通过SUBMIT关键字,可以在当前程序内部调用其他程序,而无需关闭当前程序. SUBMIT语句的一般语法如下: &qu ...

  3. 前端 Git-Hooks 工程化实践

    前言 前段时间,部门的前端项目迁移到 monorepo 架构,笔者在其中负责跟 git 工作流相关的事情,其中就包括 git hooks 相关的工程化的实践.用到了一些常用的相关工具如 husky.l ...

  4. 慢SQL的致胜法宝

    大促备战,最大的隐患项之一就是慢SQL,对于服务平稳运行带来的破坏性最大,也是日常工作中经常带来整个应用抖动的最大隐患,在日常开发中如何避免出现慢SQL,出现了慢SQL应该按照什么思路去解决是我们必须 ...

  5. 【JS 逆向百例】PEDATA 加密资讯以及 zlib.gunzipSync() 的应用

    关注微信公众号:K哥爬虫,持续分享爬虫进阶.JS/安卓逆向等技术干货! 声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后 ...

  6. 如何在centos7中完全卸载Python3

    如何在centos7中完全卸载Python3?根据查到的资料,主要就是卸载,然后删除一些软连接删除干净,逻辑很简单,贴一些具体的操作代码,记录下来 . 卸载Python3的步骤 #卸载python3 ...

  7. 文档级关系抽取:基于结构先验产生注意力偏差SSAN模型

    文档级关系抽取:基于结构先验产生注意力偏差SSAN模型 Entity Structure Within and Throughout: Modeling Mention Dependencies fo ...

  8. 【3】VSCode 主题设置推荐,自定义配色方案,修改注释高亮颜色

    相关文章: [一]tensorflow安装.常用python镜像源.tensorflow 深度学习强化学习教学 [二]tensorflow调试报错.tensorflow 深度学习强化学习教学 [三]t ...

  9. MySQL 索引与性能调优

    索引用于快速找出在某个列中有一特定值的行,如果不使用索引MySQL必须从第l条记录开始读完整个表,直到找出相关的行.表越大,查询数据所花费的时间越多,如果表中查询的列有一个索引,MySQL能快速到达某 ...

  10. ElasticSearch-索引库、文档操作

    1.elasticsearch的作用 elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容 2.elasticsearch和lucen ...