6

Go语言基础(2)

 3 years ago
source link: https://studygolang.com/articles/31959
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go语言基础(2)

Go语言中支持函数、匿名函数和闭包

Go语言中定义函数

定义函数使用func关键字,语法如下:

func 函数名(参数)(返回值){
  函数体
}

说明:

​ 1) 函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。

​ 2) 参数:参数由参数变量和参数变量的类型组成,多个参数之间使用,分隔。

​ 3) 返回值:返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用()包裹,并用,分隔。

​ 4) 函数体:实现指定功能的代码块。

例:

func intSum(x int, y int) int {
    return x+y
}

func main() {
    fmt.Println(intSum(3,4))
}

函数的参数和返回值都属于可选项

函数调用

可以通过函数名()的方式调用函数

可变参数

可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...来标识

func intSum2(x ...int) int {
    fmt.Printf("x type is: %T\n", x) // x type is: []int
    sum := 0
    for _, item := range x {
        sum = sum + item
    }
    return sum
}

多返回值:

func calc(x,y int)(int,int) {
    return x+y, x-y
}

一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。

func demo() []int {
    return nil
}

变量作用域

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。

局部变量又分为两种: 1)函数内定义的变量无法在该函数外使用;

2) 语句块定义的变量在语句块外无法使用,通常会在if条件判断、for循环、switch语句上使用这种定义变量的方式

函数类型的变量

使用type关键字来定义一个函数类型

type calculation func(x,y int) int
func add(x,y int) int {
    return x + y

}

func sub(x,y int) int {
    return x - y

}

func main() {
    var c1 calculation
    c1 = add
    fmt.Println(c1(3,4))
    fmt.Printf("type of c:%T\n", c1)    //type of c:main.calculation
}

匿名函数

函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:

func(参数)(返回值){
  函数体
}

闭包

闭包: 外层函数返回内层函数,内层函数调用外层函数的变量;

func f1(x int) func(int) int{
    return func (y int) int {
        return x*y
    }
}

func main() {
    a1 :=f1(3)
    fmt.Println(a1(4))  // 输出12
}

高价函数

func add(x,y int) int {
    return x + y
}

func main() {
    fmt.Println(f2(5,2, add)) // 输出7 
}

defer语句

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行

在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示

JraI3i7.png!mobile

内置函数

内置函数 介绍 close 主要用来关闭channel len 用来求长度,比如string、array、slice、map、channel new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针 make 用来分配内存,主要用来分配引用类型,比如chan、map、slice append 用来追加元素到数组、slice中 panic和recover 用来做错误处理

panic/recover

Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover模式来处理错误。 panic可以在任何地方引发,但recover只有在defer调用的函数中有效。

func t1() {
    fmt.Println("func t1")
}

func t2() {
    defer func() {
        err := recover()
        if err != nil{
            fmt.Println("recover in func t2")
        }
    }()
    panic("panic in func t2")
}

func t3() {
    fmt.Println("func t3")
}

func main() {
    t1()
    t2()
    t3()
}

注意:

  1. recover()必须搭配defer使用。
  2. defer一定要在可能引发panic的语句之前定义

结构体

自定义类型

自定义类型是定义了一个全新的类型。可以基于内置的基本类型定义,也可以通过struct定义。例如:

//将MyInt定义为int类型
type MyInt int

通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

类型别名

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像儿时的乳名,上学后用学名,英语老师又会给起英文名,但这些名字都指的是他本人。

type TypeAlias = Type

rune和byte就是类型别名,内置的定义如下:

type byte = uint8
type rune = int32

类型别名与类型定义表面上看只有一个等号的差异,区别如下:

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt

    fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
    fmt.Printf("type of b:%T\n", b) //type of b:int
}

结构体

Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。

语法如下:

type 类型名 struct {
  字段名 字段类型
  字段名 字段类型
  …
}

说明:

1) 类型名:标识自定义结构体的名称,在同一个包内不能重复。

2) 字段名:表示结构体字段名。结构体中的字段名必须唯一。

3) 字段类型:表示结构体字段的具体类型。

结构体实例化

只有当结构体实例化时,才会真正地分配内存。也就是 必须实例化后才能使用结构体的字段

结构体本身也是一种类型,可以像声明内置类型一样使用var关键字声明结构体类型。

var 结构体实例 结构体类型

Go语言支持匿名结构体,也支持 指针类型结构体

例:

type person struct {
    name string
    age int
    marrired bool
}

func main() {
    var p01 person
    p01 = person{
        name: "zhsansan",
        age: 18,
        marrired: false,
    }
    fmt.Println(p01)    //{zhsansan 18 false}
    var s01 struct{name string; score int}  //匿名结构体
    s01.name = "tom"
    s01.score = 80
  fmt.Println(s01)  //{tom 80}

    p02 := new(person)  //通过new关键字对结构体进行实例化,得到的是结构体的地址。p02的类型是一个结构指针
    p02.name = "eric"
    p02.age = 25
    p02.marrired = true
    fmt.Println(*p02)   //{eric 25 true}

    p03 := &person{}    //使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作
    p03.name = "robin"
    p03.age = 22
    p03.marrired = false
    fmt.Println(*p03) //{robin 22 false}

    p04 := &person{name: "hallen", age: 20, marrired: false}
    fmt.Println(*p04)   //{hallen 20 false}
}

实例化之后的结构体占用一块连续的内存。空结构体是不占用空间的。

构造函数

Go语言的结构体没有内置的构造函数,需要自定义构造函数。如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。

方法的定义格式如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
  函数体
}

例:

func newPerson(name string, age int, married bool) *person {
    return &person{name: name, age: age, marrired: married}
}

func (p person)Dream(){
    fmt.Printf("%s's dream is rich\n", p.name)
}

func main(){
    p05 := newPerson("natasha", 24, false)
    fmt.Println(*p05)   //{natasha 24 false}
    p05.Dream() //natasha's dream is rich
}

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

什么时候应该使用指针类型接收者

1)需要修改接收者中的值

2)接收者是拷贝代价比较大的大对象

3)保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者

例:

func (p *person)setAge(age int){
    p.age = age
}

func main(){
    p05.setAge(21)
    fmt.Println(*p05)       //{natasha 21 false}
}

为内置类型添加方法

例:

type MyInt int

func (m MyInt)SayTest(){
    fmt.Println("hello world")
}

func main() {
    var m1 MyInt
    m1.SayTest()        // hello world
    m1 = 100
    fmt.Printf("%v %T\n", m1, m1)   //100 main.MyInt
}

结构的嵌套

一个结构体中可以嵌套包含另一个结构体或结构体指针,如下所示:

type Address struct {
    Province string
    City string
}

type User struct {
    Name string
    Gender string
    Address Address
}

type User2 struct {
    Name string
    Gender string
    Address     //匿名字段
}

func main() {
    user1 := User{
        Name: "Sam",
        Gender: "male",
        Address: Address{
            Province: "河北",
            City: "保定",
        },
    }
    fmt.Println(user1)  //{Sam man {河北 保定}}

    var user2 User2
    user2.Name = "Alex"
    user2.Gender = "male"
    user2.Address.Province = "辽宁"
    user2.City = "大连"
    fmt.Println(user2)  //{Alex male {辽宁 大连}}
}

结构体继承

Go语言中使用结构体也可以实现结构体的继承,如下所示:

type Animal struct {
    name string
}

func (a *Animal)move() {
    fmt.Println("会动了")
}

type Dog struct {
    Feet int
    *Animal     //通过嵌套匿名结构体实现继承
}

func (d *Dog)wang() {
    fmt.Println("汪汪汪")
}

func main() {
    d1 := &Dog{
        Feet: 4,
        Animal: &Animal{    //注意嵌套的是结构体指针
            name: "hely",
        },
    }
    d1.move()
    d1.wang()
}

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)

结构体标签

Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag在结构体字段的后方定义,由一对 反引号 包裹起来,具体的格式如下:

key1:"value1" key2:"value2"

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

注意事项:为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

包(package)

包(package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt、os、io等

可以根据自己的需要创建自己的包。一个包可以简单理解为一个存放.go文件的文件夹。 该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。

package 包名

注意事项:

1) 一个文件夹下面直接包含的文件只能归属一个package,同样一个package的文件不能在多个文件夹下。

2) 包名可以不和文件夹的名字一样,包名不能包含 - 符号。

3) 包名为main的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。

在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。

结构体中的字段名和接口中的方法名如果首字母都是大写,外部包可以访问这些字段和方法。

要在代码中引用其他包的内容,需要使用import关键字导入使用的包。具体语法如下:

import "包的路径"

注意事项:

1) import导入语句通常放在文件开头包声明语句的下面。

2) 导入的包名需要使用双引号包裹起来。

3) 包名是从$GOPATH/src/后开始计算的,使用/进行路径分隔。

4) Go语言中禁止循环导入包。

在导入包名的时候,还可以为导入的包设置别名。

import 别名 "包的路径"

如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。具体的格式如下:

import _ "包的路径"

匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。

init()函数

在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是: init()函数没有参数也没有返回值。 init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。

包初始化执行的顺序是: 全局声明--> init() ---> main()

init()函数执行顺序

Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。

在运行时,被最后导入的包会最先初始化并调用其init()函数, 如下图示:

eUfU3uV.png!mobile

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK