Go语言学习记录--从函数--到结构体--到接口--到错误处理

本文介绍了Go语言的基础知识,包括函数的使用,结构体的声明与初始化,接口的隐式实现,以及错误处理机制。函数支持多个返回值,结构体用于封装属性,接口实现多态,错误处理通过error接口实现,避免了异常处理的复杂性。
摘要由CSDN通过智能技术生成

今天姑且学习简单的函数部分。

 

函数

函数可以说是一个程序里面最常见的代码封装形式,函数实质上是对一些操作进行封装,可以减少重复的代码编写。

由于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 的处理,从而导致程序出错。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

iamlongalong

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值