Go基础知识总结

2022-04-08  本文已影响0人  张氏小毛驴

1. 变量

变量的声明有四种方式:

  1. 声明一个变量,默认的初始化值为0:

    var a int

  2. 声明一个变量,初始值为100:

    var a int = 100

  3. 初始化时候省略数据类型,通过值自动推导变量的数据类型:

    var a = 100

  4. 省略掉var关键字,直接自动匹配,但要使用:=

    a := 100

一个注意的点:第四种声明变量的方式a := 100只能在局部方法中使用,全局变量不支持这种写法

多个变量一起声明的写法:

  1. 单行写法

    var a, b int = 100, 200

    var a, b = 100, "abc"

    a, b := 100, "abc"

  2. 多行写法

    var (
     a int = 100
     b string = "abc"
    )
    

匿名变量

go中使用下划线_来作为匿名变量。

go支持函数多返回值,而当我们对于某个函数的返回值是不关心的时候,可以使用匿名变量来接收

比如:fd, _ := os.Open(xxx),对于第二个返回值我们并不想要,就可以直接用_接收

2. 常量

go中常量使用关键字const

定义常量与定义变量方式类似,只是将关键字var换成了const,但常量定义没有:=这种写法

比如:

const a int = 100

const (
    a = 10
    b = 20
)

3. iota关键字

iota用于与const表示枚举类型

go中定义枚举使用的是iotaconst,如下代码,定义一个枚举

const (
    RED = iota
    BLUE
    BLACK
    ....
)

注意:在const中添加一个关键字iota,每一行的iota都会累加1,第一行的iota默认值是0

因此上面的,RED=0,BLUE=1,BLACK=2

但是如果第一行的RED我们赋值为5 * iota,那么RED=5 * 0=0,BLUE=5 * 1=5,BLACK=5 * 2=10

因为每一行的iota自动累加1,每一行相当于是5 * iota

因此有一个常见的实例,使用iota来进行左移运算实现存储单位的常量枚举:

const (
    _ = iota // 赋值给_忽略这个值
    B = 1 << (10 * iota)
    KB
    MB
    GB
    TB
    ...
)

4.函数

go函数是允许有多个返回值的。go的函数定义可以有以下几种写法:

  1. 返回多个值,使用匿名变量

    func test(a string, b int) (int, int) {
        ....
        
        return 100, 200
    }
    
  2. 返回多个值,有参数名称的

    func test(a string, b int) (c, d int) {
        ...
        c = 100
        d = 200
        
        return
    }
    

    注意:

    1. c和d属于test方法的形参,初始值默认为0,他们的作用空间也仅限于test方法,当已经给返回值变量赋值后,可以直接return就好了。

    2. 也可以返回别的变量, 比如内部在定义一个 e := 300,最后 return c, e

5. init函数

init函数是go在每个包初始化后自动执行的,而且在main函数之前执行

因此,init函数常用来:对变量初始化,注册等。

init函数的几个特点:

  1. init函数用于包的初始化,是在package xxxx的时候完成的,在main之前完成

  2. 每个包中是可以拥有多个init函数的,每个包的源文件也是可以有多个init函数的

  3. 不同包的init函数是需要根据包导入的依赖关系决定的(因为init是在package xxx之后完成)

    所以是类似栈的结构,最后的包的init方法先执行

  4. init函数不能被其他函数调用,也不需要传入参数,也无返回值

package main

import "fmt"

func int() {
    fmt.Println("init ok")
}

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

6. import 导包

go中使用import进行导包操作,有几种情况需要了解下:

  1. import _ "fmt"

    这种使用_的方式,是给fmt包起一个别名,是一个匿名,这样子会无法使用包中的方法,但是一旦导包,就会执行包里的init()方法

  2. import aa "fmt"

    这种方式是给fmt包起一个别名aa,调用包中方法时候,就可以使用aa,比如aa.Println()

  3. import . "fmt"

    这种方式是将fmt包中的所有方法全部导入到当前包中,那么fmt包中的所有方法都可以直接当成本包的方法来调用了,不用再加包名fmt(但这样本包就不能定义与fmt包所有函数的函数名相同的函数了)

7. defer

defer关键字是go独有的,是一种延迟语句,在函数return前执行defer。

一个函数中可以添加多个defer语句,执行顺序是逆序的,先定义的defer最后执行

一般defer用于资源的关闭操作比较多。

有个文章可以看看Golang中defer、return、返回值之间执行顺序的坑

结论就是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。

8. 数组

  1. 声明数组的方式

    • var myArray1 [10]int
    • myArray1 := [5]int{1,2,3,4}
  2. 数组长度是固定的

  3. 固定长度的数组在传参的时候,是严格匹配数组类型的

    func add(array [4]int) {
     fmt.Println(array[0], array[1], array[2], array[3])
    }
    func main() {
     arr := [5]int{1, 2, 3, 4}
     add(arr)
    }
    

    这样子传参是不行的,报错:cannot use arr (variable of type [5]int) as type [4]int in argument to add,参数是[4]int类型,传参是[5]int

  4. 需要注意的是,数组是一个值类型,在赋值和作为参数传递时将产生一次复制动作。

9. 数组切片(slice)

数组切片slice,也叫动态数组。

创建数组切片有两种方式:基于数组和直接创建

  1. 基于数组创建

    func main() {
        // 先定义一个数组
        var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10}
        // 基于数组创建一个数组切片
        var mySlice []int = myArray[:5]
    }
    

    注意:go语言支持用myArray[first:last]这样的方式基于数组生成一个数组切片,这种[first,last]是左闭右开的。

    如果基于myArray的所有元素创建数组切片:mySlice := myArray[:]

    基于myArray的前5个元素创建数组切片:mySlice := myArray[:5]

    基于myArray的第5个元素开始到所有元素创建切片:mySlice := myArray[5:]

  2. 直接创建

    使用Go提供的内置函数make(),比如:

    • 创建一个初始元素个数为5的数组切片,元素初始值为0:mySlice := make([]int, 5)

    • 创建一个初始元素个数为5的数组切片,初始值为0,并预留10个元素的存储空间:

      mySlice := make([]int, 5, 10)

元素的遍历

  1. 使用len()函数获取元素个数

    for i := 0; i < len(mySlice); i++ {
        ....
    }
    
  2. 使用range关键字遍历

    for i, v := range mySlice {
        ....
    }// i 是index v是元素值
    

动态增减元素:

  1. 数组切片支持内置函数cap()len()cap()函数返回的是数组切片分配的空间大小,而len()函数返回的是数组切片中当前所存储的元素个数。

  2. 如果需要新增元素,可以使用append()函数,生成一个新的数组切片

    mySlice = append(mySlice, 1, 2, 3)

    注意:

    1. 函数append()的第二个参数开始是一个不定参数,可以添加若干个元素

    2. 也可以将一个数组切片追加到另一个数组切片的末尾

      mySlice2 := []int{8, 9, 10}
      mySlice = append(mySlice, mySlice2...)
      

      这里需要注意,第二个参数mySlice2后面加了三个点,也就是一个省略号,如果没有这个省略号的话会编译错误,因为append方法从第二个参数开始的所有参数都是待添加的元素,加上省略号相当于将mySlice2包含的元素逐个打散再加入

  3. 数组切片扩容的机制

    在append的时候,如果长度增加后超过容量,比如mySlice := make([]int, 3, 4),切片mySlice的容量是4个,当前长度是3个元素,那么在执行append,mySlice = append(mySlice, ,3, 4, 5)后,新增3个元素,加上之前的元素就总共有6个了,超过了容量4,所以这时候切片需要扩容,而扩容的机制就是原始容量的2倍,也就是在新增元素后发现超过了原始的容量的话,会自动以初始容量的2倍去扩容

  4. 切片复制

    使用内置函数copy(),用于将内容从一个数组切片复制到另一个数组切片。

    如果加入的两个数组切片没有一样大,就会按其中较小的那个数组切片的元素个数进行复制。

    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := []int{6, 7, 8}
    
    copy(slice2, slice2) // 只会复制slice1的前三个元素到slice2中
    // slice2 = {1,2,3}  slice1 = {1,2,3,4,5}
    
    copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
    // slice2 = {6,7,8} slice1 = {6,7,8,4,5}
    
  5. 动态数组在传参上是引用传递的,而且不同元素长度的动态数组他们的形参是一致的

    func printArray(myArray []int) {
        ...
    }
    

10. map

  1. map的声明

    var myMap map[int]string

    其中myMap是变量名,int是键的类型,string是值的类型

    只声明没有创建的map还不可用!

  2. map的创建

    使用make()函数创建:myMap = make(map[int]string, 10)

    10表示的是map的容量,与切片的容量类似

  3. map的赋值

    • 可以先声明,再创建,最后赋值

      var myMap map[int]string
      myMap = make(map[int]string, 10)
      myMap[0] = "java"
      myMap[1] = "Go"
      
    • 直接使用:=

      myMap := make(map[int]string)
      myMap[0] = "java"
      myMap[1] = "Go"
      
    • 声明时赋值

      myMap := map[int]string{
          0: "java",
          1: "Go",
      }
      
  4. 元素删除

    使用内置函数delete(),用于删除容器内的元素

    delete(myMap, 0),第二个参数是键,如果这个键不存在,啥也不会发生,也不会有影响。

    但如果传入的map是nil,则会抛出异常panic

  5. 元素查找

    从map中查找一个特定的键,可以使用如下代码:

    value, ok := myMap[1]
    if ok { // 找到了
        ....
    }
    

    只需要查看第二个返回值ok是否为true就知道找没找到,不需要像其他语言那样检查取到的值是不是为nil

11. 面向对象

我们都知道面向对象三个特点:封装,继承,多态。

但是go中并不像其他面向对象语言那样有很多的概念,go语言的面向对象编程是基于语言类型系统的,整个类型系统通过接口串联。

1. 类型系统

go语言中的类型是可以添加方法的,可以给任何类型,包括内置类型增加新方法。比如:

type Integer int

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

// 可以这样使用
func main() {
    var a Integer = 1
    if a.Less(2) {
        fmt.Println(a, "Less 2")
    }
}

上面代码使用type定义了一个新的类型Integer,实质上它就是一个int类型,然后就给这个新类型增加了个新方法Less()。

新增方法这个语法可以以java类的概念来理解为:Integer就是一个类,而a就相当于类中的this,而Less是类里的一个方法,当然a就可以调用到类里的成员了,但是这里的类实质是一个int,所以也就成员变量就是自身int值变量了,但如果a是一个结构体那就有成员变量了。

注意:当我们需要修改到对象的成员时,需要用到指针。比如代码修改为如下:

func (a *Integer) Add(b Integer) {
    *a += b
}

这里需要修改到对象a的值,所以需要用指针引用。

如果没有需要修改对象的值,go并不要求一定要用指针的,有时候对象很小,用指针传递反而不划算

其实上面用指针和不用指针的具体原因,归根结底就是:Go语言的类型是基于值传递的,要修改变量的值,就需要传递指针。

2. 结构体

结构体的定义很简单,基本和C一样:

type Person struct {
    name string
    age int
}

// 新增一个方法
func (p *Person) setAge(age int) {
    p.age = age
}
func (p Person) getAge() {
    return p.age
}

当然,结构体也是go的一种类型,也是可以添加方法的,按我的理解,其实结构体就相当于是面向对象的类,添加的方法就是成员方法,而本身的成员变量就是类中的成员变量。

结构体初始化:

结构体初始化有以下几种实现:

  1. p := new(Person)
  2. p := &Person{}
  3. p := &Person{"zhangsan", 18}
  4. p := &Person{name: "zhangsan", age: 20}

Go语言中没有构造函数这种概念,对象的创建通常做法是交给一个全局的创建函数来完成,以NewXXX命名,表示构造函数:

func NewPerson(name string, age int) *Person {
    return &Person{name, age}
}
3. 封装

回到面向对象三要素,封装,其实结构体就已经是封装的实现了。

这里有个注意的点就是:

类名,属性名,方法名,首字母大写表示对外(也就是其他包)可以访问,否则只能在本包内访问

4. 继承

go语言其实也是提供了继承的,只是采用的是组合的写法,比如以下例子:

// 定义父类
type Animal struct {
    name string
    age int
}

// 父类方法
func (a *Animal) Say() {
    fmt.Println("animal say...")
}

// 定义子类继承父类
type Dog struct {
    Animal
    weight int
}

func main() {
    d := &Dog{}
    
    d.Say()
    
    d.name = "旺财"
    
    fmt.Println(d) 
}

输出:

animal say...
&{{旺财 0} 0}

没有初始化值的变量会默认为对应类型的零值。

5. 多态

在理解go语言的多态之前,得先了解go语言的接口类型。

先来了解下其他语言的接口,在java中,对于接口的实现是必须在实现类中声明要实现的接口的,如果要实现一个接口,需要像下面代码这样编写代码:

// 定义一个接口类
public interface Person {
    // 接口方法
    public void say();
}

// 定义实现类,需要使用关键字implements显式的说明实现哪一个接口
class Teacher implements Person {
    public void say() {
        system.out.println("Hello 我是老师")
    }
}

而在go语言中,一个类只要实现了接口要求的所有函数,就可以说这个类实现了这个接口,当然go中接口使用的关键字还是interface

比如:

有一个File类,并且该类有四个方法,Read(),Write(),Seek(),Close()

type File struct {
    // ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error

然后有以下一些接口:

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类来说都不知道有这些接口的存在,但是在go里,认为File类实现了这些接口。

因此可以这样子进行赋值:

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

实质上,这样子不就是多态么!

接口的赋值:

go语言中接口赋值分为以下两种情况:

接口查询

接口查询可以检查接口所指向的对象实例是否实现了某个接口,从而进行接口转换,比如:

var file Writer = new(File)
if file1, ok := file.(ReadWriter); ok {
    ...
}

这里是Writer接口所指向的对象实例是File类,是实现了ReadWriter的,所以这里ok会为true,file1是ReadWriter接口的实例,所以相当于是从Writer接口转为了ReadWriter接口了。

万能类型

在Go语言中,有这么一种空接口,源码里是这样的:type any = interface{},是一个空接口,根据之前对接口实现的理解,空接口里没有任何方法,那么就可以认为所有的类型其实都是实现了这个接口的,因此这个interface{}可以指向任何对象,称为Any类型,也叫万能类型。

var a interface{} = new(int)
var b interface{} = new(string)
var c interface{} = struct{X int}{1}
a = 10
b = "hello"
fmt.Println(a, b, c) // 输出:10 hello {1}

任何对象实例都实现了interface{},就类似于Java中的Object类一样,那我们就可以用interface{}类型引用任意的数据类型了,像上面的代码那样,这用在函数中传参就很有用了!

类型查询(类型断言)

基于Go语言所有的对象实例都实现了空接口interface{}这个前提,那我们便可以直接了当的询问接口指向的对象实例的类型:xxx.(type)

func test(arg interface{}) {
    switch arg.(type) {
    case int:
        fmt.Println("int type")
    case string:
        fmt.Println("string type")
    default:
        fmt.Println("unknown type")
    }
}

func main() {
    var v1 interface{} = "hello"
    var v2 int = 100
    v3 := struct{ X int }{1}

    test(v1)
    test(v2)
    test(v3)
}

12. 学习文档

《Go语言编程》

B站视频:8小时转职Golang工程师(如果你想低成本学习Go语言)

上一篇下一篇

猜你喜欢

热点阅读