Golang学习笔记

一、Golang的学习方向及应用领域

学习方向:

 

 

 Golang的应用领域:

1. 区块链的应用开发

2. 后台的服务系统

3. 云计算/云服务后台应用

 

二、Golang简介

  • 核心开发团队-三个大牛

  • Google 创造 Golang的原因

  • 发展历程

      

    的学习过程使用的是1.9版本,开发工具前期使用‘VSCode’ 熟练后使用‘jetbrains-golang’

  • Go语言的特点

                  

                 

 

三、Golang的开发工具

 

 现在JetBrains公司已经有了Golang专门的IDE工具了。

 

开发工具前期我使用‘VSCode’ 熟练后使用‘jetbrains-golang’

 

四、Windows下搭建Go开发环境-安装和配置SDK

 

 

 

 

 如何配置环境变量:

 

 

 

五、Linux下搭建Go开发环境-安装和配置SDK

  • Linux下安装SDK

     如何确定Linux是多少位?

     :

    step1:下载go SDK--》https://golang.google.cn/dl/

    wget https://dl.google.com/go/go1.9.2.linux-amd64.tar.gz
    或者下载后上传到Ubuntu

    step2:将go1.9.2.linux-amd64.tar.gz 拷贝到/opt目录下

    cp go1.9.2.linux-amd64.tar.gz /opt

    step3:到/opt下解压go1.9.2.linux-amd64.tar.gz

    cd /opt
    tar -zxvf go1.9.2.linux-amd64.tar.gz  【解压后就可以看见一个go目录】

    step4:查看

  • Linux下配置Golang环境变量

    step1:使用root权限修改/etc/profile文件

    # go-sdk 2020-02-12
    export GOROOT=/opt/go
    export PATH=${PATH}:$GOROOT/bin
    export GOPATH=$HOME/goproject

    step2:生效配置

    source /etc/profile

     

六、Mac下搭建Go开发环境-安装和配置SDK

 

 

 

 

七、Hello world!

编写一个hello.go程序,输出“hello world”

 

 

 

 

八、Golang执行流程

 

Go 程序开发注意事项:

 

 

 

九、Go语言转意字符

 

 

package main
import "fmt"
func main() {
    fmt.Println("姓名\t年龄\t籍贯\t住址")
    fmt.Println("\njoin\t12\t河北\t北京")
}

 

 运行结果:

 

十、Golang变量

  • 注意事项

     

  • 声明方式

        // golang的变量使用方式1
        // 第一种:指定变量类型,声明后若不赋值,使用默认值
        // int 的默认值是0 其它数据
        var age int
        age = 20
        fmt.Println("age = ", age)

      // 第二种:根据值自行判断变量类型(类型推导)
        var num = 10.11
        fmt.Println("num = ", num)

        // 第三种:省略var,注意 :=  左侧的变量不应该是已经声明过的,否则编译报错
        // 下面的方式等价于 var name string     name = "tom"
        name := "tom"
        fmt.Println("name = ", name)
  • 多变量声明

        // 该案例演示Golang如何一次性声明多个变量
        var num1, num2, num3 int
        fmt.Println("num1=", num1, "\tnum2=", num2 , "\tnum3=", num3)
    
        // 一次性声明多个变量的方式2 ==> age= 100     sum= 1000     name= 名字
        var age, sum, name = 100, 1000, "名字"
        fmt.Println("age=", age, "\tsum=", sum , "\tname=", name)
    
        // 一次性声明多个变量的方式3,使用类型推导 ==> hight= 100     weight= 1000     student= 名字
        hight, weight, student := 100, 1000, "名字"
        fmt.Println("hight=", hight, "\tweight=", weight , "\tstudent=", student)
  • 全局变量

    package main
    import "fmt"
    
    // 定义全局变量
    var n1 = 1
    var n2 = 100
    var boy = "join"
    // 上面的声明方式,也可以改为一次性声明
    var (
        n3 = 300
        n4 = 400
        boy2 = "mary"
    )
    
    func main() {
        // 输出全局变量
        fmt.Println("n1=", n1, "n2=", n2 , "boy=", boy)
        fmt.Println("n3=", n3, "n4=", n4 , "boy2=", boy2)
    }
  • 变量的数据类型

    结构体:类似于Java中的class

    管道:用于高并发

十一、数据类型

func main() {

    var n1 = 100
    // 查看某个变量的数据类型
    // fmt.Printf() 可以做格式化输出
    fmt.Printf(" n1 的数据类型是: %T  \n", n1)


    // 在程序中查看变量的占用字节大小和数据类型
    var n2 int64 = 10
    // unsafe.Sizeof() 可以返回变量占用的字节数
    fmt.Printf(" n2 的数据类型是: %T   n2 占用字节大小是 %d  \n", n2, unsafe.Sizeof(n2))
}
  • 整数数据类型

    有符号整数:

     无符号整数:

     其它:

    使用细节:

     

  • 小数类型/浮点型

     

        // 演示精度丢失:尾数部分可能丢失
        var num3 float32 = -123.0000901
        var num4 float64 = -123.0000901
        fmt.Println("num3 = ", num3, "num4 = ", num4)

    使用细节:

        // Golang的浮点型默认声明为float64
        var num5 = 1.1
        fmt.Printf("num5 的数据类型是 %T \n", num5)
    
        // 浮点型声明:十进制形式 如:5.12   .123
        num6 := 5.12
        num7 := .123    // =>0.123
        fmt.Println("num6 = ", num6, "num7 = ", num7)
    
        // 浮点型声明:科学计数法形式,e与E一样
        num8 := 5.1234e2    // =>5.1234 * 10的2次方
        num9 := 5.1234E-2    // =>5.1234 / 10的2次方
        fmt.Println("num8 = ", num8, "num9 = ", num9)
  • 字符类型(char)


    // 演示Golang中字符类型的使用
    func main() {
    
        var c1 byte = 'a'
        var c2 byte = '0'
    
        // 当我们直接输出byte值,就是输出了对应字符的ASCII码值
        // a==> 97    0==>48
        fmt.Println("c1 = ", c1)
        fmt.Println("c2 = ", c2)
        // 如果我们希望输出对应的字符,需要使用格式化输出
        fmt.Printf("c1 =  %c c2 =  %c \n", c1, c2)
    
        // var c3 byte = '黄'
        // 会报错:./main.go:19:16: constant 40644 overflows byte
        // 因为byte范围是0~255,所以换一个范围大的类型即可
        var c3 int = ''
        fmt.Printf("c3 =  %c c3对应的ASCII码值 = %d \n", c3, c3)
    }
     

    使用细节:

        // 可以直接给某个变量赋一个数字,然后按格式输出%c,会输出数字对应的Unicode字符
        var c4 int = 22269    //22269->国
        fmt.Printf("c4 =  %c \n", c4)
    
        //字符类型可以直接进行运算,相当于一个整数,运算时按ASCII码值运算
        var n1 = 10 + 'a'    //10 + 97 = 107
        fmt.Println("n1 = ", n1)

     字符类型本质讨论:

  • 布尔类型(bool)

  • 字符串类型(string)

    使用细节:

     

    package main
    import (
        "fmt"
    )
    
    // 演示Golang中string类型的使用
    func main() {
        // string的基本使用
        var address string = "北京长城!"
        fmt.Println(address)
    
        // string的注意事项
        var str string = "hello world!!!"
        // str[0] = 'y'    // 报错:./main.go:14:9: cannot assign to str[0]   不能修改
        fmt.Println(str)
    
        //    字符串的两种表现形式(1)双引号,(2)反引号,在esc下面那个键
        str2 := "abc\nabc"    //双引号会转意
        fmt.Println(str2)
    
        // 使用反引号``,以字符串的原生形式输出,包括换行和特殊字符,可以防止攻击
        str3 := `
        package main
        import "fmt"
        
        func main() {
            // golang的变量使用方式1
            // 第一种:指定变量类型,声明后若不赋值,使用默认值
            // int 的默认值是0 其它数据
            var age int
            age = 20
            fmt.Println("age = ", age)
        
            // 第二种:根据值自行判断变量类型(类型推导)
            var num = 10.11
            fmt.Println("num = ", num)
        
            // 第三种:省略var,注意 :=  左侧的变量不应该是已经声明过的,否则编译报错
            // 下面的方式等价于 var name string     name = "tom"
            name := "tom"
            fmt.Println("name = ", name)
        }
        `
        fmt.Println(str3)
    
        // 字符串拼接方式
        var str4 = "hello" + "world"
        str4 += " hhhha!"
        fmt.Println(str4)
    
    }
    View Code
  • 基本数据类型的默认值

     

    使用细节:

  • 基本数据类型的相互转换

     基本语法:

    // 演示Golang中基本数据类型的转换
    func main() {
    
        var i int32 = 100
        //    希望将i =》 float
        var n1 float32 = float32(i)
        var n2 int8 = int8(i)
        var n3 int64 = int64(i)        //低精度-》高精度
    
        fmt.Printf("i=%v, n1=%v, n2=%v, n3=%v\n", i, n1, n2, n3)
    }

    使用细节:

        // 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化
        fmt.Printf("i type is %T\n", i)    //int32
    
        //    在转换中,比如将int64转为int8【-128~127】,编译不会报错
        //    只是转换的结果按溢出处理,和我们希望的不一样
        var num1 int64 = 999999
        var num2 int8 = int8(num1)    //63
        fmt.Println("num2 = ", num2)

     

  • 基本数据类型和string类型的相互转换

    在程序开发中,我们经常将基本数据类型转换成string或者将string转成基本数据类型

    基本数据类型转string方式1:fmt.Sprintf("%参数", 表达式)  【个人习惯这个,灵活】

    // 演示Golang中基本数据类型转成string
    func main() {
    
        var num1 int = 99
        var num2 float64 = 23.456
        var b bool = true
        var myChar byte = 'h'
        var str string     //空的string
    
        // 使用第一种方式转换    fmt.Sprintf方法
        // 前面的%参数详情:https://studygolang.com/pkgdoc   下的fmt
        str = fmt.Sprintf("%d", num1)
        fmt.Printf("str type is %T str = %q  \n", str, str)
    
        str = fmt.Sprintf("%f", num2)
        fmt.Printf("str type is %T str = %q \n", str, str)
    
        str = fmt.Sprintf("%t", b)
        fmt.Printf("str type is %T str = %q  \n", str, str)
    
        str = fmt.Sprintf("%c", myChar)
        fmt.Printf("str type is %T str = %q  \n", str, str)
    }

    基本数据类型转string方式2:使用  strconv  包的函数

    package main
    import (
        "fmt"
        "strconv"
    )
    
    // 演示Golang中基本数据类型转成string
    func main() {
    
        // 使用第二种方式转换    使用  strconv  包的函数
        // 详情:https://studygolang.com/pkgdoc   下的strconv
        var num3 int = 99
        var num4 float64 = 23.456
        var b2 bool = true
        var str string     //空的string
    
        str = strconv.FormatInt(int64(num3), 10)
        fmt.Printf("str type is %T str = %q  \n", str, str)
    
        // 说明: 'f'--格式;10--表示小数位保留10位;64--表示这个小数是float64
        str = strconv.FormatFloat(num4, 'f', 10, 64)
        fmt.Printf("str type is %T str = %q  \n", str, str)
    
        str = strconv.FormatBool(b2)
        fmt.Printf("str type is %T str = %q  \n", str, str)
    
    }

     

    Itoa函数:可以直接将int类型转为string类型

        // strconv包中还有一个Itoa函数:可以直接将int类型转为string类型
        var num5 int64 = 567
        str = strconv.Itoa(int(num5))
        fmt.Printf("str type is %T str = %q  \n", str, str)

    string转基本数据类型的方式:使用  strconv  包的函数

    package main
    import (
        "fmt"
        "strconv"
    )
    
    // 演示Golang中string转成基本数据类型
    func main() {
    
        var str string = "true"
        var b bool
        // strconv.ParseBool(str)函数会返回两个值 (value bool, err error)
        // 因为只想获取value,不想获取err,所以使用‘_’忽略
        b, _ = strconv.ParseBool(str)
        fmt.Printf("b type is %T b = %v  \n", b, b)
    
        var str2 string = "1234560"
        var num1 int64
        //将str2转成10进制的;转成64位的;使用‘_’忽略err
        num1, _ = strconv.ParseInt(str2 ,10, 64)
        fmt.Printf("num1 type is %T num1 = %v  \n", num1, num1)
        // 因为go默认转换回来的都是64位,不论是int还是float,如果我们不想要那么大,只能自己转
        var num2 int
        num2 = int(num1)
        fmt.Printf("num2 type is %T num2 = %v  \n", num2, num2)
    
        var str3 string = "123.456"
        var f1 float64
        //将str3转成转成64位的;使用‘_’忽略err
        f1, _ = strconv.ParseFloat(str3 , 64)
        fmt.Printf("f1 type is %T f1 = %v  \n", f1, f1)
    }

     

  • 指针类型

     

    package main
    import "fmt"
    
    // 演示Golang中指针类型
    func main() {
    
        // 基本数据类型在内存中的布局
        var i int = 10
        // i的地址是什么,&i
        fmt.Println("i的地址是", &i)
    
        // ptr是一个指针变量,类型是*int,本身的值是&i
        var ptr *int = &i
        fmt.Printf("ptr = %v\n", ptr)
        // 指针本身也有地址
        fmt.Println("ptr的地址是", &ptr)
        // 取出ptr指向的值
        fmt.Printf("ptr指向的值%v\n", *ptr)
    }

    使用细节:

     案例演示:

        var num int = 120
        fmt.Println("num的值是:", num)
        fmt.Println("num的地址是:", &num)
    
        var ptr *int
        fmt.Println("ptr的地址是:", &ptr)
        ptr = &num
        fmt.Println("ptr的值是:", ptr)
        // 修改ptr指向的值,即修改num的值
        *ptr = 1540
        fmt.Println("修改后num的值是:", num)

     

     

  • 值类型和引用类型

    特点:

  • 标识符的命名规范

    标识符的概念:

    标识符的命名规则:

    注意事项:

     

     

 

十二、算术运算符

运算符的基本介绍:

  • 算术运算符

     案例演示:

     注意事项:

     

  • 关系运算符(比较运算符)

     

     案例演示:

     

     注意事项:

     

  • 逻辑运算符

     

     案例演示:

     

     

     注意事项:

     

  • 赋值运算符

     

     

     

     案例演示:

     

     

     注意事项:

     

  • 位运算符

  • 其它运算符

     案例演示:

     

     特别说明:

     

  • 运算符的优先级

     

     

     特别说明:

     

     

 

十三、获取用户终端输入

  1. 使用 fmt.Scanln()
    package main
    import (
        "fmt"
    )
    
    // 要求:可以从控制台接收用户信息【姓名,年龄,薪水,是否通过考试】
    func main() {
    
        // 声明变量
        var name string
        var age byte
        var sal float32
        var isPass bool
    
        // 方式1:fmt.Scanln()
        fmt.Println("请输入姓名 ")
        // 当程序执行到 fmt.Scanln(&name) 时,程序会停止在这,等待用户输入并回车
        fmt.Scanln(&name)
    
        fmt.Println("请输入年龄 ")
        fmt.Scanln(&age)
    
        fmt.Println("请输入薪水 ")
        fmt.Scanln(&sal)
    
        fmt.Println("请输入是否通过考试 ")
        fmt.Scanln(&isPass)
    
        fmt.Printf("姓名是 %v \n年龄是 %v \n薪水是 %v \n是否通过考试 %v \n ", name, age, sal, isPass)
    
    }
    View Code

     

  2. 使用 fmt.Scanf()
    package main
    import (
        "fmt"
    )
    
    // 要求:可以从控制台接收用户信息【姓名,年龄,薪水,是否通过考试】
    func main() {
    
        // 声明变量
        var name string
        var age byte
        var sal float32
        var isPass bool
    
        fmt.Println("请输入您的姓名,年龄,薪水,是否通过考试;使用空格隔开! ")
        fmt.Scanf("%s %d %f %t",&name, &age, &sal, &isPass )
    
        fmt.Printf("姓名是 %v \n年龄是 %v \n薪水是 %v \n是否通过考试 %v \n ", name, age, sal, isPass)
    
    }
    View Code

 

十四、进制

(1)二进制:0,1,满2进1

  • 在golang中,不能直接使用一个二进制表示一个整数,可以用八进制、十进制和十六进制表示,它沿用了c语言的特点

(2)十进制:0-9,满10进1

(3)八进制:0-7,满8进1,以数字0开头

(4)十六进制:0-9及A-F,满16进1,以0x或0X开头,此处A-F不区分大小写,例如0x21AF+1=0x21B0

  • 案例演示:
    package main
    import (
        "fmt"
    )
    
    // 进制的基本使用
    func main() {
    
        var i int = 5
        // 二进制输出
        fmt.Printf("%b \n", i)
    
        // 八进制:0-7,满8进1,以数字0开头
        var j int = 011
        fmt.Println("j=", j)
    
        // 十六进制:0-9及A-F,满16进1,以0x或0X开头,此处A-F不区分大小写
        var k int = 0x11
        fmt.Println("k=", k)
    }

     

 

十五、位运算

  • 原码、反码、补码

  • golang中的3个位运算符:“按位与&”、“按位或|”、“按位异或^”

    按位与&    :    两边全为1,则为1,否则为0

    按位或|      :   有一个1,则为1,否则为0

    按位异或^:   两边一个为0 一个为1,则为1,否则为0

    代码演示:
    package main
    import (
        "fmt"
    )
    
    // 位运算的演示:在计算机运算时,都是以补码的方式来进行的
    func main() {
    
        // 2&3
        //2的补码 0000 0010 因为2是整数,所以前面为0000,负数则为1000
        //3的补码 0000 0011
        //     ===》0000 0010 = 2  “两边全为1,则为1,否则为0” 【 而且结果还是补码,但是正数的反、补码==原码】
        fmt.Println("2&3 =",2&3)
    
        // 2|3
        //2的补码 0000 0010 因为2是整数,所以前面为0000,负数则为1000
        //3的补码 0000 0011
        //     ===》0000 0011 = 3  “有一个1,则为1,否则为0” 【 而且结果还是补码,但是正数的反、补码==原码】
        fmt.Println("2|3 =",2|3)
    
        // 2^3
        //2的补码 0000 0010 因为2是整数,所以前面为0000,负数则为1000
        //3的补码 0000 0011
        //     ===》0000 0001 = 1  “两边一个为0 一个为1,则为1,否则为0” 【 而且结果还是补码,但是正数的反、补码==原码】
        fmt.Println("2^3 =",2^3)
    
        // -2^2
        //-2的原码 1000 0010 负数反码=(原码符号位不变,其它取反)1111 1101 负数补码=(反码+1)1111 1110
        //                   1111 1110
        //2的补码   0000 0010
        //       ===》1111 1100【 结果还是补码,要转回原码】结果的反码=1111 1011 所以原码=1000 0100 = -4
        fmt.Println("-2^2 =",-2^2)  
    }

     

  • golang中的2个移位运算符:“右移运算符>>”、“左移运算符<<”

    右移运算符>> : 低位溢出,符号位不变,并用符号位补溢出的高位

    左移运算符<< : 符号位不变,低位补0

    代码演示:
    package main
    import (
        "fmt"
    )
    
    // 移位运算的演示:在计算机运算时,都是以补码的方式来进行的
    //            右移运算符>> : 低位溢出,符号位不变,并用符号位补溢出的高位
    //            左移运算符<< : 符号位不变,低位补0
    func main() {
    
        // 1>>2  1的原、反、补码=0000 0001;所以1右移2位=0000 0000 = 0
        fmt.Println("1>>2 =",1>>2)
    
        // 1<<2  1的原、反、补码=0000 0001;所以1左移2位=0000 0100 = 4
        fmt.Println("1<<2 =",1<<2)
    
        // -1>>2   -1的原码=1000 0001、-1反码=(原码符号位不变,其它取反)1111 1110、-1补码=(反码+1)1111 1111;所以-1右移2位得出结果的补码=1111 1111
        //补码1111 1111的反码 = 1111 1110 原码=1000 0001=-1
        fmt.Println("-1>>2 =",-1>>2)
    
        // -1<<2  -1的原码=1000 0001、-1反码=(原码符号位不变,其它取反)1111 1110、-1补码=(反码+1)1111 1111;所以-1左移2位得出结果的补码=1111 1100
        //补码1111 1100的反码 = 1111 1011 原码=1000 0100=-4
        fmt.Println("-1<<2 =",-1<<2)
    }

 

十六、程序流程控制

在程序中,程序运行的流程控制决定程序是如何执行的,是我们必须掌握的,主要有三大流程控制语句。
        1) 顺序控制
        2) 分支控制
        3) 循环控制

  • 1、顺序控制

    程序从上到下逐行地执行,中间没有任何判断和跳转。

  • 2、分支控制

    分支控制就是让程序有选择执行。有下面三种形式
           1) 单分支
           2) 双分支
           3) 多分支

    1、单分支

    2、双分支

     3、多分支控制

    4、switch 分支控制

    基本语法:

     switch 的使用的注意事项和细节:

    1) case/switch 后是一个表达式( 即:常量值变量、一个有返回值的函数等都可以)

    2) case 后的各个表达式的值的数据类型,必须和 switch 的表达式数据类型一致

    3) case 后面可以带多个表达式,使用逗号间隔。比如 case 表达式 1, 表达式 2 ...

    4) case 后面的表达式如果是常量值(字面量),则要求不能重复

    5) case 后面不需要带 break , 程序匹配到一个 case 后就会执行对应的代码块,然后退出 switch,如果一个都匹配不到,则执行 default

    6) default 语句不是必须的.

    7) switch 后也可以不带表达式,类似 if --else 分支来使用。

    8) switch 后也可以直接声明/定义一个变量,分号结束,不推荐

    9) switch 穿透-fallthrough ,如果在 case 语句块后增加 fallthrough ,则会继续执行下一个 case,也叫 switch 穿透

     10) Type Switch:switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际指向的变量类型

    5、switch 和 if 的比较

  • 3、循环控制--for 循环控制

    程序从上到下逐行地执行,中间没有任何判断和跳转。

     1、快速入门

    var str string = "hello world!"
    for i := 0; i < len(str); i++ {
        fmt.Printf("%c \n", str[i])
    }

    2、for 循环的基本语法

     3、for 循环执行流程

    4、for 循环的使用注意事项和细节讨论程

    1) 循环条件是返回一个布尔值的表达式

    2) for 循环的第二种使用方式
            for 循环判断条件 {
                   //循环执行语句
           }
    将变量初始化和变量迭代写到其它位置

    3) for 循环的第三种使用方式

           for {
                  //循环执行语句
           }
    上面的写法等价 for ; ; {} 是一个无限循环, 通常需要配合 break 语句使用

    4) Golang 提供 for-range 的方式,可以方便遍历字符串和数组


  • 4、循环控制-while 和 do..while 的实现

    Go 语言没有 while 和 do...while 语法,这一点需要同学们注意一下,如果我们需要使用类似其它语言(比如 java / c 的 while 和 do...while ),可以通过 for 循环来实现其使用效果

    1、while 循环的实现

    2、do..while 的实现

  • 5、跳转控制语句-break

    break 语句用于终止某个语句块的执行,用于中断当前 for 循环或跳出 switch 语句。

    1、案例演示

    随机生成1-100的一个数,直到生成99为止,看看一共用了几次?
    package main
    import (
        "fmt"
        "math/rand"
        "time"
    )
    
    //break例子
    func main() {
    
        //在go中,需要随机生成一个随机数种子,否则返回的值总是固定的
        // time.Now().Unix() : 返回一个从1970:01:01的0时-分0秒到现在的秒数(单位秒)
        //rand.Seed(time.Now().Unix())
    
        // 随机生成1-100的整数, rand.Intn(100)是生成[0, 100)
        //n := rand.Intn(100) + 1
        //fmt.Println("n = ", n)
    
        var count int = 0
        for {
            // UnixNano() : 返回一个从1970:01:01的0时-分0秒到时间t的纳秒数(单位纳秒)
            rand.Seed(time.Now().UnixNano())
            n := rand.Intn(100) + 1
            fmt.Println("n = ", n)
            count++
            if (n == 99) {
                break
            }
        }
        fmt.Println("生成99共用了 ", count)
    }

    2、break注意事项和细节说明

    以 for 循环使用 break 为例

  • 6、跳转控制语句-continue

    continue 语句用于结束本次循环,继续执行下一次循环
    continue 语句出现在多层嵌套的循环语句体中时。可以通过标签指明要跳过的是哪一层循环 , 这个和前面的 break 标签的使用的规则一样。

    1、continue注意事项和细节说明

    以 for 循环使用 continue 为例

  • 7、跳转控制语句-goto

    1) Go 语言的 goto 语句可以无条件地转移到程序中指定的行。

    2) goto 语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能。

    3) 在 Go 程序设计中一般不主张使用 goto 语句,以免造成程序流程的混乱,使理解和调试程序都产生困难。

    1、goto 循环的基本语法

    goto label
    .. .
    label: statement

     2、for 循环执行流程

    3、快速入门

    package main
    import (
        "fmt"
    )
    
    //goto 例子
    func main() {
    
        fmt.Println("ok1")
        goto label1
        fmt.Println("ok2")
        fmt.Println("ok3")
        label1:
        fmt.Println("ok4")
        fmt.Println("ok5")
        fmt.Println("ok6")
    }

  • 8、跳转控制语句-return

    return 使用在方法或者函数中,表示跳出所在的方法或函数。

    1) 如果 return 是在普通的函数,则表示跳出该函数,即不再执行函数中 return 后面代码,也可以理解成终止函数。

    2) 如果 return 是在 main 函数,表示终止 main 函数,也就是说终止程序。

    package main
    import (
        "fmt"
    )
    
    //return 例子
    func main() {
    
        for i:=1; i <= 10; i++ {
            if i == 3 {
                return
            }
            fmt.Println("哇哇哇!", i)
        }
        fmt.Println("Hello World!")
    }

     

      

十七、函数、包和错误处理

1、函数

1)基本语法

2)入门案例

func cal(num1 float64, num2 float64) float64 {
    return num1 + num2
}

3)函数使用的注意事项和细节

1) 函数的形参列表可以是多个,返回值列表也可以是多个

2) 形参列表和返回值列表的数据类型可以是值类型和引用类型。

3) 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似 public , 首字母小写,只能被本包文件使用,其它包文件不能使用,类似 private

4) 函数中的变量是局部的,函数外不生效

5) 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。

6) 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。

7) Go 函数不支持函数重载

8) 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用

package main
import (
    "fmt"
)

func getSum(n1 int, n2 int) int {
    return n1 + n2
}

func main() {

    a := getSum
    fmt.Printf("a的数据类型是:%T,getSum的数据类型是%T\n", a, getSum)

    // 等价于 res := getSum(10. 40)
    res := a(10, 40)
    fmt.Println("res = ", res)
}

 9) 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用

func getSum(n1 int, n2 int) int {
    return n1 + n2
}

func myFun(funVar func(int, int) int, num1 int, num2 int) int {
    return funVar(num1, num2)
}

func main() {

    res2 := myFun(getSum, 50, 50)
    fmt.Println("res2 = ", res2)
}

10) 为了简化数据类型定义,Go 支持自定义数据类型

基本语法:type 自定义数据类型名 数据类型    // 理解: 相当于一个别名
案例:type myInt int    // 这时 myInt 就等价 int 来使用了
案例:type mySum func (int, int) int    // 这时 mySum 就等价 一个 函数类型 func (int, int) int

11) 支持对函数返回值命名

// 支持对函数返回值命名,在返回值列表上定义了名称,就可以直接返回,不用 return sum, sub
func getSumAndSub(n1 int, n2 int) (sum int, sub int) {
    sum = n1 +n2
    sub = n1- n2
    return
}

func main() {
    a1, b1 := getSumAndSub(10, 5)
    fmt.Printf("a1 = %v,b1 = %v\n", a1, b1)
}

12) 使用 _ 标识符,忽略返回值

13) Go 支持可变参数

 

2、包的引出

1) 在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,比如 main.go中,去使用 utils.go 文件中的函数,如何实现? -》包

2) 现在有两个程序员共同开发一个 Go 项目,程序员 xiaoming 希望定义函数 Cal ,程序员 xiaoqiang也想定义函数也叫 Cal。两个程序员为此还吵了起来,怎么办? -》包

1)打包基本语法

package 包名

2)引入包的基本语法

import "包的路径"

3)包使用的快速入门

 4)包使用的注意事项和细节

1) 在给一个文件打包时,该包对应一个文件夹,比如这里的 utils 文件夹对应的包名就是 utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。

2) 当一个文件要使用其它包函数或变量时,需要先引入对应的包

引入方式 1:
    import "包名"
引入方式 2:
    import (
        "包名"
        "包名"
    )
package 指令在 文件第一行,然后是 import 指令。
在 import 包时,路径从 $GOPATH 的src 下开始,不用带 src , 编译器会自动从 src 下开始引入

  以下是我的环境变量中定义的:

    

    initDemo所在位置:

3) 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的 public ,这样才能跨包访问。比如 utils.go 的

4) 在访问其它包函数,变量时,其语法是 包名.函数名, 比如这里的 main.go 文件中

5) 如果包名较长,Go 支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了说明: 如果给包取了别名,则需要使用别名来访问该包的函数和变量

6) 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义

7) 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main .这个就是一个语法规范,如果你是写一个库 ,包名可以自定义

 

十八、init 函数

1、函数简介

每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也就是说 init 会在 main 函数前被调用。

2、案例说明

package main
import (
    "fmt"
)

// init函数:通常可以在init函数中完成初始化工作
func init() {
    fmt.Println("init() ....")
}

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

3、inti 函数的注意事项和细节

1) 如果一个文件同时包含全局变量定义, init 函数main 函数,则执行的流程全局变量定义->init函数->main 函数 

2) init 函数最主要的作用,就是完成一些初始化的工作,比如下面的案例

package main
import (
    "fmt"
    // 引入包
    "go_code/initDemo/utils"
)

var age = test()

// 为了看到全局变量实现被初始化的,我们这里先写这个函数
func test() int {
    // 1
    fmt.Println("全局变量被定义 ....")
    return 90
}

// init函数:通常可以在init函数中完成初始化工作
func init() {
    // 2
    fmt.Println("init() ....")
}

func main() {
    // 3
    fmt.Println("main() ....")
    fmt.Println("Age = ", utils.Age, "Name = ", utils.Name)
}
/src/go_code/initDemo/main.go
package utils
import "fmt"

// Age和Name是全局变量,在这里初始化,在其它地方被调用
var Age int
var Name string

func init() {
    fmt.Println("utils包下的 init() ....")

    Age = 100
    Name = "soldier~"
}
/src/go_code/initDemo/utils/utils.go

执行/init/main.go后输出:

4、面试题:案例如果 main.go 和 utils.go 都含有 变量定义,init 函数时,执行的流程又是怎么样的呢?

 

十九、匿名函数

1、匿名函数简介

Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。

2、案例说明

1)使用方式 1:在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次

// 匿名函数使用方式
func main() {
    // 使用方式 1:在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次
    // 演示求两个数的和
    res := func (n1 int, n2 int) int {
        return n1 + n2
    }(10, 20)
    fmt.Println("res = ", res)
}

2)使用方式 2:将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数

// 匿名函数使用方式
func main() {
    // 使用方式 2:将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
    a := func(n1 int, n2 int) int {
        return n1 +n2
    }
    res2 := a(5, 5)
    fmt.Println("res2 = ", res2)
    res3 := a(50, 50)
    fmt.Println("res3 = ", res3)
}

3、全局匿名函数

如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。

package main
// 使用括号可以导入多个包,定义变量也是
import (
    "fmt"
)

// 定义多个全局变量
var (
    // Fun1就是一个全局匿名函数,注意首字母大写 == java的public
    Fun1 = func(n1 int, n2 int) int {
        return n1 * n2
    }
)

// 匿名函数使用方式
func main() {
    // 全局匿名函数的使用
    res4 := Fun1(100, 100)
    fmt.Println("res4 = ", res4)
}

 

二十、闭包

1、基本介绍

闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)。

2、案例说明

// 累加器
//    AddUpper:方法名    func (int) int:返回值类型列表
func AddUpper() func (int) int {
    var n int = 10
    return func (x int) int {
        n = n + x
        return n
    }
}

// 闭包案例演示
func main() {
    // 使用累加器
    f := AddUpper()
    fmt.Println(f(1))    // 11
    fmt.Println(f(2))    // 13
    fmt.Println(f(3))    // 16
}

 对上面代码的说明和总结:

1) AddUpper 是一个函数,返回的数据类型是 fun (int) int

2) 闭包的说明

  返回的是一个匿名函数, 但是这个匿名函数引用到函数外的 n(当做一个全局变量来理解即可) ,因此这个匿名函数就和 n 形成一个整体,构成闭包

3) 大家可以这样理解: 闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。

4) 当我们反复的调用 f 函数时,因为 n 只初始化一次,因此每调用一次就进行累计。

5) 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。

6) 对上面代码的一个修改,加深对闭包的理解

// 累加器
//    AddUpper:方法名    func (int) int:返回值类型列表
func AddUpper() func (int) int {
    var n int = 10
    var str string = "hello"
    return func (x int) int {
        n = n + x
        str += string(36)    //36 = '$'
        fmt.Println("str = ", str)    // 1.str = "hello$"        2.str = "hello$$"    3.str = "hello$$$"
        return n
    }
}

// 闭包案例演示
func main() {
    // 使用累加器
    f := AddUpper()
    fmt.Println(f(1))    // 11
    fmt.Println(f(2))    // 13
    fmt.Println(f(3))    // 16
}

3、闭包的最佳实践

请编写一个程序,具体要求如下

1) 编写一个函数 makeSuffix(suffix string)可以接收一个文件后缀名(比如.jpg),并返回一个闭包

2) 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。

3) 要求使用闭包的方式完成

4) strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。

代码:

package main
import (
    "fmt"
    "strings"
)

func makeSuffix(suffix string) func (string) string {
    return func (name string) string {
        // 如果 name 没有指定后悔,则加上,否则直接返回
        if !strings.HasSuffix(name, suffix) {
            return name + suffix
        }
        return name
    }
}

// 闭包案例演示
func main() {
    // 返回一个闭包
    f2 := makeSuffix(".jpg")
    fmt.Println("文件处理后 = ", f2("winter"))         // winter.jpg
    fmt.Println("文件处理后 = ", f2("soldier.jpg"))    // soldier.jpg
}

上面代码的总结和说明:

1) 返回的匿名函数和 makeSuffix (suffix string) 的 suffix 变量 组合成一个闭包,因为返回的函数引用到 suffix 这个变量

2) 我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用

 

二十一、函数的 defer

在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。

1、入门案例

func sum(n1 int, n2 int) int {
    
    // 当执行到defer时,暂不执行,会将defer后面的语句压入到独立的栈(defer栈)
    // 当函数执行完毕后,再从defer栈,按照后进先出的方式出栈,执行
    defer fmt.Println("ok1 n1= ", n1)    //defer3
    defer fmt.Println("ok2 n2= ", n2)    //defer2

    res := n1 + n2
    defer fmt.Println("ok3 res= ", res)    //defer1
    return res
}

func main() {
    res := sum(10, 20)
    fmt.Println("res= ", res)
}

 

2、defer 的注意事项和细节

1) 当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中[为了方便了;理解,暂时称该栈为 defer 栈], 然后继续执行函数下一个语句。

2) 当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制),所以我们看到前面案例输出的顺序。

3) 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。请看一段代码:

func sum(n1 int, n2 int) int {
    // 当执行到defer时,暂不执行,会将defer后面的语句压入到独立的栈(defer栈)
    // 当函数执行完毕后,再从defer栈,按照后进先出的方式出栈,执行
    defer fmt.Println("ok1 n1= ", n1)    //defer3
    defer fmt.Println("ok2 n2= ", n2)    //defer2

    // 添加一个增加语句
    n1++    //=11
    n2++    //=21
    res := n1 + n2    //=32
    defer fmt.Println("ok3 res= ", res)    //defer1
    return res
}

func main() {
    res := sum(10, 20)
    fmt.Println("res= ", res)
}

3、defer 的最佳实践

defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。看下面模拟代码

 说明

1) 在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.close() defer connect.close()

2) 在 defer 后,可以继续使用创建资源。

3) 当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源。

4) 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。

 

二十二、函数参数传递方式

函数参数传递方式有两种:

  1) 值传递
  2) 引用传递

值类型参数默认就是值传递,而引用类型参数默认就是引用传递。

其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。

1、值类型和引用类型

1) 值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct

2) 引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型

2、值传递和引用传递使用特点

1)值类型参数默认就是值传递:变量直接存储内存通常在中分配

 

2)引用类型参数默认就是引用传递:变量存储的是一个地址,这个地址对应的空间才真正存储数据(),内存通常在上分配,当没有任何变量引用这个地址是,该地址对应的数据空间就变成一个垃圾,由GC来回收。

 

3)如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用

 

二十三、字符串常用的系统函数

1)统计字符串的长度,按字节==》len(str)

func main() {
    // 统计字符串的长度,按字节 len(str)
    // golang的编码统一为utf-8(ascii的字符(字母和数字)占一个字符,汉字占3个)
    str := "hello黄"
    fmt.Println("str len = ", len(str))    //8
}

 

2)字符串遍历,同时处理有中文的问题==》r := []rune(str)

func main() {
    str2 := "hello北京"
    // 字符串遍历,同时处理有中文的问题==》r := []rune(str)
    r := []rune(str2)
    for i := 0; i < len(r); i++ {
        fmt.Printf("字符 = %c\n", r[i])
    }
}

 

3)字符串转整数==》n, err := strconv.Atoi("12")

import (
    "fmt"
    "strconv"
)
func main() {
    // 字符串转整数==》n, err := strconv.Atoi("12")
    n, err := strconv.Atoi("hello")
    if err != nil {
        fmt.Println("转换错误", err)
    } else {
        fmt.Println("转换的结果是", n)
    }
}

可利用这个特性来进行参数校验,比如校验输入的QQ号是否全部为数字

 

4)整数转字符串==》str := strconv.Itoa(12345)

import (
    "fmt"
    "strconv"
)
func main() {
    // 整数转字符串==》str := strconv.Itoa(12345)
    str := strconv.Itoa(12345)
    fmt.Printf("str的类型是%T, 值是%v\n", str, str)
}

 

5)字符串 转 []byte==》var bytes = []byte("hello go")

func main() {
    // 字符串 转 []byte==》var bytes = []byte("hello go")
    var bytes = []byte("hello go")
    fmt.Println("bytes = ", bytes)
}

 

6)[]byte 转 字符串==》 str := string([]byte{97, 98, 99})

func main() {
    // []byte 转 字符串==》 str = string([]byte{97, 98, 99})
    var str = string([]byte{97, 98, 99})
    fmt.Println("str = ", str)
}

 

7)10 进制转 2, 8, 16 进制==》str := strconv.FormatInt(123, 2) // 2表示进制数,可换成8 , 16

import (
    "fmt"
    "strconv"
)
func main() {
    str := strconv.FormatInt(123, 2)
    fmt.Println("123对应的二进制数数:", str)
    str = strconv.FormatInt(123, 16)
    fmt.Println("123对应的十六进制数数:", str)
}

 

8)查找子串是否在指定的字符串中==》strings.Contains("seafood", "foo") //true

import (
    "fmt"
    "strings"
)
func main() {
    res := strings.Contains("seafood", "foo")
    fmt.Println("res = ", res)
}

 

9)统计一个字符串有几个指定的子串==》strings.Count("ceheese", "e") //4

import (
    "fmt"
    "strings"
)
func main() {
    count := strings.Count("ceheese", "e")
    fmt.Println("count = ", count)
}

 

10)不区分大小写的字符串比较(==符号是区分字母大小写的 )==》fmt.Println(strings.EqualFold("abc", "Abc")) // true

11)返回子串在字符串第一次出现的 index 值,如果没有返回-1==》strings.Index("NLT_abc", "abc") // 4

12)返回子串在字符串最后一次出现的 index,如没有返回-1==》strings.LastIndex("go golang", "go")

13)将指定的子串替换成另外一个子串==》strings.Replace("go go hello", "go", "go 语言", n) n 可以指定你希望替换几个,如果 n=-1 表示全部替换

import (
    "fmt"
    "strings"
)
func main() {
    str := "go go hello"
    strNew := strings.Replace(str, "go", "soldier", -1)
    fmt.Printf("str = %v\nstrNew = %v\n", str, strNew)
}

 

14)按照指定的某个字符,双引号下的","为分割标识 , 将一个字符串拆分成字符串数组==》strings.Split("hello,wrold,ok", ",")

import (
    "fmt"
    "strings"
)
func main() {
    strArr := strings.Split("hello,wrold,ok", ",")
    for i := 0; i < len(strArr); i++ {
        fmt.Printf("strArr[%v] = %v\n", i, strArr[i])
    }
    fmt.Printf("strArr = %v\n", strArr)
}

 

15)将字符串的字母进行大小写的转换==》strings.ToLower("Go") // go strings.ToUpper("Go") // GO

import (
    "fmt"
    "strings"
)
func main() {
    // 将字符串的字母进行大小写的转换==》strings.ToLower("Go") // go strings.ToUpper("Go") // GO
    str := "golang Hello"
    fmt.Println("转成小写:",  strings.ToLower(str))
    fmt.Println("转成大写:",  strings.ToUpper(str))
}

 

16)将字符串左右两边的空格去掉==》strings.TrimSpace(" tn a lone gopher ntrn       ")

17)将字符串左右两边指定的字符去掉==》strings.Trim("! hello! ", " !")")// ["hello"] //将左右两边 !和 " "空格去掉

18)将字符串左边指定的字符去掉==》strings.TrimLeft("! hello! ", " !")// ["hello"] //将左边 ! 和 " "空格去掉

19)将字符串右边指定的字符去掉==》strings.TrimRight("! hello! ", " !")// ["hello"] //将右边 ! 和 " "空格去掉

20)判断字符串是否以指定的字符串开头==》strings.HasPrefix("ftp://192.168.10.1", "ftp") // true

21)判断字符串是否以指定的字符串结束==》strings.HasSuffix("NLT_abc.jpg", "abc") //false

 

二十四、时间和日期相关函数

1) 时间和日期相关函数,需要导入 time 包

import (
    "fmt"
    "time"
)

 

2) time.Time 类型,用于表示时间

func main() {
    // 1、获取当前时间
    now := time.Now()
    fmt.Printf("现在的时间是:%v\nnow的类型是:%T\n", now, now)
}

 

3) 如何获取到其它的日期信息

func main() {
    // 1、获取当前时间
    now := time.Now()
    fmt.Printf("现在的时间是:%v\nnow的类型是:%T\n", now, now)

    // 2、通过now可以获取年月日时分秒
    fmt.Println("现在的年份是:", now.Year())
    fmt.Printf("现在的月份是:%v,类型是:%T\n", now.Month(), now.Month())
    fmt.Println("现在的月份是:", int(now.Month()))
    fmt.Printf("现在是%v号\n", now.Day())
    fmt.Printf("现在是%v点(时)\n", now.Hour())
    fmt.Printf("现在是%v分\n", now.Minute())
    fmt.Printf("现在是%v秒\n", now.Second())
}

 

4) 格式化日期时间

方式1:使用 Printf 或者 SPrintf

func main() {
    // 1、获取当前时间
    now := time.Now()

    // 格式化时间
    fmt.Printf("现在是:%v-%v-%v %v:%v:%v\n", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
    dateStr := fmt.Sprintf("现在是:%v-%v-%v %v:%v:%v", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())
    fmt.Println("dateStr = ", dateStr)
}

 

方式2:使用 time.Format() 

func main() {
    // 1、获取当前时间
    now := time.Now()

    // 格式化时间的第二种方式
    fmt.Println(now.Format("2006-01-02 15:04:05"))
    fmt.Println(now.Format("2006/01/02 15:04:05"))
    fmt.Println(now.Format("2006/01"))
    fmt.Println(now.Format("2006-01-02"))
    fmt.Println(now.Format("15:04:05"))
}

对上面代码的说明

"2006/01/02 15:04:05"或者"2006-01-02 15:04:05" 这个字符串的各个数字是固定的,必须是这样写。

"2006/01/02 15:04:05"这个字符串各个数字可以自由的组合,这样可以按程序需求来返回时间和日期

 

5)时间的常量

const (
    Nanosecond  Duration = 1  //纳秒
    Microsecond          = 1000 * Nanosecond   //微秒
    Millisecond          = 1000 * Microsecond  //毫秒
    Second               = 1000 * Millisecond  //秒
    Minute               = 60 * Second  //分钟
    Hour                 = 60 * Minute  //小时
)
常量的作用:在程序中可用于获取指定时间单位的时间,比如想得到 100 毫秒:
100 * time. Millisecon

 

6)结合 Sleep 来使用一下时间常量

func main() {
    // 需求1:每隔1秒打印一个数字,直到100退出
    // 需求2:每隔0.1秒打印一个数字,直到100退出
    i := 0
    for {
        i++
        fmt.Println(i)
        // 休眠
        // time.Sleep(time.Second)
        time.Sleep(time.Millisecond* 100)
        if i == 100 {
            break
        }
    }
}

 

7)time 的 Unix 和 UnixNano 的方法

func main() {
    // 1、获取当前时间
    now := time.Now()

    // time 的 Unix 和 UnixNano 的使用
    fmt.Printf("Unix的时间戳=%v\tUnixNano的时间戳=%v\n", now.Unix(), now.UnixNano())
}

 

8)练习:编写一段代码来统计 函数test 执行的时间

package main
import (
    "fmt"
    "time"
    "strconv"
)

func test() {
    str := ""
    for i:=0; i<10000; i++ {
        str += "hello" + strconv.Itoa(i)
    }
}

func main() {
    // 执行test函数前先获取当前时间戳
    start := time.Now().Unix()
    test()
    end := time.Now().Unix()
    fmt.Printf("执行test函数共使用了%v秒\n", end - start)
}

 

二十五、内置函数

Golang 设计者为了编程方便,提供了一些函数,这些函数可以直接使用,我们称为 Go 的内置函数。文档:https://studygolang.com/pkgdoc -> builtin

1) len:用来求长度,比如 string、array、slice、map、channel

2) new:用来分配内存,主要用来分配值类型,比如 int、float32,struct...返回的是指针

func main() {
    num1 := 100
    fmt.Printf("num1的的类型是:%T,值是:%v,地址是:%v\n", num1, num1, &num1)

    num2 := new(int)    //*int
    // num2的类型==》*int
    // num2的值==》地址 0xc42001c0f8(这个地址是系统分配的)
    // num2的地址==》地址 0xc42000c030(这个地址是系统分配的)
    // num2指向的值==》100
    *num2 = 100
    fmt.Printf("num2的的类型是:%T,值是:%v,地址是:%v,指向的值是:%v\n", num2, num2, &num2, *num2)
}

上面代码对应的内存分析图

3) make:用来分配内存,主要用来分配引用类型,比如 channel、map、slice。

 

二十六、错误处理

看一段代码,引出错误处理

func test() {
    num1 := 10
    num2 := 0
    res := num1 /num2
    fmt.Println("res = ", res)
}

func main() {
    test()
fmt.Println("这里执行吗...") }

对上面代码的总结:

1) 在默认情况下,当发生错误后(panic) ,程序就会退出(崩溃.)

2) 如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)

3) 这里引出我们要将的错误处理机制

1、基本说明

1) Go 语言追求简洁优雅,所以,Go 语言不支持传统的 try...catch...finally 这种处理。

2) Go 中引入的处理方式为:defer, panic, recover

3) 这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理

2、使用defer+recover来捕获和处理错误

func test() {

    // 使用defer+recover来捕获和处理错误
    defer func() {    //defer声明并执行一个匿名函数
        fmt.Println("defer比正常代码后执行!在return之后执行,如果存在多个defer声明,则defer之间遵循后进先出原则")
        err := recover()    //recover()内置函数,可以捕获到异常
        if err != nil {    //说明捕获到了异常
            fmt.Println("err = ", err)
        }
    }()

    num1 := 10
    num2 := 0
    fmt.Println("正常代码比defer先执行!")
    res := num1 /num2
    fmt.Println("res = ", res)
}

func main() {
    test()
    fmt.Println("捕获异常后,这里就可以正常执行了...")
}

3、错误处理的好处

进行错误处理后,程序不会轻易挂掉,如果加入预警代码,就可以让程序更加的健壮。

 

 4、自定义错误

Go 程序中,也支持自定义错误, 使用 errors.New 和 panic 内置函数。

1) errors.New("错误说明") , 会返回一个 error 类型的值,表示一个错误

2) panic 内置函数 ,接收一个 interface{}类型的值(也就是任何值了)作为参数。可以接收 error 类型的变量,输出错误信息,并退出程序.

import (
    "fmt"
    "errors"
)

func main() {
    test2()
    fmt.Println("mian()的其它业务代码...")
}

// 返回一个error类型数据
func readFile(fileName string) (err error) {
    if fileName == "config.ini" {
        // 读取文件...
        return nil
    } else {
        // 返回一个自定义错误
        return errors.New("读取文件错误!")
    }
}

func test2() {
    err := readFile("config.xxxx")
    if err != nil {
        // 如果读取文件发生错误,就输出这个错误并定位panic的位置,并终止程序
        panic(err)
    }
    fmt.Println("test()的其它业务代码...")
}

 

二十七、数组与切片(slice)

1、数组介绍与快速入门

数组可以存放多个同一类型数据。数组也是一种数据类型,在 Go 中,数组是值类型。

func main() {
    // 1、定义一个数组
    var hens [7]float64
    // 2、给数组赋值,下标从0开始
    hens[0] = 3.0
    hens[1] = 1.0
    hens[2] = 3.0
    hens[3] = 4.0
    hens[4] = 2.0
    hens[5] = 5.0
    hens[6] = 2.0
    // 3、遍历数组,求出和
    total := 0.0
    for i := 0; i < len(hens); i++ {
            total += hens[i]
    }
    // 4、求出平均数
    avg := fmt.Sprintf("%.2f", total / float64(len(hens)))
    fmt.Printf("总和是:%v,平均数是:%v\n", total, avg)
}

2、数组定义和内存布局

1)数组的定义

var 数组名 [数组大小]数据类型

var a [5]int

赋初值 a[0] = 1 a[1] = 30 ....

2)数组在内存布局(重要)

 对上图的总结:

1)当定义数组后,其实数组的各个元素的默认值都是0(int类型、float类型)、" "(string类型)、false(bool类型)

2)数组的地址可以通过数组名来获取 &intArr

3)数组的第一个元素的地址,就是数组的首地址

4)数组的各个元素的地址间隔是依据数组的类型决定,比如 int64 -> 8  int32->4...

func main() {
    var intArr [3]int    //int占8个字符
    // 当定义数组后,其实数组的各个元素的默认值都是0
    fmt.Println(intArr)
    intArr[0] = 10
    intArr[1] = 20
    intArr[2] = 30
    fmt.Println(intArr)
    fmt.Printf("intArr的地址是:%p,intArr[0]的地址是:%p,intArr[1]的地址是:%p,intArr[2]的地址是:%p\n", &intArr, &intArr[0], &intArr[1], &intArr[2])
}

3、数组的使用

1)入门案例:从终端循环输入 5 个成绩,保存到 float64 数组,并输出.

func main() {
    var score [5]float64
    for i := 0; i < len(score); i++ {
        fmt.Printf("请输入第%d个学生的成绩:\n", i+1)
        fmt.Scanln(&score[i])
    }
    // 变量打印
    for i := 0; i < len(score); i++ {
        fmt.Printf("score[%d] = %v\n", i+1, score[i])
    }
}

2)四种初始化数组的方式(重要

func main() {
    // 四种初始化数组的方式
    var intArr01 [3]int = [3]int{1, 2, 3}
    fmt.Println("intArr01 =", intArr01)

    var intArr02 = [3]int{5, 6, 7}
    fmt.Println("intArr02 =", intArr02)
    // 这里的 [...] 是规定的写法
    var intArr03 = [...]int{8, 9, 10}
    fmt.Println("intArr03 =", intArr03)

    var intArr04 = [...]int{1: 800, 0: 900, 2: 1000}
    fmt.Println("intArr04 =", intArr04)

    // 类型推导
    strArr := [...]string{1: "love~", 0: "**soldier", 2: "qiuxiang!"}
    fmt.Println("strArr =", strArr)
}

4、数组的遍历

1)常规变量,就是前面的使用for循环遍历

2)for-range 结构遍历

这是 Go 语言一种独有的结构,可以用来遍历访问数组的元素。

func main() {
    // 演示for-range遍历
    strArr := [...]string{1: "love~", 0: "**soldier", 2: "qiuxiang!"}
    for i, v := range strArr {
            fmt.Printf("i = %v, v = %v\t", i, v)
            fmt.Printf("strArr[%d] = %v\n", i, strArr[i])
    }
    for _, value := range strArr {
            fmt.Printf("value = %v\n", value)
    }
}

5、数组使用的注意事项和细节

1) 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化

2) var arr []int这时 arr 就是一个 slice 切片,切片后面讲解

3) 数组中的元素可以是任何数据类型,包括值类型和引用类型(&),但是不能混用。

4) 数组创建后,如果没有赋值,有默认值(零值)

数值类型(int、float)数组:默认值为 0

字符串类型(string)数组:默认值为 ""

bool类型数组: 默认值为 false

5) 数组下标必须在指定范围内使用,否则报 panic:数组越界,比如

var arr [5]int则有效下标为 0-4

6) Go 的数组属于值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响:

7 ) 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)

func main() {
    // 演示引用传递,改变数组值
    arr := [...]int{0: 10, 1: 20, 2:30}
    fmt.Println("arr =", arr)
    test(&arr)
    fmt.Println("引用传递后,arr =", arr)
}

func test(arr *[3]int) {
    // !!注意写法
    (*arr)[0] = 88
}

8) 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度,看下面案例

6、切片的基本介绍与快速入门

先看一个需求:我们需要一个数组用于保存学生的成绩,但是学生的个数是不确定的,请问怎么办?解决方案:-》使用切片

1) 切片的英文是 slice

2) 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。

3) 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。

4) 切片的长度是可以变化的,因此切片是一个可以动态变化数组

5) 切片定义的基本语法:

var 切片名 []类型
比如:var a [] int
func main() {
    // 演示切片的基本使用
    var arr [5]int = [...]int{1, 11, 33, 66, 99}
    // 声明/定义一个切片
    // slice是切片名
    // arr[1: 3] 表示slice引用到intArr这个数组
    // 引用intArr数组的起始下标为1,最后下标为3(不包括3)
    slice := arr[1: 3]
    fmt.Println("arr =", arr)
    fmt.Println("slice的元素是:", slice)
    fmt.Println("slice的个数是:", len(slice))
    // 切片的容量可以动态变化
    fmt.Println("slice的容量是:", cap(slice))
}

7、切片在内存中形式(重要)

 对上面的分析图总结:

1. slice 的确是一个引用类型
2. slice 从底层来说,其实就是一个数据结构(struct 结构体)

type slice struct {
    ptr *[2]int
    len int
    cap int
}

8、切片的使用

1)方式1:定义一个切片,然后让切片去引用一个已经创建好的数组,比如前面的案例就是这样的

func main() {
    // 演示切片的基本使用
    var arr [5]int = [...]int{1, 11, 33, 66, 99}
    // 声明/定义一个切片
    // slice是切片名
    // arr[1: 3] 表示slice引用到intArr这个数组
    // 引用intArr数组的起始下标为1,最后下标为3(不包括3)
    slice := arr[1: 3]
    fmt.Println("arr =", arr)
    fmt.Println("slice的元素是:", slice)
    fmt.Println("slice的个数是:", len(slice))
    // 切片的容量可以动态变化
    fmt.Println("slice的容量是:", cap(slice))
}

2)方式 2:通过 make 来创建切片

make在前面有提到过:https://www.cnblogs.com/HuangJie-sol/p/12297878.html#_label24

var 切片名 []type = make([]type, len, [cap])

参数说明:

  type: 就是数据类型
  len : 大小
  cap :指定切片容量,可选, 如果你分配了 cap,则要求 cap>=len(必须)

func main() {
    // 演示使用make声明切片
    // 5 :  slice大小    10 :指定切片容量
    var slice []float64 = make([]float64, 5, 10)
    slice[1] = 10
    slice[3] = 20
    // 对于切片,必须make使用
    fmt.Println(slice)
    fmt.Println("slice的size = ", len(slice))
    fmt.Println("slice的cap = ", cap(slice))
}

 对上面代码的小结:

1) 通过 make 方式创建切片可以指定切片的大小和容量

2) 如果没有给切片的各个元素赋值,那么就会使用默认值:

  int , float=> 0
  string =>””
  bool =>false

3) 通过 make 方式创建的切片对应的数组是由 make 底层维护,对外不可见,即只能通过 slice 去访问各个元素.

3)方式 3:定义一个切片,直接就指定具体数组,使用原理类似 make 的方式

func main() {
    // 方式 3:定义一个切片,直接就指定具体数组,使用原理类似 make 的方式
    var strSlice []string = []string{"**soldier", "love~", "qiuxiang!"}
    fmt.Println(strSlice)
    fmt.Println("strSlice的size = ", len(strSlice))    //3
    fmt.Println("strSlice的cap = ", cap(strSlice))    //
}

4)方式 1 和方式 2 的区别(面试)

9、切片的遍历

切片的遍历和数组一样,也有两种方式:

  for 循环常规方式遍历
  for-range 结构遍历切片

10、切片的使用的注意事项和细节

1) 切片初始化时 var slice = arr[startIndex:endIndex]

说明:从 arr 数组下标为 startIndex,取到 下标为 endIndex 的元素(不含 arr[endIndex])

2) 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长

var slice = arr[0:end] 可以简写 var slice = arr[:end]

var slice = arr[start:len(arr)] 可以简写: var slice = arr[start:]

var slice = arr[0:len(arr)] 可以简写: var slice = arr[:]

3) cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素

4) 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用

5) 切片可以继续切片

func main() {
    var arr [5]int = [...]int{10, 20, 30, 40, 50}
    slice := arr[1: 4]    //==20 30 40
    for index, value := range slice {
        fmt.Printf("slice[%v] = %v\t", index, value)
    }
    fmt.Println()
    slice2 := slice[1: 2]    //==30
    // 因为arr、slice、slice2 指向的数据空间是同一个,因此arr[2]、slice[1]都会随之改变
    slice2[0] = 100

    fmt.Println("slice = ", slice)
    fmt.Println("slice2 = ", slice2)
    fmt.Println("arr = ", arr)
}

6) 用 append 内置函数,可以对切片进行动态追加

func main() {
    var slice []int = []int{100, 200, 300}
    slice = append(slice, 400, 500, 600)
    fmt.Println("slice = ", slice)
    // 通过append将切片slice追加给slice
    // ...是必须的,否则会报错
    slice = append(slice, slice...)
    fmt.Println("slice = ", slice)
}

对上面代码的小结:

切片 append 操作的底层原理分析:

  切片 append 操作的本质就是对数组扩容

  go 底层会创建一下新的数组 newArr(安装扩容后大小)

  将 slice 原来包含的元素拷贝到新的数组 newArr

  slice 重新引用到 newArr

  注意 newArr 是在底层来维护的,程序员不可见.

7) 切片的拷贝操作

func main() {
    var slice []int = []int{1, 2, 3, 4, 5}
    // 10:切片大小len
    var slice2 = make([]int, 10)
    copy(slice2, slice)
    fmt.Println("slice = ", slice)
    fmt.Println("slice2 = ", slice2)
}

 对上面代码的说明:

(1) copy(para1, para2) 参数的数据类型是切片

(2) 按照上面的代码来看, slice 和 slice2 的数据空间是独立,相互不影响,也就是说 将slice[0]为任何数(999),slice2[0] 仍然是 1

8) 关于拷贝的注意事项

 说明: 上面的代码没有问题,可以运行, 最后输出的是 [1]

9) 切片是引用类型,所以在传递时,遵守引用传递机制。看两段代码,并分析底层原理

 

11、string 和 slice

1) string 底层是一个 byte 数组,因此 string 也可以进行切片处理

func main() {
    str := "hello@soldier"
    // 使用切片获取soldier
    slice := str[6: ]
    fmt.Println("slice = ", slice)
}

2) string 和切片在内存的形式,以 "abcd" 画出内存示意图

3) string 是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串

4) 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string

rune在前面有用到过:https://www.cnblogs.com/HuangJie-sol/p/12297878.html#_label22

func main() {
    str := "hello@soldier"
    arr := []byte(str)
    arr[0] = 'z'
    str = string(arr)
    fmt.Println("str = ", str)

    // 细节:我们转成 []byte 后可以处理英文和数字,但不能处理中文
    // 原因:[]byte 是按字节来处理的,而中文占3个字节,因此会出现乱码
    // 解决办法:将 string 转成 []rune 即可,因为 []rune 按字符处理,兼容汉字
    arr2 := []rune(str)
    arr2[0] = ''
    str = string(arr2)
    fmt.Println("str = ", str)
}

 

二十八、排序和查找

排序的基本介绍

  

查找

在 Golang 中,我们常用的查找有两种:

  1) 顺序查找

  2) 二分查找(该数组是有序)

1、冒泡排序

func BubbleSort(arr *[5] int) {
    fmt.Println("排序前,arr =", (*arr))
    // 临时变量,用于交换
    temp := 0
    for i := 0; i < len(*arr) - 1; i++ {
        for j := 0; j < len(*arr) - 1 - i; j++ {
            if (*arr)[j] > (*arr)[j+1] {
                // 交换
                temp = (*arr)[j]
                (*arr)[j] = (*arr)[j+1]
                (*arr)[j+1] = temp
            }
        }
    }
    fmt.Println("排序后,arr =", (*arr))
}

func main() {
    // 定义数组
    arr := [5]int{24, 69, 80, 13, 57}
    BubbleSort(&arr)
    fmt.Println("main.arr =", arr)
}

2、查找的案例演示

第一种:顺序查找==》有一个数列:白眉鹰王、金毛狮王、紫衫龙王、青翼蝠王,猜数游戏:从键盘中任意输入一个名称,判断数列中是否包含此名称:

func main() {
    names := [4]string{"白眉鹰王", "金毛狮王", "紫衫龙王", "青翼蝠王"}
    var heroName = ""
    havaHero := false
    fmt.Println("请输入要查找的英雄名字:")
    fmt.Scanln(&heroName)
    // 第一种方式:顺序查找
    for index, value := range names {
        if value == heroName {
            havaHero = true
            fmt.Printf("找到了【%v】,下标是:%v\n", value, index)
            break
        }
    }
    if !havaHero {
        fmt.Printf("没找到【%v】\n", heroName)
    }
}

 

第二种:二分查找,前提是数组是有序的

func main() {
    // 二分查找:前提是有序列表
    arr := [6]int{1,8, 10, 89, 1000, 1234}
    BinaryFind(&arr, 0, len(arr) - 1, 89)
}

/*
    二分查找的思路: 比如我们要查找的数是 findVal
    1. arr 是一个有序数组,并且是从小到大排序
    2. 先找到 中间的下标 middle = (leftIndex + rightIndex) / 2, 然后让 中间下标的值和 findVal 进行比较:
        2.1 如果 arr[middle] > findVal , 就应该向 leftIndex ---- (middle - 1)
        2.2 如果 arr[middle] < findVal , 就应该向 middel+1---- rightIndex
        2.3 如果 arr[middle] == findVal , 就找到
        2.4 上面的 三点 的逻辑会递归执行
    3. 想一下,怎么样的情况下,就说明找不到[分析出退出递归的条件!!]
    if leftIndex > rightIndex {
    // 找不到..
    return ..
}
*/
func BinaryFind(arr *[6]int, leftIndex int, rightIndex int, findVal int) {
    //判断 leftIndex 是否大于 rightIndex
    if leftIndex > rightIndex {
        fmt.Println("找不到")
        return
    }
    //先找到 中间的下标
    middle := (leftIndex + rightIndex) / 2
    if (*arr)[middle] > findVal {
        //说明我们要查找的数,应该在 leftIndex --- middel-1
        BinaryFind(arr, leftIndex, middle - 1, findVal)
    } else if (*arr)[middle] < findVal {
        //说明我们要查找的数,应该在 middel+1 --- rightIndex
        BinaryFind(arr, middle + 1, rightIndex, findVal)
    } else {
        //找到了
        fmt.Printf("找到了,下标为%v \n", middle)
    }
}

 

二十九、二维数组

 1、使用方式 1: 先声明/定义,再赋值

func main() {
    // 定义/声明一个二维数组
    var arr [4][6]int
    // 赋值
    arr[0][0] = 1
    arr[3][0] = 4
    arr[3][5] = 7
    // 遍历,使用for-range
    for _, value1 := range arr {
        for _, value2 := range value1 {
            fmt.Print(value2, " ")
        }
        fmt.Println()
    }
}

二维数组在内存的存在形式(重点):

func main() {
    // 定义/声明一个二维数组
    var arr [4][6]int
    fmt.Printf("arr[0]在内存中地址是:%p\n", &arr[0])
    fmt.Printf("arr[3]在内存中地址是:%p\n", &arr[1])

    fmt.Printf("arr[0][0]在内存中地址是:%p\n", &arr[0][0])
    fmt.Printf("arr[1][0]在内存中地址是:%p\n", &arr[1][0])
}

 所以,与一维数组一样:https://www.cnblogs.com/HuangJie-sol/p/12297878.html#_label26

 

2、使用方式 2: 直接初始化

func main() {
    // 定义/声明并赋值一个二维数组
    var arr [2][3]int = [2][3]int{
        {1, 2, 3},
        {4, 5, 6}}
    fmt.Println("arr =", arr)
}

说明:二维数组在声明/定义时也对应有四种写法[和一维数组类似]

  var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}

  var 数组名 [大小][大小]类型 = [...][大小]类型{{初值..},{初值..}}

  var 数组名 = [大小][大小]类型{{初值..},{初值..}}

  var 数组名 = [...][大小]类型{{初值..},{初值..}}

 

三十、map

map 是 key-value 数据结构,又称为字段或者关联数组。
var map 变量名 map[keytype]valuetype

1)golang 中的 map,key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组;通常 key 为 int 、string注意: slice, map 还有 function 不可以,因为这几个没法用 == 来判断

2)valuetype 的类型和 key 基本一样,通常为: 数字(整数,浮点数),string,map,struct

3)map 声明的举例:(注意:声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用)

var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string

1、案例演示:

func main() {
    // 声明/定义一个map
    var a map[string]string
    // 使用前 必须make给map分配空间,这里分配10个
    a = make(map[string]string, 10)
    a["no1"] = "soldier"
    a["no2"] = "hello"
    // map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准
    a["no2"] = "qiuxiang"
    fmt.Println(a)
}

1) map 在使用前一定要 make
2) map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准
3) map 的 value 是可以相同的.
4) map 的 key-value 是无序
5) make 内置函数数目

2、map的其它定义使用方式

func main() {
    heros := make(map[int]string)
    heros[1] = "奥特曼"
    heros[2] = "小怪兽"
    fmt.Println(heros)

    cities := map[int]string{
        1: "soldier",
        2: "qiuxiang",
    }
    fmt.Println(cities)
}

 3、map 的增删改查操作

1)map 增加和更新

// 如果 key 还没有,就是增加,如果 key 存在就是修改
map["key"] = value

2)map 删除:

// delete 是一个内置函数,如果 key 存在,就删除该 key-value,如果 key 不存在,不操作,但是也不会报错
delete(map,"key")

如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以遍历一下 key, 逐个删除或者 map = make(...),make 一个新的,让原来的成为垃圾,被 gc 回收

func main() {
    heros := make(map[int]string)
    heros[1] = "奥特曼"
    heros[2] = "小怪兽"
    fmt.Println(heros)

    cities := map[int]string{
        1: "soldier",
        2: "qiuxiang",
    }
    fmt.Println(cities)

    // 演示全部删除
    // 方式一:遍历
    for key, _ := range heros {
        delete(heros, key)
    }
    fmt.Println("全部删除后heros =", heros)
    // 方式二:make 一个新的,让原来的成为垃圾,被 gc 回收
    cities = make(map[int]string)
    fmt.Println("全部删除后cities =", cities)
}

3)map 查找:

func main() {
    heros := make(map[int]string)
    heros[1] = "奥特曼"
    heros[2] = "小怪兽"

    // 演示查找
    value, ok := heros[1]
    if ok {
        fmt.Println("heros中存在key = 1,value =", value)
    } else {
        fmt.Println("heros中不存在key = 1")
    }
}

4、map 切片

切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化了。

1)案例演示

func main() {
    // 1、声明/定义一个map切片,我理解成Java的json串
    heros := make([]map[string]string, 2)    //准备放入两个英雄
    // 2、添加一个英雄信息
    if heros[0] == nil {
        heros[0] = make(map[string]string, 2)
        heros[0]["name"] =  "奥特曼"
        heros[0]["age"] =  "500"
    }
    if heros[1] == nil {
        heros[1] = make(map[string]string, 2)
        heros[1]["name"] =  "小怪兽"
        heros[1]["age"] =  "200"
    }
    fmt.Println(heros)
    // 如果这里使用heros[2],会越界 因为只声明了2个空间,虽然map切片的个数可以动态变化,但是必须使用append函数
    // if heros[2] == nil { ...

    // 使用append添加第三个英雄
    newHero := map[string]string {
        "name": "孙悟空",
        "age": "9999+",
    }
    heros = append(heros, newHero)
    fmt.Println(heros)
}

5、map 排序

1) golang 中没有一个专门的方法针对 map 的 key 进行排序

2) golang 中的 map 默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样

3) golang 中 map 的排序,是先将 key 进行排序,然后根据 key 值遍历输出即可

import (
    "fmt"
    "sort"
)
func main() {
    // map的排序
    map1 := make(map[int]int, 10)
    map1[10] = 100
    map1[1] = 13
    map1[4] = 26
    map1[8] = 90
    fmt.Println(map1)

    // 如果按照map的key的顺序进行排序输出:
    // 1、现将map的key放入切片slice中
    // 2、对切片排序
    // 3、遍历切片,然后按照key来输出map
    var keys []int
    for key, _ := range map1 {
        keys  = append(keys, key)
    }
    fmt.Println("排序前,keys =", keys)
    // 排序
    sort.Ints(keys)
    fmt.Println("排序后,keys =", keys)
    
    for _,value := range keys  {
        fmt.Printf("map[%v] = %v\n", value, map1[value])
    }
}

 6、map 使用细节

1) map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map

2) map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长 键值对(key-value)

3) map 的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面 value 是一个 map 更好),比如 value 为 Student 结构体

 

三十一、面向对象编程

1) Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。

2) Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。

3) Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等

4) Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。

5) Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在 Golang 中面向接口编程是非常重要的特性。

1、快速入门-面向对象的方式(struct)

package main
import (
    "fmt"
)

// 定义一个Cat结构体
type Cat struct {
    Name string
    Age int
    Color string
}

func main() {
    // 创建一个cat变量
    var cat1 Cat
    cat1.Name = "小白"
    cat1.Age = 20
    cat1.Color = "绿色"
    fmt.Println("cat1 =", cat1)
}

 2、注意事项和细节说明

1) 字段声明语法同变量,示例:字段名 字段类型

2) 字段的类型可以为:基本类型、数组或引用类型

3) 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:
  布尔类型是 false ,数值是 0 ,字符串是 ""。
  数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]
  指针,slice,和 map 的零值都是 nil ,即还没有分配空间

type Person struct {
    Name string
    Age int
    Scores [5]float64
    ptr *int    //指针
    slice []int    //切片
    map1 map[string]string    //map
}

func main() {
    var person Person
    fmt.Println("person =", person)

    if person.ptr == nil {
        fmt.Println("ok1")
    }
    if person.slice == nil {
        fmt.Println("ok2")
    }
    if person.map1 == nil {
        fmt.Println("ok3")
    }

    // 再次声明,使用slice、map前一定要先make
    person.slice = make([]int, 10)
    person.slice[0] = 100

    person.map1 = make(map[string]string)
    person.map1["key1"] = "value1"

    fmt.Println("person =", person)
}

4) 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。

3、创建结构体变量和访问结构体字段

1)方式 1-直接声明

var person Person

2)方式 2-{}

type Person struct {
    Name string
    Age int
}

func main() {
    person2 := Person{"soldier", 20}
    fmt.Println("person2 =", person2)
}

 3)方式 3-&指针

type Person struct {
    Name string
    Age int
}

func main() {
    var p3 *Person = new(Person)
    // 因为p3是一个指针,因此标准的给字段赋值方式
    // (*p3).Name = "soldier1" 也可以写成 p3.Name = "soldier1"
    (*p3).Name = "soldier1"
    p3.Name = "soldier2"
    (*p3).Age = 10
    p3.Age = 20
    fmt.Println("person2 =", (*p3))
}

 4)方式 4-{}加&指针

type Person struct {
    Name string
    Age int
}

func main() {
    var p4 *Person = &Person{"soldier", 20}
    // (*p4).Name = "soldier1" 也可以写成 p4.Name = "soldier1"
    (*p4).Name = "soldier1"
    p4.Name = "soldier2"
    (*p4).Age = 10
    p4.Age = 20
    fmt.Println("p4 =", (*p4))

}

说明:
  1) 第 3 种和第 4 种方式返回的是 结构体指针。

  2) 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 ,比如 (*person).Name = "tom"

  3) 但 go 做了一个简化,也支持 结构体指针.字段名,比如 person.Name = "tom"。更加符合程序员使用的习惯,go 编译器底层 对 person.Name 做了转化 (*person).Name

4、结构体使用注意事项和细节

1) 结构体的所有字段在内存中是连续

package main
import (
    "fmt"
)

// 结构体
type Point struct {
    x int
    y int
}

// 结构体
type Rect struct {
    leftUp, rightDown Point
}
type Rect2 struct {
    leftUp, rightDown *Point
}

func main() {
    r1 := Rect{Point{1, 2}, Point{3, 4}}
    // r1有四个int,在内存中是连续分布的
    fmt.Printf("r1.leftUp.x的地址 = %p, r1.leftUp.y的地址 = %p, r1.rightDown.x的地址 = %p, r1.rightDown.y的地址 = %p\n", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y)

    // r2有两个*Point    ,这两个*Point类型本身的地址也是连续的,但它们指向的地址不一定是连续的
    r2 := Rect2{&Point{10, 20}, &Point{30, 40}}
    // 打印地址
    fmt.Printf("r2.leftUp本身的地址 = %p, r2.rightDown本身的地址 = %p\n", &r2.leftUp, &r2.rightDown)
    // 它们指向的地址不一定连续,这要看系统运行时的分配
    fmt.Printf("r2.leftUp指向的地址 = %p, r2.rightDown指向的地址 = %p\n", r2.leftUp, r2.rightDown)
}

 2) 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)

 3) 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转

 4) struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化

import (
    "fmt"
    "encoding/json"
)
type Monster struct {
    // `json:name` 就是struct 的 tag
    Name string `json:"name"`
    Age int `json:"age"`
}

func main() {
    // 1、创建一个Monster变量
    monster := Monster{"牛魔王", 200}

    // 2、将monster变量序列化为 json格式字符串 json.Marshal函数使用反射
    jsonStr, err := json.Marshal(monster)
    if err != nil {
         fmt.Println("json处理错误", err)
    }
    fmt.Println("jsonStr = ", string(jsonStr))
}

 

三十二、工厂模式

一个结构体的声明是这样的:

package model
type Student struct {
    Name string...
}

因为这里的 Student 的首字母 S 是大写的,如果我们想在其它包创建 Student 的实例(比如 main 包),引入 model 包后,就可以直接创建 Student 结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如 是 type student struct {....} 就不不行了,怎么办---> 工厂模式来解决.

1、使用工厂模式实现跨包创建结构体实例(变量)的案例:

1)如果 model 包的 结构体变量首字母大写,引入后,直接使用, 没有问题

student.go==》

package model

// 定义一个结构体,大写
type Student struct {
    Name string
    Score float64
}

main.go==》

package main
import (
    "fmt"
    "go_code/factory/model"
)

func main() {
    var stu = model.Student {
        Name : "soldier",
        Score : 78.0,
    }
    fmt.Println(stu)
}

2)如果 model 包的 结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决

student.go==》

package model

// 定义一个结构体,小写
type student struct {
    Name string
    Score float64
}

// 因为student结构体首字母小写,因此只能在student.go中使用
// 但是我们可以通过工厂模式解决 跨包使用问题
func NewStudent(name string, score float64) *student {
    return &student {
        Name : name,
        Score : score,
    }
}

main.go==》

package main
import (
    "fmt"
    "go_code/factory/model"
)

func main() {
    // student结构体首字母小写,我们可以通过工厂模式来解决
    var stu = model.NewStudent("soldier", 88.0)
    fmt.Println(stu)
    fmt.Println(*stu)
    fmt.Println("name =", stu.Name, ", score =", stu.Score)
}

2、工厂模式总结:

其实是外部方法,通过调用内部的全局方法(首字母大写),来访问内部的私有变量(首字母小写)

3、如果student结构体中的变量,如Score变为小写score,私有了,该如何调用呢?

student.go==》

package model
// 定义一个结构体,小写
type student struct {
    Name string
    score float64
}

// 因为student结构体首字母小写,因此只能在student.go中使用
// 但是我们可以通过工厂模式解决 跨包使用问题
func NewStudent(name string, score float64) *student {
    return &student {
        Name : name,
        score : score,
    }
}

// 如果score字段首字母小写,则其它包不可以直接调用,但可以提供这个方法间接调用
//  (stu *student):在go语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接受者的函数,方法和函数只差了一个,那就是方法在 func 和标识符之间多了一个参数
// 传入stu *student 表示可以修改stu的值,传入stu student不会修改stu的值
// 其实这里就相当于java中给一个类添加内部方法,类型是public
func (stu *student) GetScore() float64 {
    return stu.score
}

main.go==》

package main
import (
    "fmt"
    "go_code/factory/model"
)

func main() {
    // student结构体首字母小写,我们可以通过工厂模式来解决
    var stu = model.NewStudent("soldier", 88.0)
    fmt.Println(stu)
    fmt.Println(*stu)
    fmt.Println("name =", stu.Name, ", score =", stu.GetScore())
}

在go中,函数和方法是不一样的,函数在前面有说过,4;一个方法是一个结构体的一种行为,Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct

三十三、面向对象编程思想-抽象

我们在前面去定义一个结构体时候,实际上就是把一类事物的共有的属性(字段)行为(方法)提取出来,形成一个物理模型(结构体)。这种研究问题的方法称为抽象。

代码实现:

package main
import (
    "fmt"
)
//定义一个结构体 Account
type Account struct {
    AccountNo string
    Pwd string
    Balance float64
}
//方法
//1. 存款
func (account *Account) Deposite(money float64, pwd string) {
    //看下输入的密码是否正确
    if pwd != account.Pwd {
        fmt.Println("你输入的密码不正确")
        return
    }
    //看看存款金额是否正确
    if money <= 0 {
        fmt.Println("你输入的金额不正确")
        return
    }
    account.Balance += money
    fmt.Println("存款成功~~")
}
//取款
func (account *Account) WithDraw(money float64, pwd string) {
    //看下输入的密码是否正确
    if pwd != account.Pwd {
        fmt.Println("你输入的密码不正确")
        return
    }
    //看看取款金额是否正确
    if money <= 0 || money > account.Balance {
        fmt.Println("你输入的金额不正确")
        return
    }
    account.Balance -= money
    fmt.Println("取款成功~~")
}
//查询余额
func (account *Account) Query(pwd string) {
    //看下输入的密码是否正确
    if pwd != account.Pwd {
        fmt.Println("你输入的密码不正确")
        return
    }
    fmt.Printf("你的账号为=%v 余额=%v \n", account.AccountNo, account.Balance)
}
func main() {
    //测试
    account := Account{
        AccountNo : "gs1111111",
        Pwd : "666666",
        Balance : 100.0,
    }
    //这里可以做的更加灵活,就是让用户通过控制台来输入命令...
    //菜单....
    account.Query("666666")
    account.Deposite(200.0, "666666")
    account.Query("666666")
    account.WithDraw(150.0, "666666")
    account.Query("666666")
}
View Code

 

三十四、面向对象编程三大特性-封装

1)Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样。)

2)封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。例如java中实体类字段都是private,但其get/set方法是public的。

1、封装的实现步骤

1) 将结构体、字段(属性)的首字母小写(不能导出了,其它包不能使用,类似 private)

2) 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数

3) 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值

func (var 结构体类型名) SetXxx(参数列表) (返回值列表) {
    //加入数据验证的业务逻辑
    var.字段 = 参数
}
4) 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
func (var 结构体类型名) GetXxx() {
    return var.age;
}

特别说明:在 Golang 开发中并没有特别强调封装,这点并不像 Java. 所以提醒学过 java 的朋友,不用总是用 java 的语法特性来看待 Golang, Golang 本身对面向对象的特性做了简化的.

2、快速入门案例

package model
import "fmt"

// 定义一个结构体
type person struct {
    Name string
    age int    //其他包不能直接访问...
    sal float64
}

// 写一个工厂模式的函数,相当于构造函数
func NewPerson(name string) *person {
    return &person {
        Name : name,
    }
}

// 为了访问age 和 sal 使用封装的思想写其get/set方法
func (p *person) SetAge(age int)  {
    if age >0 && age < 150 {
        p.age = age
    } else {
        fmt.Println("年龄范围不正确...")
        // 给程序员一个默认值,int类型默认值是0
    }
}
func (p *person) GetAge() int {
    return p.age
}

func (p *person) SetSal(sal float64)  {
    if sal >= 3000 && sal <= 30000 {
        p.sal = sal
    } else {
        fmt.Println("工资范围不正确...")
        // 给程序员一个默认值,float64类型默认值是0
    }
}
func (p *person) GetSal() float64 {
    return p.sal
}
person.go
package main
import (
    "fmt"
    "go_code/fengzhuangDemo/model"
)

func main() {
    p := model.NewPerson("soldier")
    p.SetAge(21)
    p.SetSal(10000)
    fmt.Println(p)
    fmt.Println("姓名:", p.Name, ",年龄:", p.GetAge(), ",工资:", p.GetSal())
}
main.go

文件路径:

运行结果:

 

三十五、面向对象编程三大特性-继承

1) 现实中两个结构体的字段和方法几乎,但是我们却写了相同的代码, 代码复用性不强

2) 出现代码冗余,而且代码不利于维护,同时也不利于功能的扩展

3) 解决方法-通过继承方式来解决

基本介绍:

继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法,只需嵌套一个 已定义好的结构体的匿名结构体即可。

 也就是说:在 Golang 中,如果一个 struct 嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性

1、基本语法

type Goods struct {
    Name string
    Price int
}
type Book struct {
    Goods    //这里就是嵌套匿名结构体 Goods
    Writer string
}

2、快速入门案例

package main
import (
"fmt"
)
//编写一个学生考试系统
type Student struct {
    Name string
    Age int
    Score int
}
//将 Pupil 和 Graduate 共有的方法也绑定到 *Student
func (stu *Student) ShowInfo() {
    fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n", stu.Name, stu.Age, stu.Score)
}
func (stu *Student) SetScore(score int) {
    //业务判断
    stu.Score = score
}
//小学生
type Pupil struct {
    Student //嵌入了 Student 匿名结构体
}
//显示他的成绩
//这时 Pupil 结构体特有的方法,保留
func (p *Pupil) testing() {
    fmt.Println("小学生正在考试中.....")
}
//大学生, 研究生。。
type Graduate struct {
    Student //嵌入了 Student 匿名结构体
}
//显示他的成绩
//这时 Graduate 结构体特有的方法,保留
func (p *Graduate) testing() {
    fmt.Println("大学生正在考试中.....")
}
    //代码冗余.. 高中生....
func main() {
    //当我们对结构体嵌入了匿名结构体使用方法会发生变化
    pupil := &Pupil{}
    pupil.Student.Name = "tom~"
    pupil.Student.Age = 8
    pupil.testing()
    pupil.Student.SetScore(70)
    pupil.Student.ShowInfo()
    graduate := &Graduate{}
    graduate.Student.Name = "mary~"
    graduate.Student.Age = 28
    graduate.testing()
    graduate.Student.SetScore(90)
    graduate.Student.ShowInfo()
}
main.go

3、继承的深入讨论

继承的好处:

1) 代码的复用性提高了

2) 代码的扩展性和维护性提高了

1) 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用。

package main
import "fmt"

type People struct {
    Name string
    age int
}

func (p *People) SayOk() {
    fmt.Println("People SayOk", p.Name)
}

type Woman struct {
    People
}

func main() {
    var woman Woman
    woman.People.Name = "soldier"
    woman.People.age = 21
    woman.People.SayOk()
}

2) 匿名结构体字段访问可以简化

func main() {
    var woman Woman
    woman.People.Name = "soldier"
    woman.People.age = 21
    woman.People.SayOk()

    // 对上面的写法可以简化
    woman.Name = "soldier ~~~"
    woman.age = 22
    woman.SayOk()
}

对上面的代码小结

(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name

(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段

(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找..如果都找不到就报错. 

3) 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分

4) 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。

5) 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

6) 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值

 

 

 

三十六、面向对象编程-多重继承

一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承

type Goods struct {
    Name string
    Price float64
}
type Brand struct {
    Name string 
    Address string
}

type TV struct {
    Goods
    Brand
}

多重继承细节说明

1) 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。

2) 为了保证代码的简洁性,建议大家尽量不使用多重继承

 

三十七、接口(interface)

多态特性主要是通过接口来体现的。

1、快速入门案例

package main
import (
    "fmt"
)

// 声明、定义一个接口
type Usb interface {
    Start()
    Stop()
}

// 定义一个结构体 手机
type Phone struct {

}
// 让 Phone 实现 Usb 接口的方法
func (p Phone) Start() {
    fmt.Println("手机开机了...")
}
func (p Phone) Stop() {
    fmt.Println("手机关机了...")
}

// 定义一个结构体 相机
type Camera struct {
}
func (c Camera) Start() {
    fmt.Println("相机开始工作。。。")
}
func (c Camera) Stop() {
    fmt.Println("相机停止工作。。。")
}

// 定义一个结构体 电脑
type Computer struct {

}
//编写一个方法 Working 方法,接收一个 Usb 接口类型变量
//只要是实现了 Usb 接口 (所谓实现 Usb 接口,就是指实现了 Usb 接口声明所有方法)
//usb 变量会根据传入的实参,来判断到底是 Phone,还是 Camera
func (c Computer) Working (usb Usb) {
    // 通过 usb 接口调用
    usb.Start()
    usb.Stop()
}

func main() {
    // 先创建结构体变量
    computer := Computer{}
    phone := Phone{}
    camera := Camera{}

    // 关键点
    computer.Working(phone)
    computer.Working(camera)
}

 

2、接口概念的再说明

interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量。到某个自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。

3、基本语法

type 接口名 interface {
    method1(参数列表) 返回值列表
    method2(参数列表) 返回值列表
    .....
}

实现接口所有方法:

func (t 自定义类型) method1 (参数列表) 返回值列表 {
    // 方法实现
}
func (t 自定义类型) method2 (参数列表) 返回值列表 {
    // 方法实现
}
// .....

小结说明:
1) 接口里的所有方法都没有方法体,即接口的方法都是没有实现的方法。接口体现了程序设计的多态高内聚低偶合的思想。

2) Golang 中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字

4、注意事项和细节

1) 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

三十八、

posted @ 2020-02-12 09:46  soldier_cnblogs  阅读(346)  评论(0)    收藏  举报