类型系统

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

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

为类型添加方法

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

type Integer int

func (a Integer) Less(b Integer) bool {
return a < b
}

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

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

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

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

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

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

值语义和引用语义

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

b = a
b.Modify()

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

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

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

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

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

输出:

[1 2 3]
[1 3 3]

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

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

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

输出:

[1 3 3]
[1 3 3]

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

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

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

结构体

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

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

// 定义一个矩形类型
type Rect struct {
x, y float64
width, height float64
}

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

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

初始化

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

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

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

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

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

匿名组合

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

type Base struct {
Name string
} func (base *Base) Foo() {
fmt.Println("This is foo in Base")
}
func (base *Base) Bar() {} type Foo struct {
Base
} func (foo *Foo) Bar() {
foo.Base.Bar()
}

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

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

func main() {
foo := new(Foo)
foo.Foo() // 输出:This is foo in Base
foo.Base.Foo() // 输出:This is foo in Base
}

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

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

type Foo struct {
// ...
Base
}

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

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

type Foo struct {
*Base
}

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

可见性

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

type Rect struct {
X, Y float64
Width, Height float64
}

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

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

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

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

接口

非侵入式接口

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

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

// 定义一个File类
type File struct {
} func (f *File) Read(buf []byte) (n int, err error) {
return 0, nil
}
func (f *File) Write(buf []byte) (n int, err error) {
return 0, nil
}
func (f *File) Seek(off int64, whence int) (pos int64, err error) {
return 0, nil
}
func (f *File) Close() error {
return nil
}

假设有如下接口:

type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
} type IReader interface {
Read(buf []byte) (n int, err error)
} type IWriter interface {
Write(buf []byte) (n int, err error)
} type ICloser interface {
Close() error
}

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

var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)

接口赋值

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

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

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

// Integer是一个新的类型
type Integer int func (a Integer) Less(b Integer) bool {
return a < b
} func (a *Integer) Add(b Integer) {
*a += b
} // LessAdder是一个接口类型
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}

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

var a Integer = 1
var b LessAdder = &a

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

type ReadWriter interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
} type IStream interface {
Write(buf []byte) (n int, err error)
Read(buf []byte) (n int, err error)
}

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

如下赋值是完全正确的:

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

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

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

接口查询

接口查询的语法如下:

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

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

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

类型查询

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

var v1 interface{} = new(File)
switch v := v1.(type) {
case *File:
fmt.Println(v, "is File type")
default:
fmt.Println(v, "is Unknown type")
}

接口组合

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

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

type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}

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

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

Any类型

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

var v1 interface{} = 1                    // 将int类型赋值给interface{}
var v2 interface{} = "abc" // 将string类型赋值给interface{}
var v3 interface{} = &v2 // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ x int }{1} // 将结构体类型赋值给interface{}
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. [转帖]Kubernetes部署Minio集群存储的选择,使用DirectPV CSI作为分布式存储的最佳实践

    Kubernetes部署Minio集群存储的选择,使用DirectPV CSI作为分布式存储的最佳实践 个人理解浅谈 1. 关于在kubernetes上部署分布式存储服务,K8s存储的选择 非云环境部 ...

  2. 【转帖】什么是RLHF

    什么是RLHF? **字面翻译:**RLHF (Reinforcement Learning from Human Feedback) ,即以强化学习方式依据人类反馈优化语言模型. 强化学习从人类反馈 ...

  3. CPU实际频率查看

    cat /sys/devices/system/cpu/cpu3/cpufreq/cpuinfo_cur_freq

  4. [转帖]Kdump配置及使用(详细)总结(二)

    一.简介 本文主要介绍如何打开Kdump并对其相关文件进行配置.前面章节已经对Kdump调试机理进行总结总结,具体可以点击下面链接: Kdump调试机理总结(一) crash工具分析vmcore文件常 ...

  5. [转帖]JVM 问题诊断快速入门

    https://zhuanlan.zhihu.com/p/110197145 JVM 全称为 Java Virtual Machine,翻译为中文 "Java 虚拟机".本文中的J ...

  6. FS OFS RS ORS

  7. 【构造,树】【Loj】Loj6669 Nauuo and Binary Tree

    2023.7.3 Problem Link 交互库有一棵 \(n\) 个点的二叉树,你每次可以询问两个点之间的距离,猜出这棵二叉树.\(n\le 3000\),询问次数上限 \(30000\). 首先 ...

  8. [1] 以逆向的角度来看流程控制语句——if

    [1] 以逆向的角度来看流程控制语句--if 1. if语句(单分支) ​ if语句转换的条件跳转指令与if语句的判断结果是相反的, 因为C语言是根据代码行的位置决定编译后二进制代码地址高低的,即低行 ...

  9. 从嘉手札<2024-1-10.2>

    我们每个人都是在受挫中成长起来的 你不能剥夺他人在受苦中获益的权利 大部分人对吃苦的含义可能理解的太肤浅了 穷并不是吃苦 吃苦的本质是长时间为了某个目标而聚焦的能力 在这个过程中放弃娱乐生活.放弃无效 ...

  10. C# 字符串转码后操作二进制文件

    String转码后写入二进制文件,读二进制文件进行解码返回. public class BinaryClass { /// <summary> /// 写二进制文件 /// </su ...