美文网首页
初识Go语言

初识Go语言

作者: 小杰的快乐时光 | 来源:发表于2018-08-18 21:13 被阅读0次

    Go 语言结构

    初识Go语言

    Go 语言结构

    package main  //①
    
    import "fmt" //②
    
    func main() { //③
       /* 这是我的第一个简单的程序 */
       fmt.Println("Hello, World!")
    }
    

    代码说明:

    ① 定义包名,必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。

    ② 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。

    ③ 程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。

    说明:对比Java中的public 与 protected,Go语言中当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。

    注释

    Go语言支持两种注释:

    • 行注释以//开始,直到出现换行符时结束。

    • 块注释以/* 开始,以*/结束

    标识符

    Go语言标识符是一个非空的字母或数字串,其中第一个字符必须是字母,该标识符不能是关键字的名字。

    标识符是区分大小写的,以大写字母开头的标识符是公开的---以Go语言术语来讲就是可以导出的。其他的任何标识符都是私有的---以Go语言术语来讲就是未导出的。

    行分隔符

    在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。有两个地方必须使用分号,即当我们在一行中放入一条或多条语句时,或者使用原始的for循环语句时。

    如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

    Go语言基本数据类型

    • 布尔型: bool,布尔型的值只可以是常量 true 或者 false。 默认值为false

    Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。

    • 整型:int8,byte,int16,int,uint,uintptr等

    • 浮点型:float32(精确到小数点后6位)、float64(精确到小数点后15位),默认为 float64

    • 复数类型:complex64(两个float32组成),complex128(两个float64组成,这个为复数默认值)

    • 字符串:string

    • 字符类型:rune 等同于 int32

    • 错误类型:error

    • 派生类型:指针类型(Pointer),数组类型(array),结构化类型(struct),Channel 类型,函数类型,切片类型,接口类型(interface),Map 类型

    数据类型的长度根据运行程序所在的操作系统类型所决定的。

    比如 int类型 在32位或64位操作系统中,各自的长度就不一样。

    类型 符号 长度范围
    uint8 无符号 8位整型 (0 到 255)
    uint16 无符号 16位整型 (0 到 65535)
    uint32 无符号 32位整型 (0 到 4294967295)
    uint64 无符号 64位整型 (0 到 18446744073709551615)
    int8 有符号 8位整型 (-128 到 127)
    int16 有符号 16位整型 (-32768 到 32767)
    int32 有符号 32位整型 (-2147483648 到 2147483647)
    int64 有符号 64位整型 (-9223372036854775808 到 9223372036854775807)

    **浮点类型与其他数字类型的长度 **

    类型 长度
    float32 math.MaxFloat32大约是3.4e38。最小的非负值大约是1.4e-45。
    float64 math.MaxFloat64大约是1.8e308。最小的非负值大约是4.9e-324。
    byte 类似 uint8,是它的别名,两个可以直接赋值,无需转换。区别自定义类型
    rune 类似 int32,是它的别名,两个可以直接赋值,无需转换。区别自定义类型
    int 与 uint 一样大小,默认的整数类型,依据平台,分为 32位和 64 位
    uintptr 无符号整型,用于存放一个指针

    如果需要在不同的数值类型间进行数值运算或者比较操作,那么就必须进行类型转换,通常是将类型转换为最大的类型以防止精度丢失,类型转换采用 type(value)的方式进行。

    若需要进行缩小尺寸的类型转换,我们就需要自定义向下转换函数

    比如 int 与 uint8的转换

    func IntToUint8(a int)(uint8,error)  {
       if 0<=a && a<= math.MaxUint8{
          return uint8(a),nil
       }
       return 0,fmt.Errorf("%d is out of the uint8 range",a)
    }
    

    字符串

    在go语言中,字符串是使用UTF-8格式编码的只读的Unicode字节序列。每个字符对应一个rune类型。一旦字符串变量赋值之后,内部的字符就不能修改,英文是一个字节,中文是三个字节。

    使用range迭代字符串时,需要注意的是range迭代的是Unicode而不是字节。返回的两个值,第一个是被迭代的字符的UTF-8编码的第一个字节在字符串中的索引,第二个值的为对应的字符且类型为rune(实际就是表示unicode值的整形数据)。

    默认值是空字符串,而非NULL

    const s = "Go语言"
    for i, r := range s {
        fmt.Printf("%#U  : %d\n", r, i)
    }
    
    -----output-----
    U+0047 'G' : 0
    U+006F 'o' : 1
    U+8BED '语' : 2
    U+8A00 '言' : 5
    

    Go语言变量

    在数学概念中,变量表示没有固定值且可改变的数,从计算机角度来讲,变量是一段或者多段用来存储数据的内存。

    作为静态语言,go变量有固定的数据类型,变量类型决定了变量内存的长度和存储格式。

    我们只能修改变量值,无法改变类型。

    在编码阶段,我们用一名字来表示这段内存,但是编译后的机器码从不使用变量名,而是直接通过内存地址来访问目标数据。

    • 指定变量类型:var + 变量名 + 类型,比如
    var v1 int = 10 
    
    • 根据值自行判定变量类型:var + 变量名,比如
    var v1 = 10
    
    • 变量声明再赋值
    var v1 int
    v1 = 123
    
    • 省略 var 关键字,注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误,而且这种不能声明全局变量,也就是说只能在函数内使用。比如
     v1:= 10
    

    := 是用来明确表达同时进行变量声明与初始化的工作,类型自动推导。

    :=的使用

    它只能被用在函数体内,而不可以用于全局变量的声明与赋值。

    声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误。但是全局变量是允许声明且不使用。 同一类型的多个变量可以声明在同一行

    在下面代码中使用 := 就会产生影子变量,一般情况下不要出现下面这种情况的代码

    func main() {
       var a,b,c = 1,2,3//①
       for a:=4;a<5 ;a++  {  //内部a覆盖了外部a的值
          b:=5 //内部b覆盖了外部b的值
          c =6  //依旧是外部的c
          fmt.Printf("inner:a=%v, b=%v, c=%v\n",a,b,c)
       }
       fmt.Printf("outer:a=%v, b=%v, c=%v\n",a,b,c)
    }
    
    ----output----
    inner:a=4, b=5, c=6
    outer:a=1, b=2, c=6
    

    上面代码中其实for是创建了一个新的作用域,属于内部作用域,跟①不是在同一个作用域

    := 模式并不总是重新定义变量,也有可能是赋值操作

    func main() {
        x := 100
        println(&x)
        x,y := 200,"abc"  //在这里,x为赋值操作,y才是变量定义
        println(&x,x)
        println(&y,y)
    }
    
    ---output---
    0xc00008cf38
    0xc00008cf38 200
    0xc00008cf40 abc
    
    

    := 变为 赋值操作的前提条件是:至少有一个新变量被定义,且必须是同一个作用域,否则会产生影子变量。

    多变量声明

    //类型相同多个变量, 非全局变量var vname1, vname2, vname3 type
    vname1, vname2, vname3 = v1, v2, v3
    
    //和python很像,不需要显示声明类型,自动推断
    var vname1, vname2, vname3 = v1, v2, v3 
    
    //出现在:=左侧的变量不应该全是已经被声明过的,否则会导致编译错误
    vname1, vname2, vname3 := v1, v2, v3 
    
    // 这种因式分解关键字的写法一般用于声明全局变量
    var (
        vname1 v_type1
        vname2 v_type2
    )
    
    

    多重赋值功能

    将 i 与 j 的值互换

    i,j = j,i;
    
    

    多重返回值与空白标识符 _

    比如GetName() 函数中会返回 三个值:name1,name2,name3,而我们只需要第三个值 name3,那么我们可以采用以下方式取得。前两个值使用 空白标识符 _ 抛弃不用。

    //_ 表示丢弃这个值
    _,_,name3 = GetName();
    
    

    空白标识符 _ 在Go语言中是一个只写变量,只能写入,无法读取。它有两种用途:

    • 比如在导包时,使用 _ 空标志符导入一个包时,就是想执行里面的init函数

      import _ "github.com/go-sql-driver/mysql"
      
      
    • 舍弃不需要的值

    但是空白标记符在函数参数上使用时,传参并不能忽略该值

    func main() {
        test(1,2,"abc")
    }
    
    func test(x,y int,s string ,_ bool)  {
        fmt.Println(x,y,s)
    }
    
    

    会报如下错误

    .\mian.go:6:6: not enough arguments in call to test
        have (number, number, string)
        want (int, int, string, bool)
    
    

    变量零值

    type user struct {
        name string
    }
    
    var (
        a int
        b bool
        str string
        浮点 float32    //中文可以作为变量标识符
        n  *user
        u   user
        var demo1 [10]int
        var demo2 []int
        var demo3 map[string]string
    )
    
    

    当一个变量被var声明之后,系统自动赋予它该类型的零值:

    • int 零值为 0
    • float 零值为 0.0
    • bool 零值为 false
    • string 零值为空字符串""
    • 指针零值为 nil
    • 结构体的零值为各字段的零值集合
    • 数组的零值为 [0 0 0 0 0 0 0 0 0 0]
    • 切片零值为 nil
    • map零值为 nil
    • 函数零值为nil
    • channel零值为nil
    • interface零值为nil

    这些变量在 Go 中都是经过初始化的。大部分类型的零值在内存中占据的字节都是零,为什么不是所有呢?因为这依赖于编译器。

    nil值

    只有pointer, channel, func, interface, map, slice 这些类型的值才可以是nil。结构体变量和数组变量并不可以为nil,将一个变量声明为 nil时,需要指出该变量的类型,编译器无法猜出该变量的具体类型

    package main
    
    func main() {  
        var x = nil // 错误
        _ = x
    }
    
    

    在一个 nil 的slice中添加元素是没问题的,但对一个map做同样的事将会生成一个运行时的panic:

    package main
    
    func main() {  
        var a [10]int
        a[0] = 1
        fmt.Println(a) // 正常打印
        var m map[string]int // 或者是 var x5 map[string]int = nil 
        m["one"] = 1 
        fmt.Println(m) //  panic: assignment to entry in nil map
    }
    
    

    并不是所有的类型都可以声明为 nil

    var x = nil //错误
    var x interface{} = nil  //正确
    var x string = nil //错误,""是字符串的零值
    var x error = nil   //正确
    var x map[string]int = nil  //正确
    var x user = nil  //错误
    var x [10]int = nil //错误
    
    

    nil值可以比较,比如声明一个切片或者字典

    func main() {
        var demo1 []int
        var demo2 map[string]string
        fmt.Println(demo1 == nil) //true
        fmt.Println(demo2 == nil) //true
    }
    
    

    const关键字

    常量是一个简单值的标识符,在程序运行时,不会被修改的量。被 const 修饰的常量不能再被重新赋予任何值。可以被 const 修饰的数据类型只能是:布尔型、数字型(整数型、浮点型和复数)和字符串型,字符。在const 定义中,对常量名没有强制要求全部大写,不过我们一般都会全部字母大写,以便阅读。

    常见的定义格式

    //显式类型定义:
    const B string = "abc"
    //隐式类型定义,编译器可以根据变量(常量)的值来推断其类型。
    const B = "abc"
    
    

    同时声明多个常量

    const(
        NUM1 = 1
        NUM2 = 2
        NUM3 = 3
    )
    
    

    常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

    没有被使用的常量并不会引发编译错误。

    在Go语言中,预定义了 truefalseiota常量。

    iota关键字

    自增默认类型为 int,初始值为0,可被编译器修改的常量,每次出现 const 就会重置为 0 ,在下一个 const出现之前,每出现一次 iota,值就会加1。

    //示例1
    func main() {
       const (
          c0 = iota;
          c1 = iota;
          c2 = iota;
       )
       fmt.Println(c0,c1,c2)// 0 1 2
    }
    
    

    上面的const 赋值语句相同,因此可以简写为下面实例,后面常量如果没有赋值,则继承上一个常量值。

    //示例2
    func main() {
       const (
          c0 = iota
          c1 
          c2 
       )
       fmt.Println(c0,c1,c2)// 0 1 2
    }
    
    

    iota 的起始行 为 const 的第一行,哪怕第一行并没出现 iota,且后续自增按行序递增,而不是按上一取值递增

    //示例3
    func main() {
        fmt.Println(c0,c1,c2) // 1 1 3
    }
    
    const (
        c0 = 1 // 在这行,iota 就已经被初始化为 0,并开始在下面每行 iota 值加1。
        c1 = 1
        c2 = 1 + iota
    )
    
    

    若有新的常量声明后,iota 不再向下赋值

    //示例4
    func main() {
       fmt.Println(c0,c1,c2) // 0 8 8
    }
    
    const (
        c0 = iota
        c1 = 8
        c2
    )
    
    

    自增类型默认为 int,可以显式指定类型,进行类型转换,要注意类型取值范围。

    const (
        a         = iota
        b float32 = iota
        c         = iota
    )
    
    

    常量值也可以是某些编译器能在编译期计算出结果的表达式

    const (
        a         = len("hello world")
        b float32 = unsafe.Sizeof(uintptr(0))
    )
    
    

    常量与变量的区别

    const x  = "java"
    var y =  "golang"
    
    func main() {
        println(&x,x)
        println(&y,y)
    }
    
    ---output---
    ./main.go:16:10: cannot take the address of x
    
    

    不同于变量在运行期分配存储内存(非优化状态),常量通常会在预处理阶段直接展开,作为指令数据。

    常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获得常量的地址。

    常量声明方式不同,对编译器的影响

    const x  = 100  //隐式类型定义
    const y byte =  x  //正常进行,相当于  const y byte = 100  
     
    const a int  = 100 //显式指定常量类型,编译器会做强类型检查
    const b byte  = a //会报错:cannot use a (type int) as type byte in const initializer
    
    

    枚举

    由于Go语言并不支持Enum 关键字,但可以使用 const 与 iota 关键字来实现枚举

    func main() {
        const (
            Sunday = iota
            Monday
            Tuesday
            Wednesday
            Thursday
            Firday
            Saturday
        )
        fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Firday, Saturday)
    }
    
    ----output----
    0 1 2 3 4 5 6
    
    

    注:以大写字母开头的常量在包外可见,小写的不可见。

    //存储单位的常量枚举
    type ByteSize float64
    const (
        _ = iota // 通过赋值给空白标识符来忽略值
        KB ByteSize = 1<<(10*iota)
        MB
        GB
        TB
        PB
        EB
        ZB
        YB
    )
    
    

    浮点数的比较

    import "math"
    
    //p为自定义精度
    func IsEqual(f1,f2,p float64) bool {
       return math.Abs(f1 - f2)< p
    }
    
    

    复数表示

    var v3 complex64
    var v4 complex128
    var v5 complex64
    
    func main() {
       v3 = 3.2 + 12i
       v4 = 3.2 + 12i
       v5 = complex(3.2, 12i)
    
    

    对于一个复数 z = complex(x,y); 通过Go内置函数 real(z)获得该复数的实部x,通过imag(z)获得虚部y

    类型转换

    go强制要求使用显式类型转换

    如果转换的目标是指针/单向通道/没有返回值的函数类型,那么必须将其用括号扩起来

    func main() {
        x := 100
        p := (*int)(&x) //正确
        p := *int(&x)//错误
    }
    

    自定义类型

    可以使用 type 基于现有基础类型,结构体,函数类型创建用户自定义类型。

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

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

    和 var,const 类似,可以将多个 type 合并成组

    func main() {
        type ( //组
            user struct { //自定义结构体
                name string
                age uint8
            }
            event func(string) bool //自定义函数类型
        )
        
        u := user{
            name: "zhangsan",
            age:  20,
        }
        
        var f event = func(s string) bool {
            println(s)
            return len(s) == 0
        }
    }
    

    即便自定义类型的底层数据结构相同,也不能表示它们之间有什么关系,它们是属于完全不同的两种类型。

    自定义类型不会继承底层数据结构的方法,不能看作是底层数据结构的别名,不能做隐式转换,不能直接用于比较表达式。

    func main() {
        type data int
        var d data = 10
        var x int = d       //不能做隐式转换
        var x int = int(d)  //显式转换可以
        println( x == d)    //不能直接用于比较表达式
        println( x == int(d)) //可以比较
    }
    

    自定义类型和类型别名区别

    //定义一个别名
    type myInt = int
    //定义一个自定义类型
    type myInt1 int
    

    类型别名是Go1.9版本添加的新功能。

    类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型,拥有相同的底层结构。但是反过来讲拥有相同的底层结构不一定是别名。

    之前见过的runebyte就是类型别名,他们的定义如下:

    type byte = uint8
    type rune = int32
    

    类型别名只会在代码中存在,在编译完成后。类型别名就不存在了

    //类型定义
    type NewInt int
    
    //类型别名
    type MyInt = int
    
    func main() {
        var a NewInt
        var b MyInt
        
        fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
        fmt.Printf("type of b:%T\n", b) //type of b:int
    }
    

    自增

    自增,自减不再是运算符,只能是独立语句,不能用做表达式,也不能前置

    func main() {
        a := 1
        ++a //不能前置
        --a //不能前置
        
        a++
        a--
        
        if (a++) > 1 { //不能用作表达式
            
        }
        p := &a
        *p++ //相当于(*p)++
    }
    

    指针

    不能将内存地址与指针混为一谈。

    内存地址是内存中每个字节单元的唯一编号,而指针是一个实体。

    指针自己也需要分配内存空间,指针是一个专门用来保存地址的整形变量。

    p:= &x x := 100
    memory 0x1200 100
    address 0x800 0x1200
    • 取址运算符 ‘&’ 用于获取对像地址
    • 指针运算符 ‘*’ 用于间接引用目标对象
    • 二级指针用于获取 指向指针的指针的存放地址

    并不是所有的对象都能取地址,比如获取 map的元素地址

    判断两个指针是否相等:要么指向同一快地址,要么都为nil。

    指针支持相等运算,不支持加减运算和类型转换。

    可以通过 unsafe.POinter 将指针转换为 uintptr后进行加减运算

    值传递还是指针传递

    不管是指针,引用类型,还是其他类型参数,都是值拷贝,区别无非就是拷贝目标对象或拷贝指针而已。

    func main() {
        a := 100
        p := &a
        fmt.Printf("pointer: %p, target: %v\n",&p,p)
        test(p)
    }
    
    func test(x *int)  {
        fmt.Printf("pointer: %p, target: %v\n",&x,x)
    }
    
    ---output---
    pointer: 0xc000006028, target: 0xc00000a0c8
    pointer: 0xc000006038, target: 0xc00000a0c8
    

    表面上看传递指针比较好,但是复制的指针会延长目标对象生命周期,还有可能会导致它被分配到堆上

    要使用函数改变int,string 的值并且函数外的值也要受影响,可以使用二级指针或者返回值

    func main() {
        a := 100
        p := &a
        test(&p)
        fmt.Println(*p)
    }
    
    func test(x **int)  {
        a := 200
        *x = &a
    }
    
    ---output---
    200
    

    相关文章

      网友评论

          本文标题:初识Go语言

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