美文网首页
go语言入门

go语言入门

作者: dounine | 来源:发表于2020-02-21 21:50 被阅读0次

    title: go语言入门
    date: 2019-02-12 22:03:27


    前言

    因项目需要和个人喜好,决定系统入门go语言。

    go是由Google开发、开源、强类型的编译型语言。与c语言类似,不同的是,go中每行语句结束不用加 ; :-)

    本笔记主要参考 《Go语言实战》

    后加:本文所基于的GO语言版本较低(1.10),当时还并未支持 Go Modules

    一、Hello world

    编写hello.go文件:

    package main    // 程序入口包
    
    import (
        "fmt"
    )
    
    // 程序入口函数
    func main() {
        fmt.Println("Hello world")
    }
    

    在命令行中输入:go run hello.go

    二、基础语言

    1. 变量

    1.1. 基本变量类型

    // 布尔型
    bool
    
    // 字符串型
    string
    
    // 整型
    int  int8  int16  int32  int64
    uint uint8 uint16 uint32 uint64 uintptr
    
    // byte型
    byte // uint8
    
    // 表示一个 Unicode 码点
    rune // int32 的别名
    
    // 浮点型
    float32 float64
    
    // 复数型
    complex64 complex128
    

    1.2. 变量声明

    特性

    • 使用var关键字声明(像js),变量类型在变量名后
    • 短变量声明 (像python)
    • 使用()一次声明多个变量
    • 若声明的变量没被使用,会报错
    // 一般声明
    var v_name1 int
    var v_name2 = 1     // 根据值自行判别变量类型
    
    // 短变量声明。不能用于声明全局变量
    v_name3 := "hello"
    
    // 多变量声明
    var vname1, vname2, vname3 int
    var (
        ToBe   bool       = false
        MaxInt uint64     = 1<<64 - 1
    )
    
    // 指针变量声明
    var p *int
    v_p := &v_name1
    *v_p = 233
    

    :和c类似,go也分 全局变量(函数外、包内) 和 局部变量(函数内/控制语句内)

    1.3. 变量零值

    变量声明时没有赋予初始值,则默认被赋予零值

    • 布尔型零值:false
    • 字符串型零值:""
    • 数值型零值:0
    • 指针型零值:nil

    1.4. 强制类型转换

    表达式T(v)将值v转换为类型T

    var i int = 42
    var f float64 = float64(i)
    var u uint = uint(f)
    

    1.5. 类型推导

    当不指定数据类型时,系统会自行推导变量类型。如下:

    var i int
    j := i // j 也是一个 int
    
    // 初始值为常量,则取决于常量的精度
    i := 42           // int
    f := 3.142        // float64
    g := 0.867 + 0.5i // complex128
    

    1.6. 变量输出

    使用fmt包中的函数:fmt.Printffmt.Println

    格式化输出fmt.Printf == c语言中的printf%T输出数据的类型,%v输出任意数据的值,%p输出地址数据,%d输出整型数据,等等。

    直接输出fmt.Println == python中的print

    2. 常量

    const关键字常量。可以不指定常量的数据类型。

    const b string = "abc"
    const b = "abc"
    const Pi = 3.14
    

    3. 运算符

    运算符与c语言类似。具体如下,优先级从高到低:

    分类 描述 关联性
    后缀 () [] -> . ++ -- 左到右
    一元 + - ! ~ (type) * & sizeof() 右到左
    乘法 * / % 左到右
    加法 + - 左到右
    移位 << >> 左到右
    关系 < <= > >= 左到右
    相等 == != 左到右
    按位AND & 左到右
    按位XOR ^ 左到右
    按位OR ` ` 左到右
    逻辑AND && 左到右
    逻辑OR ` ` 左到右
    条件 ?: 右到左
    分配 = += -= *= /= %= >>= <<= &= ^= ` =` 右到左
    逗号 , 左到右

    4. 语句

    go中iffor语句不需要加();语句大括号{不需要换行。

    4.1. if语句

    特性

    • 可以初始化变量,仅在if语句中使用。
    // if...else...
    if numA < 20 {
        fmt.Printf("a小于20\n" );
    } else {
        fmt.Printf("numA 不小于 20\n" );
    }
    
    // if语句的分号前面,相当于初始化一个变量,仅在if语句内使用
    if i := 30; numA < i {
        fmt.Println(i)
    }
    

    4.2. switch语句

    特性

    • case语句结束自动break
    • case可以同时匹配多个值,如:case v1,v2,v3:
    • 匹配一个case成功后,可以使用fallthrough强制匹配下一个case
    • switch可以没有条件
    var grade string
    var marks int = 90
    
    // case语句结束自动break。
    // 可以同时case多个值,case v1,v2,v3:
    switch marks {
        case 90: grade = "A"
        case 80: grade = "B"
        case 50,60,70 : grade = "C"
        default: grade = "D"
    }
    fmt.Printf("你的等级是 %s\n", grade );
    
    t := time.Now()
    switch {
        case t.Hour() < 12:
            fmt.Println("Good morning!")
        case t.Hour() < 17:
            fmt.Println("Good afternoon.")
        default:
            fmt.Println("Good evening.")
    }
    
    switch {
        case false:
            fmt.Println("1、case 条件语句为 false")
            fallthrough
        case true:
            fmt.Println("2、case 条件语句为 true")
            fallthrough
        case false:
            fmt.Println("3、case 条件语句为 false")
            fallthrough
        case true:
            fmt.Println("4、case 条件语句为 true")
        case false:
            fmt.Println("5、case 条件语句为 false")
            fallthrough
        default:
            fmt.Println("6、默认 case")
    }
    /*
    输出:
    2、case 条件语句为 true
    3、case 条件语句为 false
    4、case 条件语句为 true
    */
    

    4.3. select语句

    select语句类似于switch语句。

    但是,区别在于:

    • 每个case必须是一个通信操作(数据结构通道的操作),要么是发送要么是接收。
    • select随机执行一个未堵塞的case。如果所有case都堵塞,它将等待,直到有case可以通行。
    var c1, c2, c3 chan int
    var i1, i2 int
    // 随机执行一个case,若所有case都堵塞,则直到有case可以通行为止
    select {
        case i1 = <-c1:
            fmt.Println("received ", i1, " from c1")
        case c2 <- i2:
            fmt.Println("sent ", i2, " to c2")
        case i3, ok := (<-c3):  // same as: i3, ok := <-c3
            if ok {
                fmt.Println("received ", i3, " from c3")
            } else {
                fmt.Println("c3 is closed")
            }
        default:
            fmt.Println("no communication")
    }
    /*
    输出:
    no communication
    */
    

    4.4. for语句

    特性

    • for只带条件判断相当于while
    • for中使用range关键字,可以遍历顺序结构,返回keyvalue
    for i := 0; i < 10; i++ {
        fmt.Printf("i 的值为: %d\n", i)
        if i > 5{
            break
        }
    }
    // 相当于c的while
    for numA < numB {
        numA++
        fmt.Printf("numA 的值为: %d\n", numA)
    }
    numbers := [6]int{1, 2, 3, 5}
    // range关键字,可以对 slice、map、数组、字符串等进行迭代
    for key, value := range numbers {
        fmt.Printf("第 %d 位 x 的值 = %d\n", key, value)
    }
    

    5. 函数 引用-数据类型

    go语言的一大“神奇”特点,就是喜欢把原本前面的东西放到后面,函数也不例外。

    特性

    • 函数以关键字func进行声明
    • 返回类型(和值),放在参数项的后面
    • 允许先声明返回值
    • 多值返回
    • deferdefer语句会将指定函数推迟到外层函数返回之后再执行。并且,被推迟的函数将被压入一个栈中。
    //==========函数============
    func add(x int, y int) int {
        return x + y
    }
    
    // 先声明返回值,直接用return返回。适用于短函数
    func subtract(x int, y int) (z int) {
        z = x - y
        return
    }
    
    // 返回多值
    func swap(x, y string) (string, string) {
        return y, x
    }
    /*
    使用:
    stringA, stringB = swap(stringA, stringB)
    */
    
    // 传指针
    func swap_p(x, y *int) {
        var temp int
        temp = *x    /* 保持 x 地址上的值 */
        *x = *y      /* 将 y 值赋给 x */
        *y = temp    /* 将 temp 值赋给 y */
    }
    
    // defer栈
    func deferTest() {
        fmt.Println("counting")
    
        for i := 0; i < 10; i++ {
            defer fmt.Println(i)
        }
    
        fmt.Println("done")
    }
    /*
    输出
    counting
    done
    9
    8
    7
    6
    5
    4
    3
    2
    1
    0
    */
    

    另外,跟JavaScript一样,go语言也有:

    • 函数变量,函数也是属于一类数据类型。
    • 函数做参。多态性的一种体现。
    • 函数作为返回值(函数闭包)。得以使用函数的局部变量。
    • 匿名函数
    • 函数自调用
    // 函数做参。函数参数,要指明函数参数的参数类型和返回值类型
    func handleNum(num float64, fn func (float64) float64) {
        fmt.Println("函数做参开始")
        fmt.Println(fn(num))
        fmt.Println("函数做参结束")
    }
    
    // 闭包函数(将函数作为返回值)
    func getSequence() func() int {
        i:=0
        return func() int {
            i+=1
            return i
        }
    }
    
    func main() {
        // 函数变量
        getSquareRoot := func(x float64) float64 {
            return math.Sqrt(x)
        }
        fmt.Println(getSquareRoot(9))
    
        // 函数做参
        handleNum(16, getSquareRoot)
    
        // 函数闭包
        nextNumber := getSequence()     // nextNumber 为一个函数,函数 i 为 0
        fmt.Println(nextNumber())       // 调用 nextNumber 函数,i 变量自增 1 并返回
        fmt.Println(nextNumber())
        fmt.Println(nextNumber())
    
        nextNumber1 := getSequence()    // 创建新的函数 nextNumber1,并查看结果
        fmt.Println(nextNumber1())      // 输出 1
        fmt.Println(nextNumber1())      // 输出 2
    
        // 匿名函数和函数自调用
        func (count int) {
            fmt.Println("匿名函数开始")
            for i := 0; i < count; i++ {
                fmt.Println(i)
            }
            fmt.Println("匿名函数结束")
        } (3)
    }
    

    输出:

    函数做参开始
    4
    函数做参结束
    1
    2
    3
    1
    2
    匿名函数开始
    0
    1
    2
    匿名函数结束
    

    6. 包

    6.1. 包的特性

    java相似,每个.go文件开头需要用package关键字声明文件所属于的。并且,包的名字需要与目录名字相同。main包除外。

    例如,项目中有一个routers/目录,routers/目录下有一个router.go文件,那router.go文件开头必须声明所属于的包:

    package routers
    

    6.2. main包

    每个程序(项目)都必须有一个main包,编译器会根据main包找到main()函数,这是程序的入口函数。若没找到main()函数,程序则不会执行。

    6.3. 导入包

    import关键字用于导入一个外包。格式如下:

    import "fmt"
    

    或者导入多个包时:

    import (
        "fmt"
        "math"
        "net/http"
    )
    

    根据以上包名,编译器会依次在以下目录中查找:

    1)GOROOT/src,安装路径下的src目录
    2)GOPATH/src,工作空间下的src目录
    3)若以上都没找到,且包路径中包含URL,那么会从网上获取包,并保存到GOPATH/src目录下。比如:

    import "github.com/99MyCql/chatRoom/routers"
    

    6.4. 命名导入

    导入包的名字默认为包名,但如果出现重名情况,我们可以通过给包重新命名来化解。

    import (
        "fmt"
        myfmt "mylib/fmt"   // myfmt为该包的新名字
    )
    

    go语言中若导入了某包(会调用该包中的init()函数),而又没使用该包,编译器则会报错。

    解决这个问题可以使用空白标识符_来重命名这个包,表明导入该包却不使用该包。如:

    import "github.com/99MyCql/chatRoom/routers"
    

    6.5. init()函数

    一个包中,可以有一个或多个init()函数(多个init()函数不能在同一个.go文件中)。

    init()函数会在main()函数执行前被调用。

    每个被导入的包(不管有没有被使用),都会调用包中的所有init()函数。通常,init()函数被用来进行一些初始化操作。

    使用空白标识符_,可以让包中的init()函数被调度使用,同时编译器不会因为包没被使用而报错。

    6.6. 包中名的可见性(special)

    在一个包内,所有文件的全局变量是共享的。

    对于包外,以大写字母开头的全局变量和函数是公开的,以小写字母开头的私有的。如:

    • fmt.Println() 是调用fmt包中公开的Println()函数。
    • fmt.Println(math.pi) 输出math包中变量,会报错,因为该变量是私有的。

    6.7. 使用另一个包中的变量和函数

    通过 <package name>.<var/fun> 格式(跟C++使用类中变量函数相似),来使用另一个包中公开的变量和函数。如:

    fmt.Println()   // 使用 fmt 包中 Println() 函数
    math.PI         // 使用 math 包中 PI 变量
    

    三、进阶数据结构

    1. 指针 值-数据类型

    与c语言指针类似,go指针指向对应类型的变量。

    但不同的是:

    • go语言指针不能进行运算

    • 指针变量的声明中,标识符*必须贴近变量类型,而不贴近变量名。如:var name *T

    • 指针类型的零值为nil

    // 指针变量声明
    var p *int
    v_p := &v_name1
    *v_p = 233
    

    2. 数组 值-数据类型

    go语言数组跟c语言的相似,但也有不同。

    特性

    • 格式var name [len]T,如:var a [2]string

    • go语言的数组是一种数据类型,而且是一种值类型。即数组名是一个值,包含着整个数组的数据

    • 需要编译器自己识别数组长度时,不能使[]中空闲,而必须使用[...]

    • 指向数组的指针格式为:var name *[len]T,如:var arrp *[5]int

    //====================数组=====================
    // 变量 a 是一个值类型,而不是引用类型。包含着整个数组的数据
    var a [2]string
    a[0] = "Hello"
    a[1] = "World"
    fmt.Println(a[0], a[1])
    fmt.Println(a)
    
    // 使用字面量声明数组
    array1 := [5]int{10,20,30,40,50}
    array2 := [...]int{1,2,3,4,5}   // ... 可以使编译器根据元素数量,自动确定数组长度
    fmt.Println(array1, array2)
    
    // 数组元素类型为指针
    array3 := [5]*int{0:new(int), 1:new(int)}   // 用 下标:... 进行特定位置的初始化
    *array3[0] = 10
    *array3[1] = 20
    fmt.Println(*array3[0])
    fmt.Println(array3)
    
    // 多维数组
    var array4 [4][2]int    // 4行2列的二维数组,即有4行,每行有2个int
    
    // 指向数组变量的指针
    arrP := &a
    fmt.Printf("a 's type is %T\n", a)
    fmt.Printf("arrp 's type is %T\n", arrP)
    (*arrP)[1] = "Today"    // arrP[1] = "Today" 也可以
    fmt.Println(*arrP)
    
    // new 指向数组的指针
    var arrp *[5]int    // 此时为nil
    fmt.Println(arrp)   // 输出 nil
    arrp = new([5]int)  // 分配相应的数组空间,并返回指针
    fmt.Println(arrp)   // 输出 &[0 0 0 0 0]
    
    // 与c语言的不同,go的数组名是值而不是指针。以下按照c语言的思路,在go中是错误的
    // var p *int
    // p = a
    
    // 数组是值类型而不是引用类型的示例
    arrA := [2]int{1,2}
    arrB := arrA    // 相当于赋值了整个数组的值
    arrB[1] = 3
    fmt.Println(arrA, arrB) // 输出 [1 2] [1 3]
    

    输出:

    Hello World
    [Hello World]
    [10 20 30 40 50] [1 2 3 4 5]
    10
    [0xc42008a018 0xc42008a030 <nil> <nil> <nil>]
    [[0 20] [0 0] [0 60] [0 0]]
    a 's type is [2]string
    arrp 's type is *[2]string
    [Hello Today]
    <nil>
    &[0 0 0 0 0]
    [1 2] [1 3]
    

    注意

    由于在go语言中,数组类型是值类型而不是引用类型。所以,在函数传参时,我们需要传入指向数组的指针,而不是数组值。但,若是希望拿到该数组的副本,则可以选择使用传入值。

    同时,指向数组的指针还必须指明数组的长度,这其实十分不方便。但切片可以很好地解决这个问题。

    // 10的6次方数组
    var array [1e6]int
    foo(&array)
    
    // 函数接受一个指向包含100万个整型值数组的指针
    func foo(array *[1e6]int) {
        ...
    }
    

    3. 切片 引用-数据类型

    切片是go自带的数据类型,围绕动态数组的概念来构建。

    同时,切片是一个引用类型,所以切片的零值为nil

    3.1. 切片的内部实现

    切片其实是一个很小的结构体,对底层数组进行了抽象。“切片结构体”包含三个属性:

    • 指向底层数组的指针。底层数组会一直存在,直到没有指向它的切片

    • 切片的长度。动态数组的长度

    • 切片的容量。容量相当于动态数组的长度上限

    3.2. 切片的创建和初始化

    • 格式为:name []T。注意[]中无值,有值为数组

    • 未初始化的切片为“空指针”,零值为nil

    • make关键字创建,还可以声明切片的长度和容量。推荐

    • 通过数组或切片[x:y]来创建切片(包含x位元素,排除y位元素),可以使用[x:][:y]等,还可以通过[x:y:z]规定切片的容量(z)

    // 切片的创建和初始化
    // 注意切片和数组的区别
    var slice1 []int                // nil切片(空指针),指向底层数组的指针为空
    if slice1 == nil {
        fmt.Println("slice1 is nil")
    }
    slice2 := []int{0,1,2,3,5:6}    // 创建并初始化,跟数组很像
    slice3 := make([]int, 0)        // 空切片(不是nil切片),长度和容量为0
    slice4 := make([]string, 5)     // 用make创建字符串切片,长度和容量都为5
    slice5 := make([]int, 3, 5)     // 长度为3,容量为5
    fmt.Println(slice1, slice2, slice3, slice4, slice5)
    
    arr := [5]int{0,1,2,3,4}
    arrSlice1 := arr[1:3]           // 用数组创建切片。此时切片指向该数组下标1位置,并且长度为2、容量为4(下标1到原数组结束)
    arrSlice2 := arr[1:3:3]         // 规定容量为3(容量不可超过原数组)。此时,arrSlice1和arrSlice2共享同一个底层数组
    newSlice1 := slice2[1:3]        // 用切片构建切片。两个切片共享一个底层数组
    newSlice2 := newSlice1
    fmt.Println(arr, arrSlice1, arrSlice2, newSlice1, newSlice2)
    

    输出:

    slice1 is nil
    [] [0 1 2 3 0 6] [] [    ] [0 0 0]
    [0 1 2 3 4] [1 2] [1 2] [1 2] [1 2]
    

    3.3. 切片的使用

    • len()函数获取切片长度,cap()函数获取切片容量

    • func copy(dst, src []T) intsrc切片的内容拷贝到dst切片中,拷贝的长度为两个slice中长度较小的长度值

    • func append(s []T, x ...T) []T 返回一个新切片。当原切片容量不足时,append函数会创建一个新的容量更大的底层数组,并将原切片的底层数组复制到新数组里,再追加新的值。append(dist, x, y)追加多个值(x,y...)到dist切片。append(dist, src...)将整个src切片追加到dist切片尾。

    • 切片的多维和遍历/迭代,与数组一样

    // len() 和 cap()
    fmt.Printf("slice5 is %v, len is %d, capacity is %d\n", slice5, len(slice5), cap(slice5))
    fmt.Printf("arrSlice1 is %v, len is %d, capacity is %d\n", arrSlice1, len(arrSlice1), cap(arrSlice1))
    
    // func copy(dst, src []T) int
    copy(slice5, arrSlice1)     // copy()函数会根据长度复制
    fmt.Printf("slice5 is %v, len is %d, capacity is %d\n", slice5, len(slice5), cap(slice5))
    
    // func append(s []T, x ...T) []T   返回一个新切片
    // 当追加后,目标切片长度超过容量时,append函数会创建一个新的容量更大的底层数组,将原本数组复制到新数组中,再追加新的值
    slice1 = append(slice1, 10)
    fmt.Printf("slice1 is %v, len is %d, capacity is %d\n", slice1, len(slice1), cap(slice1))
    slice1 = append(slice1, slice2...)  // 用标识符 ... 将整个切片追加到另一个切片
    fmt.Printf("slice1 is %v, len is %d, capacity is %d\n", slice1, len(slice1), cap(slice1))
    

    输出:

    slice5 is [0 0 0], len is 3, capacity is 5
    arrSlice1 is [1 2], len is 2, capacity is 4
    slice5 is [1 2 0], len is 3, capacity is 5
    slice1 is [10], len is 1, capacity is 1
    slice1 is [10 0 1 2 3 0 6], len is 7, capacity is 8
    

    3.4. 切片的“陷阱”

    切片赋值后,两个切片会共享同一个底层数组,一个切片修改值时会影响到另一个数组。切片共享底层数组示例图:

    [图片上传失败...(image-b37feb-1601651171851)]

    // 切片赋值后,会共用一个底层数组
    sliceA := []string{"hello", "world", "!", "!", "!"}
    fmt.Printf("sliceA is %v\n", sliceA)
    sliceB := sliceA[:3]
    sliceB[2] = "?"         // 由于sliceB和sliceA共享一个底层数组,通过sliceB修改底层数组,会影响到sliceA
    fmt.Printf("sliceB is %v\n", sliceB)
    fmt.Printf("sliceA changs : %v\n", sliceA)
    

    输出:

    sliceA is [hello world ! ! !]
    sliceB is [hello world ?]
    sliceA changs : [hello world ? ! !]
    

    4. 映射 引用-数据类型

    映射又称map、键值对,基于特定的hash函数/散列函数。

    映射也是引用类型,零值为nil

    4.1. 映射的创建和初始化

    • 格式:name map[keyT]valueT

    • 未初始化的声明会创建nil映射。nil 映射既没有键,也不能添加键

    • make()函数进行创建,产生空映射而非nil映射

    • 用字面量初始化声明映射,采用换行的形式,需要在最后一个键值对后加 ,

    // 映射的创建和初始化
    var dictNil map[string]int  // 声明了一个nil映射,nil 映射既没有键,也不能添加键
    if dictNil == nil {
        fmt.Println("dictNil is nil")
    }
    // dictNil["red"] = 1   运行时会报错
    dict1 := make(map[string]int)    // 用make()函数创建的map,是空映射,而不是nil映射。映射/键值对中键的类型不能是切片、函数等引用数据类型
    dict1["red"] = 1
    dict2 := map[string]string {
        "Red": "#da1337",
        "Orange": "#e95a22",    // 最后一行需要加 ,
    }
    fmt.Println(dict1, dict2)
    

    输出:

    dictNil is nil
    map[red:1] map[Orange:#e95a22 Red:#da1337]
    

    4.2. 映射的操作

    • 获取值:value=map[key]。通过双赋值检测某个键是否存在:value, ok = map[key],若keymap中,oktrue;否则,okfalse;若key不在映射中,那么value是该映射元素类型的零值。

    • 增加键值对:map[key]=value

    • 删除键值对,用delete()函数:delete(map, key)

    fmt.Println(dict1["red"])   // 获取键值对
    dict1["blue"] = 2           // 增加键值对
    fmt.Println(dict1)
    delete(dict1, "red")        // 删除键值对
    fmt.Println(dict1)
    

    输出:

    1
    map[red:1 blue:2]
    map[blue:2]
    

    5. 结构体 值-数据类型

    • 结构体类型使用关键字struct进行定义,用type进行命名,如:type user struct{}

    结构体定义:

    type user struct {
        name    string
        email   string
        age     int
    }
    
    type admin struct {
        person  user    // 嵌套另一个结构体
        level   int
    }
    

    结构体使用:

    var bill user       // 初始化后,结构体中所有成员都会被赋零值
    // 短变量声明
    lisa := user{
        name:   "Lisa",
        email:  "lisa@email.com",
        age:    19,     // 采用换行形式时最后一个也需要加 ,
    }
    tom := user{name:"Tom"} // 单独声明某一个成员
    fmt.Println(bill, lisa, tom)
    
    ad := admin{lisa, 10}   // 直接按照结构体成员顺序,传入对应的值
    adP := &ad              // 获取指向结构体的指针
    fmt.Println(ad, adP)
    
    fmt.Println(ad.person.name)  // 结构体值访问内部成员
    fmt.Println(adP.person.name) // 结构体指针访问内部成员
    

    输出:

    {  0} {Lisa lisa@email.com 19} {Tom  0}
    {{Lisa lisa@email.com 19} 10} &{{Lisa lisa@email.com 19} 10}
    Lisa
    Lisa
    

    6. new 和 make

    在go语言中有两个用于内存分配的函数:newmake

    6.1. new

    函数原型:

    func new(Type) *Type
    

    说明:

    主要给值类型数据分配空间,并返回指向该数据空间的指针。这与c语言中malloc类似。但是,new()函数不能指定个数和大小,只能传入指定的数据类型(包括用户自定义的数据类型)。

    示例:

    // var i *int
    // *i=10    错误,野指针
    var i *int
    i = new(int)
    arrP := new([5]int)     // 分配长度为5的数组空间,并返回数组指针
    

    6.2. make

    函数原型:

    func make(t Type, size ...IntegerType) Type
    

    说明:

    make也是用于内存分配的,但是和new不同,它只用于chanmap以及slice的内存创建,而且它返回的类型值,而不是他们的指针。同时,make()函数还能对这三个类型的相关属性进行初始化。

    示例:

    slice := make([]int, 3, 5)     // 长度为3,容量为5,单位为int的slice
    dict := make(map[string]int)   // 键为string,值为int类型的map
    buffer := make(chan string, 10)// 缓冲为10的字符串类型通道
    

    7. 浅谈引用类型

    在Go语言中,引用类型可以看作一个指针,它并不包含实际数据。比如,切片 slice 只是一个如下的类型:

    type Slice struct {
        point Point // 指向底层数据的指针
        len int     // 底层数据的长度
        cap int     // 底层数据的容量(最大长度)
    }
    
    

    当引用类型作为函数参数时,你可以通过引用类型修改所指向的数据(退出函数后依然有效)。但是,你不可以修改引用类型本身(退出函数后修改无效)。

    map 为例:

    func mapAdd(m map[string]interface{}) {
        m["name"] = "dounine"
    }
    
    func mapAdd2(m map[string]interface{}) {
        mp := map[string]interface{}{
            "name": "dounine",
        }
        m = mp
    }
    
    func main() {
        data3 := make(map[string]interface{})
        mapAdd(data3)
        fmt.Println("type:", reflect.TypeOf(data3), "; value:", reflect.ValueOf(data3))
    
        data4 := make(map[string]interface{})
        mapAdd2(data4)
        fmt.Println("type:", reflect.TypeOf(data4), "; value:", reflect.ValueOf(data4))
    }
    

    输出:

    type: map[string]interface {} ; value: map[name:dounine]
    type: map[string]interface {} ; value: map[]
    

    再以 slice 为例:

    func sliceAdd(s []int) {
        s = append(s, 6)
    }
    
    func sliceUpdate(s []int) {
        s[0] = 1
    }
    
    func main() {
        slice1 := []int{0, 1, 2, 3, 5}
        sliceAdd(slice1)
        fmt.Println(slice1)
    
        sliceUpdate(slice1)
        fmt.Println(slice1)
    }
    

    输出:

    [0 1 2 3 5]
    [1 1 2 3 5]
    

    原因是:append 函数会修改 slice 类型本身的 len 属性,退出函数后失效;而修改 slice 类型指向的数组的值,退出函数后依然有效。

    四、面向对象

    go语言中是没有关键字class的,也就是说,go语言中没有类也没有继承。但,go却是一个面向对象的语言,那它究竟如何实现面向对象呢?

    首先,go通过结构体的成员来定义类的属性,结构体名即类名;

    其次,通过语法格式让函数与结构体关联,实现类方法

    然后,通过关键字interface与结构体结合,实现接口和多态

    接着,通过结构体实名内嵌的形式,来实现对象内嵌另一个对象的has-a模式;

    最后,通过结构体匿名域内嵌的形式,来实现“继承”,即is-a模式。

    各功能的具体实现,下文一一讲解。

    1. 类方法

    1.1. 定义

    格式:

    // 用户类型
    type user struct {
        name  string
        email string
    }
    
    // notify 方法,以值为接收者
    func (u user) notify() {
        fmt.Printf("Sending User Email To %s<%s>\n",
            u.name,
            u.email)
    }
    
    // changeEmail 方法,以指针为接收者
    func (u *user) changeEmail(email string) {
        u.email = email
    }
    

    说明:

    • 一个类型的方法的声明,必须跟类型在同一个包内。

    • 方法的声明与函数类似,不同的是,需要在func与方法名之间加上接收者参数,指明方法所从属的类型。接收者有两种:值接收者指针接收者

    1.2. 调用类方法

    调用类型的方法:<类型值/指针>.<方法>,如:boss.notify()不管是值类型,还是指向类型的指针,都使用这种格式

    示例:

    // 值类型
    boss := user{
        name: "aaaaa",
        email: "123456",
        age: 20,
    }
    boss.notify()
    boss.changeEmail("2222")    // go语言隐式转换,(&boss).changeEmail()
    
    // 指针类型
    bossP := &boss
    bossP.notify()              // 隐式转换,(*bossP).notify()
    bossP.changeEmail("1111")
    

    1.3. 指针接收者和值接收者

    类型的值 使用 指针接收者声明的方法,和 类型的指针使用 值接收者声明的方法时,go语言都会进行隐式转换。所以,不管是以什么接收者声明的方法,值类型和指针类型都能调用。

    示例:

    boss := user{
        name: "aaaaa",
        email: "123456",
        age: 20,
    }
    boss.notify()
    boss.changeEmail("2222")    // go语言隐式转换,(&boss).changeEmail()
    fmt.Println(boss)
    
    bossP := &boss
    bossP.notify()              // 隐式转换,(*bossP).notify()
    bossP.changeEmail("1111")
    fmt.Println(bossP)
    

    值接收者指针接收者 的区别:

    • 值接收者得到类型的副本,修改副本值不会对原本值起作用;
    • 指针接收者得到指向类型值的指针,所以,在指针接收者的方法中修改类型数据,会影响到原本的值

    示例:

    package main
    
    import (
        "fmt"
        "math"
    )
    
    type Vertex struct {
        X, Y float64
    }
    
    // 值接收者
    func (v Vertex) Scale1(f float64) {
        v.X = v.X * f
        v.Y = v.Y * f
    }
    
    // 指针接收者
    func (v *Vertex) Scale2(f float64) {
        v.X = v.X * f
        v.Y = v.Y * f
    }
    
    func main() {
        v := Vertex{3, 4}
        fmt.Println(v)      // 输出 {3,4}
        v.Scale1(10)
        fmt.Println(v)      // 输出 {3,4}
        v.Scale2(10)
        fmt.Println(v)      // 输出 {30,40}
    }
    

    2. 接口 引用-数据类型

    2.1. 声明

    接口是一系列方法的集合。它的格式如下:

    type <接口名> interface {
        方法1名(方法参数) 方法返回值
        ...
    }
    

    同时,也可以组合(嵌入)其它接口形成新接口

    type <接口名> interface {
        接口名1
        接口名2
        ...
        方法1名(方法参数) 方法返回值
        ...
    }
    

    嵌入进来的接口,相当于把它的方法都复制到新接口中。

    2.2. 实现接口

    如果想要某个类型实现某个接口,只需要将接口中所有方法实现为类方法即可。示例:

    type I interface {
        M()
        N()
    }
    
    type T struct {
        S string
    }
    
    // 以下两个方法意味着: type T implements the interface I
    func (t T) M() {
        fmt.Println(t.S)
    }
    func (t T) N() {
        fmt.Println(t.S)
    }
    

    2.3. 使用接口

    如果某个类型实现了某个接口类型的所有方法,那么就可以将该类型的值或指针赋给这个接口类型的值

    但要注意

    • 值类型 和 指针类型 能使用 值接收者 实现的方法;
    • 但是,值类型 不能使用 指针接收者 实现的方法,指针类型 才能使用。

    示例如下:

    // 接口 I
    type I interface {
        M()
    }
    
    // 类型 T
    type T struct {
        S string
    }
    
    // 值接收者 实现的方法
    func (t T) M() {
        fmt.Println(t.S)
    }
    
    func main() {
        // 值类型 使用 值接收者 实现的方法
        var i I = T{"hello"}
        i.M()
        // 指针类型 使用 值接收者 实现的方法
        var i I = &T{"hello"}
        i.M() // 隐式转换:(*i).M()
    }
    
    // 接口 I
    type I interface {
        M()
    }
    
    // 类型 T
    type T struct {
        S string
    }
    
    // 指针接收者 实现的方法
    func (t *T) M() {
        fmt.Println(t.S)
    }
    
    func main() {
        // 值类型 不能使用 指针接收者 实现的方法
        // Error: cannot use T literal (type T) as type I in assignment:
        // T does not implement I (M method has pointer receiver)
        var i1 I = T{"hello"}
        i1.M()
        // 指针类型 才能使用 指针接收者 实现的方法
        var i2 I = &T{"hello"}
        i2.M()
    }
    

    因此,我们通常定义类型指针来操作类型。

    2.4. nil接口

    如果没有为接口赋值,而调用接口中的方法,那将会报错,因为接口值为nil

    type I interface {
        M()
    }
    
    func main() {
        var i I
        // panic: runtime error: invalid memory address or nil pointer dereference
        i.M()
    }
    

    2.5. 空接口 - 泛型

    空接口相当于C++中的泛型。格式如下:

    interface{}
    

    使用示例:

    package main
    
    import "fmt"
    
    func main() {
        var i interface{}
        describe(i) // 输出:(<nil>, <nil>)
    
        i = 42
        describe(i) // 输出:(42, int)
    
        i = "hello"
        describe(i) // 输出:(hello, string)
    }
    
    func describe(i interface{}) {
        fmt.Printf("(%v, %T)\n", i, i) // %v:变量的值,%T:变量的类型
    }
    

    2.6. 类型断言与空接口(泛型)

    类型断言可以判断变量是否为该类型。格式如下:

    t := i.(T)
    

    若不是,将报错中断程序。如果不想中断,则使用如下格式:

    t, ok := i.(T)
    

    若不是,则ok值为false

    使用示例:

    func main() {
        var i interface{} = "hello"
    
        s := i.(string)
        fmt.Println(s)
    
        s, ok := i.(string)
        fmt.Println(s, ok)
    
        f, ok := i.(float64)
        fmt.Println(f, ok)
    
        f = i.(float64) // panic: interface conversion: interface {} is string, not float64
        fmt.Println(f)
    }
    

    实现一个类型判断函数:

    func do(i interface{}) {
        switch v := i.(type) {
            case int:
                fmt.Printf("Twice %v is %v\n", v, v*2)
            case string:
                fmt.Printf("%q is %v bytes long\n", v, len(v))
            default:
                fmt.Printf("I don't know about type %T!\n", v)
        }
    }
    

    2.7. 自定义输出格式 - Stringers 接口

    Stringers接口定义如下:

    type Stringer interface {
        String() string
    }
    

    它允许用户自定义变量打印格式。示例如下:

    type Person struct {
        Name string
        Age  int
    }
    
    // type Person implements the interface Stringer
    func (p Person) String() string {
        return fmt.Sprintf("%v (%v years)", p.Name, p.Age) // 返回一个字符串
    }
    
    func main() {
        a := Person{"Arthur Dent", 42}
        z := Person{"Zaphod Beeblebrox", 9001}
        fmt.Println(a, z) // 输出:Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
    }
    

    2.8. 自定义错误处理 - error 接口

    error接口定义如下:

    type error interface {
        Error() string
    }
    

    使用如下:

    import (
        "fmt"
        "time"
    )
    
    type MyError struct {
        When time.Time
        What string
    }
    
    // type MyError implements the interface error
    func (e *MyError) Error() string {
        return fmt.Sprintf("at %v, %s",
            e.When, e.What)
    }
    
    // 返回一个 error 接口类型变量
    func run() error {
        return &MyError{
            time.Now(),
            "it didn't work",
        }
    }
    
    func main() {
        if err := run(); err != nil {
            fmt.Println(err) // at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work
        }
    }
    

    3. 嵌入类型 - 继承

    3.1. 声明

    嵌入类型将已有类型直接声明在新的结构里,新的类型被称为外部类型,被嵌入的类型被称为内部类型。如下:

    type <类型名> struct {
        内部类型1名
        ...
        属性1名
        ...
    }
    

    3.2. 创建

    注意:创建时,依然需要区分出内部类型。因为外部类型有可能会覆盖内部类型中的标识符。 示例如下:

    type user struct {
        name  string
        email string
    }
    
    type admin struct {
        user    // 嵌入 user 类型,相当于 admin 继承了 user 类型
        level string
    }
    
    func main() {
        // Error: cannot use promoted field user.name in struct literal of type admin
        // Error: cannot use promoted field user.email in struct literal of type admin
        // ad := admin{
        //     name:  "john",
        //     email: "qq.com",
        //     level: "1",
        // }
    
        // 创建类型,需要区别出内部类型
        ad := admin{
            user: user{
                name:  "john",
                email: "qq.com",
            },
            level: "1",
        }
    }
    
    

    3.2. 继承属性和方法

    内部类型中的标识符(属性和方法)都会提升到外部类型中,就像直接在外部类型中声明了一样。

    延续3.1中的例子:

    // 可以通过内部类型访问内部类型的属性
    fmt.Println(ad.user.name)   // 输出:john
    // 也可以直接访问内部类型的属性
    fmt.Println(ad.name)        // 输出:john
    

    3.3. 覆盖属性和方法

    外部类型也可以通过声明与内部类型同名的标识符,来覆盖内部标识符的属性或方法。这样,内部类型中对应的标识符将不会被提升,但其值依然存在。

    示例:

    type user struct {
        name  string
        email string
    }
    
    type admin struct {
        user
        name string // 覆盖 user 类型中的 name 属性
        level string
    }
    
    func main() {
        // cannot use promoted field user.name in struct literal of type admin
        // cannot use promoted field user.email in struct literal of type admin
        ad := admin{
            user: user{
                name:  "john",
                email: "qq.com",
            },
            name: "tom",
            level: "1",
        }
    
        fmt.Println(ad.name)        // 输出:tom
        fmt.Println(ad.user.name)   // 输出:john
    }
    

    4. 公开或未公开的标识符 - 私有与公有

    要使用另一个包中的类型时,类型名首字母需要大写,调用格式为:<package>.<name>(package为包名,name为类型名)。

    若要调用公开类型中的属性和方法时,属性和方法名的首字母也必须是大写。

    示例:

    package main
    
    import (
        "fmt"
        "study/my_study/obj" // 导入另一个包
    )
    
    func main()  {
        // Person 为 study/my_study/obj 包中的类型
        boss := obj.Person{
            Name: "aaaaa",
            Email: "123456",
        }
    
        // 调用 Person 类型的公开方法
        boss.Notify()
        boss.ChangeEmail("2222")
        fmt.Println(boss)
    }
    

    相关文章

      网友评论

          本文标题:go语言入门

          本文链接:https://www.haomeiwen.com/subject/czghqhtx.html