ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Go基础知识总结

2022-04-08 11:35:33  阅读:151  来源: 互联网

标签:总结 func int 接口 基础知识 var 数组 Go go


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方法先执行

    init.jpg

  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 file1 IFile = new(File)

  • 将一个接口赋值给另一个接口

    在go语言中,只要两个接口有相同的方法列表(次序不要求),那么它们就是等同的,可以相互赋值。

    接口的赋值也不要求必须等价,如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A,而接口A无法赋值给接口B,因为接口B中并没有接口A中的其他方法,如果赋值给接口A了,当接口A调用一个存在于接口A而接口B不存在的方法,那就找不到了

    比如:

    假设有一个Writer接口和ReadWriter接口,实体类还是上面的File类

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

    可以将ReadWriter接口的实例赋值给Writer接口:

    var file ReadWriter = new(File)
    // 接口ReadWriter 赋值给 接口Writer
    var file1 Writer = file
    // 这样子是可以的,这样file1是Writer接口的实例,只有一个Write方法可以调用是正常的
    

    但是反过来就不行了:

    var file Writer = new(File)
    // 接口ReadWriter 赋值给 接口Writer
    var file1 ReadWriter = file
    // 这样子是不可以的,这样file1是ReadWriter接口的实例,当file1调用read方法时候,并没有这个方法,因为他实质是Writer接口类型
    

接口查询

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

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语言)

标签:总结,func,int,接口,基础知识,var,数组,Go,go
来源: https://www.cnblogs.com/LucasBlog/p/16116031.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有