今天姑且学习简单的函数部分。
函数
函数可以说是一个程序里面最常见的代码封装形式,函数实质上是对一些操作进行封装,可以减少重复的代码编写。
由于Go是静态类型语言,因此,在申明函数时,要同时申明传参的类型以及返回值的类型,举个例子:
package main
import "fmt"
func main () {
myname := "longalong" // 换成 "" 则失败
if isSuccess := sayName(myname); isSuccess {
fmt.Println("sayName 执行成功了")
} else {
fmt.Println("sayName 执行失败了")
}
}
func sayName (name string) bool {
if name != "" {
fmt.Println("hello, my name is ", name)
return true
}
return false
}
再举一个数学计算的例子:
package main
import "fmt"
func main () {
a := 123.1
b := 234.5
fmt.Println("add a and b equals to : ", add(a, b))
}
func add (a, b float64) (sum float64) {
sum = a + b
return
}
和很多编程语言不一样,Go是支持多个返回值的,这点和python、lua是一致的,在js中,虽然不支持多返回值,但可以使用字典的结构来实现类似的效果。这个特点,可以用来作为错误判断的方法,约定返回的第一个值为结果,第二个值为是否错误,则可以根据函数执行结果进行处理。举个例子:
package main
import (
"fmt"
)
type conn struct {
isSuccess bool
ip string
}
func main() {
if con, err := connectXxx(); err == nil {
fmt.Println("connect result : ", con)
} else {
fmt.Println("connect error")
}
}
func connectXxx() (*conn, error) {
conX := &conn{
isSuccess: true,
ip: "192.168.123.78",
}
return conX, nil
}
结构体
结构体在 C 语言中是用的最多的,表示一系列属性的集合,在其他语言中,很少有 结构体 的概念,例如,在python中,用 字典、类 来表示一系列属性的集合,在 js 和 lua 中,由于只有类似于字典的结构,因此使用字典(对象,hash)来存储。
在 Go 中,没有 class 这种概念,但实质上,不论是什么概念,只要能达到类似的功能,那么就可以用一定的编码方式去实现相同的功能,虽然叫法和写法不同,但从功能的角度看,本质上都是一样的。
面向对象编程毋庸置疑是一种非常强大的编程范式,这和开发者认识事物的方式是一样的,因此,从理解上就极大降低了开发时所需的开发者心智。这种模式在应用开发中非常重要。
因此,在Go语言中,掌握面向对象的编程范式,就得从理解结构体开始。
一个基础的问题:什么是对象? 回答:一个具有一系列属性和方法的集合。也就是说,要实现面向对象,就要搞定“属性”以及“方法”这两个概念。
结构体,可以用来实现对象中的 “属性”。 属性,其实就是一些用来描述对象的字段,例如,一个人是一个对象,如何描述一个人的特征呢? 人有:姓名(name),年龄,性别,手机号等等属性,这时,我们可以用一个结构体来表示这些属性:
package main
import "fmt"
type people struct {
name string
age int
gender byte // 1 male, 2 female
phone string
}
func main () {
long := new(people)
long.name = "longalong"
long.age = 18
long.gender = 1
long.phone = "13011001100"
fmt.Println("long's info : ", *long)
}
这里,我们采用了 new
这个方法来创建一个结构体实例,实质就是创建了一个属性值为基础值的结构体属性集合,在后面几步操作中,逐一给 long 这个结构体实例修改属性值。这有点类似于在 js 中使用工厂模式(当然,这里更像是单例模式)去创建一个对象:
let long = {}
long.name = "longalong"
long.age = 18
long.gender = 1
long.phone = "13011001100"
console.log("long's info : ", long)
当然,这种创建的方式肯定还是比较麻烦的,如果能直接使用键值对的形式创建就好一些了,因此,可以采用另一种方式:
package main
import "fmt"
type people struct {
name string
age int
gender byte // 1 male, 2 female
phone string
}
func main () {
long := &people{
name: "longalong",
age: 18,
gender: 1,
phone: "13011001100",
}
fmt.Println("long's info : ", *long)
}
这样的结果是一样的,类似于在js中直接初始化一个对象:
let long = {
name: "longalong",
age: 18,
gender: 1,
phone: "13011001100"
}
console.log("long's info : ", long)
在Go的代码里,其实 type 后面定义的这个 people ,已经类似于一个 class 的名字了,因此,改成在js中,用class表示,可以这样写:
class People {
constructor ({name, age, gender, phone}) {
this.name = name
this.age = age
this.gender = gender
this.phone = phone
}
}
let long = new People({
name: "longalong",
age: 18,
gender: 1,
phone: "13011001100"
})
console.log("long's info : ", long)
在 python 中,写法是这样的:
class People(object):
def __init__(self, name, age, gender, phone):
self.name = name
self.age = age
self.gender = gender
self.phone = phone
long = People(name="longalong", age=18, gender=1, phone="13011001100")
print("long's info : ", long.__dict__)
其实,都是把一些属性绑定到一个特定的集合体上,这个集合体是叫 对象 还是叫 结构体,都无所谓。
一般来说,如果对象的属性都是简单的基础类型的话,那就太简单了,越简单,能够表示的逻辑关系就越少,就无法表示一些关联关系。比如,人会上学,那么,就可以把跟学校相关的信息绑定到一个人上,简单的我们可以这么写:
package main
import "fmt"
type people struct { // 省略一些基础信息age、gender、phone
name string
schoolName string // 学校名称
grades uint8 // 年级
teacher []people
}
func main() {
long := &people{
name: "longalong",
schoolName: "xxx school",
grades: 12,
teacher: []people{
people{
name: "math teacher",
},
people{
name: "chinese teacher",
},
people{
name: "english teacher",
},
},
}
fmt.Println("long's info : ", long)
}
但是我们发现,其实 schoolName、grades、teacher 这些属性本身是有逻辑相关性的,他们都属于跟学校相关的信息。因此,把他们聚合起来,形成一个新的结构体是更合理的,于是一番聚合,写成了这样:
package main
import "fmt"
type subject struct { // 课程信息
subjectName string
subType uint8 // 1 文科类课、2 理科类课、3 艺术类课、 4 体育类课
}
type teacher struct { // 教师信息
baseInfo *people // 作为人的基本自然属性
inChargeClass *classStuff // 班级信息
subjects []*subject // 所授课程
}
type student struct { // 学生信息
baseInfo *people // 作为人的基本自然属性
school *classStuff // 学校相关的事
}
type classStuff struct { // 班级信息
schoolName string
grades uint8
headerTeacher *teacher // 班主任
teachers map[string]*teacher // 任课教师字典
students []*student
}
type people struct { // 人的基本属性,省略一些基础信息age、gender、phone
name string
}
func main() {
chineseSubject := &subject{ // 创建语文课
subjectName: "chinese",
subType: 1,
}
class5 := &classStuff{ // 创建xxx学校 高三5班 基础信息
schoolName: "xxx school",
grades: 12,
}
teacherLong := &teacher{ // 创建教师longsang
baseInfo: &people{
name: "longsang",
},
inChargeClass: class5,
subjects: []*subject{
chineseSubject,
},
}
// 给班级信息中增加教师信息
class5.headerTeacher = teacherLong
class5.teachers = map[string]*teacher{
"chinese": teacherLong,
}
long := &student{ // 创建学生 long
baseInfo: &people{
name: "longalong",
},
school: class5,
}
// 给班级信息中增加学生信息
class5.students = []*student{
long,
}
fmt.Println("long's name : ", long.baseInfo.name)
fmt.Println("class5 : ", long.school)
fmt.Println("chinese subject : ", chineseSubject)
fmt.Println("teacherLong : ", teacherLong)
fmt.Println("headerTeacher : ", long.school.headerTeacher)
}
结构体内嵌
一半来说,结构体的嵌套是需要用点运算符 .
来获取相应的属性的,但是在 Go 当中有一种写法,可以把属于某结构体属性的属性不用点运算符来依层次获取,而是直接获取其属性。这有点类似于 js 中的 __proto__
属性中的内容,当在本对象的属性中,无法找到对应的属性,不是直接返回 undefined,而是去原型链上找该属性名。举个例子:
let people = {
name: "没起名字",
greet: function () {
console.log("hello, my name is : ", this.name)
}
}
let long = {
name: "longalong"
}
long.__proto__ = people
long.greet() // hello, my name is : longalong
这里,long对象本身是没有 greet 这个方法的,但是实际却调用了,就是由于他从原型链上去找到了一个叫做 greet 的方法。
上面说的是方法,但实际上方法和属性本质上是一个东西,都是键值对,只是 属性一般指的是 值,而 方法一般指的是可调用的函数。我们回到Go语言中:
package main
import "fmt"
type male struct {
beard bool // 是否留了胡子
}
type boy struct {
name string
age uint8
male
}
func main() {
littleLong := boy{
"longalong",
8,
male{
beard: false,
},
}
fmt.Println("long's info : ", littleLong)
fmt.Println("long's male beard : ", littleLong.male.beard)
fmt.Println("long's beard : ", littleLong.beard)
}
在这个例子中,我们没有给 boy 申明一个叫做 beard 的属性,但实际上,却能够直接获取 beard 的属性。这实际上,是一种类似于 继承
的性质,使用这种特性,其实就已经可以模拟出面向对象编程的一些特点了。例如,用python写一段类似的代码:
class male(object):
def __init__(self, beard):
self.beard = beard
class boy(male):
def __init__(self, name, age, beard = False):
super().__init__(beard)
self.name = name
self.age = age
long = boy("longalong", 8, False)
print("long's info : ", long)
在python、java这类基于 类 的面向对象语言中,实例化时都是直接调用 super().__init__()
的方法进行的,因此写法上,在子类中不直接初始化父类的属性,而是在父类中初始化属性。当然,es6以前,js都是采用原型链编程的方式来实现继承的,在es6以后,也有了 class 这种和 “基于类的写法一样” 的写法,但实质上,其内部还是通过原型链的方式来构建对象的,因此 class 语句在js中算作是一种语法糖。
不过不管怎么说,这几种方式都是实现了 继承
的,这里的例子简单一些,不是特别能看出继承的强大,但是,一个真实的案例中,类的属性只是类当中最最最小的部分,类当中的方法(类方式、实例方法、静态方法等)才是开发中最主要的开发工作。
值得说明的是,一个结构体中的匿名内嵌结构体,一个类型结构体仅能有 1 个,否则会出现命名冲突,无法编译成功。另外,如果不同匿名结构体中,如果都有同一个属性,那么该属性就无法再通过直接获取的方式获取到属性,而是通过点运算符按层级进入获取。举个例子:
package main
import "fmt"
type male struct {
beard bool
}
type beardStatus struct {
beard bool
}
type boy struct {
name string
male
beardStatus
}
func main () {
long := boy{
"longalong",
male{false},
beardStatus{false},
}
fmt.Println("long's info : ", long)
fmt.Println("long's beard : ", long.male.beard)
// fmt.Println("can not get beard by long.beard : ", long.beard)
}
结构体初始化
结构体本身的定义,类似于在类中定义所拥有的字段,而真正要实例化一个结构体,要在代码中调用实例化的方法。在Go中,给我们提供了三种实例化结构体的方法,最基础的就是直接实例化,另一种是 取地址符&
,还有一种是 new()
方法。初始化的方法也有3个,可以根据属性顺序直接赋值,也可以根据属性名采用键值对赋值,还可以实例化一个全新的结构体,然后分别给属性赋值。举个例子:
package main
import "fmt"
type cat struct {
color string
name string
ageMonth uint16
}
func main() {
myCat1 := cat{ // 直接实例化
"orange",
"jumao",
8,
}
myCat2 := &cat{
name: "baimao",
color: "white",
ageMonth: 9,
}
myCat3 := new(cat)
myCat3.name = "huamao"
myCat3.color = "multi"
myCat3.ageMonth = 11
fmt.Println("cat1 info : ", myCat1)
fmt.Println("cat2 info : ", myCat2)
fmt.Println("cat3 info : ", myCat3)
}
在面向对象的编程里,我们一般不是手动去给对象赋值,而是会采用 构造函数
的方式,这样可以从中间进行一些额外的操作,在js中,我们可以使用 class 的 constructor 方法来构造,但是在es6以前,跟多采用的是工厂函数来构造的。那么,在Go语言中,我们也可以使用同样的方式来简化构造流程:
package main
import "fmt"
import "errors"
type cat struct {
name string
ageMonth uint16
color string
}
func main() {
myCat, _ := genCat("baimao", 101, "white")
fmt.Println("myCat info : ", myCat)
}
func genCat(name string, ageMonth uint16, color string) (*cat, error) {
err := nil
if ageMonth > 100 { // 中间可以进行各种预处理
fmt.Println("wa, you got a very old cat")
}
if ageMonth > 200 {
err = errors.New("hehe, a cat can't be such old")
return nil, err
}
return &cat{
name: name,
ageMonth: ageMonth,
color: color,
}, err
}
接口
在面向对象编程里, 类
无疑是非常重要的,因为 类的实例化 可以让我们减少很多重复代码的编写,提高的代码的复用。而 类 本身也是很符合人类思维的一个概念,我们经常会给事物分类,例如,一个小男孩,这是一个对象,我们给他归类可以是:人、男人、小孩、儿子、能开口说话的动物 等等。
那么,我们在创建一个小男孩的时候,是否能够把分类融入其中呢? 这其实就是一个 继承
和 多态
的问题,例如,小男孩 属于 男人,那么,小男孩就应当继承 男人 这个类,但同时,小男孩也属于 小孩,那么小男孩也应当继承 小孩 这个类,这个时候该怎么办呢?
其实,在一些语言中,类是可以多继承的,例如 C++(我不了解,网上说的),再例如 python。在 es6 中,这个问题有点不一样,从语法角度,es6 的 extends 关键字是不支持继承多个类的,但由于 es6 构造类的方式实际是基于 prototype 的语法糖,那么,继承时,我们只需要使用 mixin
的方式,就可以实现多继承。
在 Go 中,如何实现多种方法的混合呢?那就是使用 接口
。
接口可以实现一个类虽然不是多继承,但是却实现了不同类的方法。这里可以用 鸭子类型
这个经典的案例来说明。比如,我们有一只丑小鸭,我们实际不确定丑小鸭是鸭子还是天鹅,从本质上讲,它是继承自 天鹅 这个类的,但同时,它看起来像鸭子,并且还会像鸭子一样叫,那么,我们就姑且认为它是鸭子。在python中可以大概这么认为:
class goose(object):
def __init__(self):
pass
class duck(object):
def __init__(self):
pass
def quack(self):
print("I can quack like duck")
return True
@staticmethod
def isDuck(obj):
if obj.quack():
print("it can quack like a duck, so it is a duck")
return True
else:
print("it can not quack, so it is not a duck")
return False
class uglyGoose(goose):
def __init__(self):
super().__init__()
def quack(self):
print("I am not duck, but I can quack like duck")
return True
ugly1 = uglyGoose()
duck.isDuck(ugly1)
当然,由于python的特殊性(没有接口的概念),所以这里用来类比 Go 语言的接口其实不是很恰当,里面有非常多不同的地方。(我没有接触过其他编程语言用接口啥的呀233333,java又不熟,ts也不熟,C#这些完全不知道)
需要知道的是,Go语言里的接口,都是 隐式实现的,也就是说,没有像java中的那种明显的关键词 implement
,在java中,所有实现接口的类,都必须在类中完全实现,否则编译不通过,而在Go中,是否实现某个接口,完全是自己决定,只是实际调用的时候,必须要有有该接口的实现,否则无法获得接口变量。
举个例子:
package main
import "fmt"
type canTalk interface {
Talk(content string) error
}
type baby struct {
name string
ageMonth uint8
}
type cat struct {
name string
color string
}
func (b *baby) Talk(content string) error {
fmt.Println("I am ya~ " + b.name + " a~ ya~ ")
fmt.Println("ya~ a~ " + content + " a~ ya~")
return nil
}
func (c *cat) Talk(content string) error {
fmt.Println("I am a " + c.color + " cat")
fmt.Println(content)
return nil
}
func main() {
var mybaby canTalk = &baby{"longalong", 11}
var mycat canTalk = &cat{"baimao", "white"}
mybaby.Talk("MAMA, BABA")
mycat.Talk("miao miao miao ~")
}
这里,我们定义了一个可以说话的接口,并且由cat 和 baby 这两个结构体去实现,之后我们就可以实例化可以说话的类型,然后,不论 cat 和 baby 中是怎么实现 Talk 这个方法的,我们都可以在实例化后的 canTalk 类型上调用 Talk 这个方法。
接口类型断言
我们有时候需要检测一个类型是否实现了某接口,就需要用到接口类型断言的方式,举个例子:
package main
import "fmt"
type canTalk interface { // 会说话的接口
talk()
}
type cat struct { // 定义一个叫做 cat 的结构体
name string
}
func (c *cat) talk() { // 让 cat 实现 可以说话 的方法
fmt.Println("I am ", c.name)
fmt.Println("I can talk miao~ miao~ ")
}
func main() {
cats := map[string]interface{}{
"cat1": &cat{"baimao"},
"cat2": &cat{"huamao"},
}
for _, val := range cats {
if val, ok := val.(canTalk); ok { // 这里进行了接口断言
val.talk()
} else {
fmt.Println("can not talk")
}
}
}
这里其实用了 空接口
的方式,把原本是 *cat 的类型,转变成了 interface{},这样才能在接口断言中方便使用,否则,就要在 cat 上再加一层接口,然后 cat 就完全实现 上面的那一层接口,那样代码就不好看了。
空接口有点类似于动态语言中的类型,是一个可以容纳各种类型的容器,正确使用的话,会感受到十分灵活。但是也不能过度使用,否则Go语言的静态类型检查作用就变小了,发生奇怪错误的可能性就增加了。
类型分支
这是Go语言里面独特的一种分支结构,用变量所实现的接口去产生分支,暂时估计不怎么用,姑且跳过,之后再看。
错误处理
错误处理对于每一门编程语言都是至关重要的,在 java 和 js 中,使用 try catch finally throw
这几个关键词来处理错误,在 python 中 使用 try except finally else raise
这几个关键词来处理错误,举个例子:
try {
// 做业务逻辑
console.log("假设执行了一堆的逻辑后出问题了")
throw new Error("出毛病了")
} catch (err) {
console.log("程序出问题了 ", err)
} finally {
console.log("虽然出毛病了,但是还是有一些无论如何都要做的事")
}
再举个python的例子:
try:
print("假设执行了一堆的逻辑后出问题了")
raise Exception("程序出毛病了")
except Exception as err:
print("哦豁,程序出问题了", err)
finally:
print("虽然出毛病了,但还是有一些必须要做的事")
但是在 Go 语言里,一般不是这么用的,开发Go的大佬们觉得现在的编程语言里的错误处理机制被过度使用了,因此,在Go里,大部分错误,都是通过 error 接口实现的,只有出现程序无法运行等错误时,才使用 panic、recover 、defer 三个关键词来解决,举个例子:
package main
import "fmt"
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("something error happend", err)
}
}()
fmt.Println("假设做了一堆的事情")
panic("Ooooops, program goes wrong")
}
但一般的错误,都是使用 error 接口实现,举个例子:
package main
import (
"errors"
"fmt"
)
func doSomething() (err error) {
fmt.Println("doing something now")
isSuccess := true
if isSuccess {
err = nil
} else {
err = errors.New("error in doSomething")
}
return
}
func main() {
if err := doSomething(); err != nil {
fmt.Println("things got wrong : ", err)
} else {
fmt.Println("things goes well")
}
}
这种方式其实还比较简洁,相比于经常抛出各种奇怪错误的写法,时常忘记写一些 catch 的处理,从而导致程序出错。