搜索
您的当前位置:首页正文

第一章 类型

来源:二三娱乐

2.1 变量

从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。
作为静态类型语言,Go变量总是有固定的数据类型,类型决定了变量内存的长度和存储格式。我们只能修改变量值,无法改变类型。

通过类型转换或指针操作,我们可用不同方式修改变量值,但不能修改变量类型。

因为内存分配发生在运行期,所以在编码阶段我们给变量起名来表示这段内存。实际上,编译后的机器码压根不用变量名,而是直接用内存地址来访问目标数据。

定义

关键字var用于定义变量。运行时内存分配会确保变量自动初始化为二进制零值(zero value),如果显式提供初始化值则可省略变量类型,由编译器推断。

func main(){
var x,y int
var a,s = 100,"xyz"
}

简短模式

简短模式限制:

  • 定义变量,同时显式初始化;
  • 不能提供数据类型
  • 只能用在函数内部
    第三条尤其要注意,否则可能造成如下栗子,本想修改全局变量,结果变成重新定义一个同名的局部变量。
var m = 10
func main() {
    println(&m,m)
    m:=1000
    println(&m,m)
}

输出:

0x4a4138 10
0xc04202ff68 1000

不同作用域,可以重新定义变量。

func main() {
     m := 10
    println(&m,m)
    {
        m:="xyz"
        println(&m,m)
    }
}

多变量赋值

进行多变量赋值时,先计算出所有右边的值,再一次完成赋值操作。

func main() {
    x,y:=1,2
    x,y=y+3,x+2   //先算y+3,x+2,再对x,y赋值
    println(x,y)

}

输出:

go build && 2.1变量.exe
3, 5

C:\projects\gocode\src\learngo\golang_nodes\2.类型>go tool objdump -s "main\.main" 2.1变量.exe
TEXT main.main(SB) C:/projects/gocode/src/learngo/golang_nodes/2.类型/2.1变量.go
  2.1变量.go:5          0x450180                65488b0c2528000000      MOVQ GS:0x28, CX
  2.1变量.go:5          0x450189                488b8900000000          MOVQ 0(CX), CX
  2.1变量.go:5          0x450190                483b6110                CMPQ 0x10(CX), SP
  2.1变量.go:5          0x450194                7646                    JBE 0x4501dc
  2.1变量.go:5          0x450196                4883ec10                SUBQ $0x10, SP
  2.1变量.go:5          0x45019a                48896c2408              MOVQ BP, 0x8(SP)
  2.1变量.go:5          0x45019f                488d6c2408              LEAQ 0x8(SP), BP
  2.1变量.go:8          0x4501a4                e8d756fdff              CALL runtime.printlock(SB)
  2.1变量.go:8          0x4501a9                48c7042405000000        MOVQ $0x5, 0(SP)

2.2 常量和枚举

const (
    _=iota
    KB = 1<<(10*iota)  //1<< 10*1
    MB                 //1<< 10*2
    GB
)

const(
    _,_=iota,iota*10  //0 , 0*10
    a,b               //1, 1*10
    c,d               //2, 2*10
        x,y                           //同上
    e,f = 100,200         // 100,200
    g,h=iota,iota*10    // 4, 4*100   恢复iota
    
)

如果iota中断,那么后面要用枚举必须得显式恢复iota,而且后续的值按照行号递增。
自增的数据类型默认为int,可显式指定类型。

const (
  a float32 = iota

在实际编码中,建议用自定义类型实现用途明确的枚举类型。但这并不能将取值范围限定在预定义的枚举值内。

常量和变量的不同

变量在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用。
数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址。

2.3 基本类型

清晰完备的预定义基础类型,使得开发跨平台应用时无须过多考虑符号和长度差异。


基本类型.png
基本类型2.png

支持八进制、十六进制以及科学计数法。标准库math定义了各数字类型的取值范围。

func main() {
    a,b,c:=100,0144,0x64
    fmt.Println(a,b,c)
    fmt.Printf("0b%b,%#o,%#x\n",a,a,a)
    fmt.Println(math.MinInt8,math.MaxInt8)
    
}

输出

100 100 100
0b1100100,0144,0x64
-128 127

标准库strconv可在不同进制(字符串)间转换。

package main

import (
    "math"
    "strconv"
    "fmt"
)

func main() {
    x,y,z:=100,0144,0x64
    fmt.Println(x,y,z)
    fmt.Printf("0b%b,%#o,%#x\n",x,y,z)
    fmt.Println(math.MinInt8,math.MaxInt8)
    a,_:=strconv.ParseInt("1100100",2,32)
    b,_:=strconv.ParseInt("0144",8,32)
    c,_:=strconv.ParseInt("64",16,32)
    fmt.Println(a,b,c)
    println("0b"+strconv.FormatInt(a,2))
    println("0"+strconv.FormatInt(a,8))
    println("0x"+strconv.FormatInt(a,16))
}

浮点数注意小数位的有效精度。

    var a float32 = 1.1234567899  //注意,默认浮点类型是float64
    var b float32 = 1.123456789
    var c float32 = 1.1234567891
    println(a,b,c)
    println(a==b,b==c)
    fmt.Printf("%v,%v,%v\n",a,b,c)

输出:

1.1234568,1.1234568,1.1234568        //小数点后7位,四舍五入
+1.123457e+000 +1.123457e+000 +1.123457e+000
true true

变量别名

官方语言规范中有两个别名。

byte alias for uint8
rune alias for int32

别名类型无须转换,可直接赋值:

func main(){
    var a byte=0x11
    var b uint8=a
    var c uint8=a+b
    Test(c)
}

func Test(x byte){
    println(x)
}

但需要注意的是,拥有相同底层结构的就是别名。例如在64位平台上,int和int64结构完全相同,但它俩也分属不同类型,须显示转换。

2.4 引用类型

所谓引用类型特指slice、map、channel这三种预定义类型。

相比数字、数组等类型,引用类型拥有更复杂的存储结构。除分配内存外,他们还须初始化一系列属性,诸如指针、长度,甚至包括哈希分布、数据队列等。

内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型则必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化。
P.S. :注意new和make的区别:

小结:
new和make都在堆上分配内存,但是它们的行为不同,适用于不同的类型。

new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。

make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:slice、map 和 channel。

new其实完全可以不用。

避免语法歧义

在进行类型转换时,如果转换的目标是指针、单向通道或没有返回值的函数类型,那么必须使用括号,以免造成语法分解错误。
如下,如果*int不加括号就会报错

func main() {
    x:=100
    p:=(*int)(&x)
    fmt.Printf("%#v\n",*p)
    println(p)
}

2.5 自定义类型

使用关键字type定义用户自定义类型,包括基于现有基础类型创建,或者是结构体、函数类型等。

import "fmt"

type flags byte

const(
    exec flags = 1 <<iota
    write
    read
)

func main() {
    f:=read | exec
    fmt.Printf("%b\n",f)
}

输出: 101

和var、const类似,多个type定义可合并成组,可在函数或代码内定义局部类型。

type (
    flags byte
    user struct {
        name string
        age byte
    }
    event func(string)bool
)
func main() {
    u:=user{"bolen",18}
    fmt.Println(u)
    var ff event= func(s string) bool {
        println(s)
        return s!=""
    }
    ff("abc")
}

即便指定了基础类型,也只是表明他们有相同的底层数据结构,两者间是完全不同的数据类型,一定不能视作别名,不能隐式转换,不能直接用于比较表达式。

struct tag

最容易忽视的是struct tag,它也属于类型组成部分,而不仅仅是元数据描述。

package main
import (
    "fmt"
)
func main() {
    var a struct{ //匿名结构类型
        x int `x`
        s string `s`

    }

    var b struct{
        x int
        s string
    }
    b=a                      //错误:cannot use a (type struct { x int "x"; s string "s" })
                            // as type struct { x int; s string } in assignment
    fmt.Println(b)
}

同样,函数的参数顺序也属签名组成部分。

package main

func main() {
    var a func (int string)
    var b func (string int)
    b=a                 //报错
    b("s",1)
}

2.6 未命名类型

与有明确标识符的bool、int、string等类型相比,数组、切片、字典、通道等类型与具体元素类型或长度等熟悉有关,故称作未命名类型。当然,也可以用type围棋命名,从而盖面命名类型。

未命名类型转换规则:

  • 所属类型相同。
  • 基础类型相同,且其中一个是未命名类型。
  • 数据类型相同,将双向通道赋值给单向通道,且其中一个为未命名类型。
  • 将默认值nil赋值给切片、字典、通道、指针、函数或接口。
  • 对象实现了目标接口。
package main

import "fmt"

func main() {
    type data [2]int
    var d data = [2]int{1,2}  //基础类型相同,右值为未命名类型
    fmt.Println(d)

    a:=make(chan int,2)
    
    var b chan<- int =a //双向通道转换为单向通道,其中b为未命名类型。
    b<-2
    
}
Top