Go36-4,5,6-变量

骑士救兵 · · 510 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

上篇

Go语言中的程序实体包括变量、常量、函数、结构体和接口。Go语言是静态类型的编程语言,所以我们在声明变量或常量的时候都需要指定它们类型,或者给予足够的信息以使Go语言能够推导出它们的类型。

声明变量

声明变量的方式:

var name string
var name = "Adam"
name := "Bob"  // 短变量声明

第三种短变量声明只能在函数体内部使用。

知识点

类型推断
后两种在声明的同时还进行了赋值,没有显示的指定类型,而是利用了Go的类型推断。

代码重构
我们通常把“不改变某个程序与外界的任何交互方式和规则,而只改变其内部实现”的代码修改方式,叫做对该程序的重构。

代码块
在Go语言中,代码块一般就是一个由花括号括起来的区域,里面可以包含表达式和语句。
Go语言本身以及我们编写的代码共同形成了一个非常大的代码块,也叫全域代码块。

空代码块

func main() {}

变量重声明

变量重声明,对已经声明过的变量再次声明:

  1. 由于变量的类型在其初始化时就已经确定了,所以对它再次声明时赋予的类型必须与其原本的类型相同,否则会产生编译错误。
  2. 变量的重声明只能发生在某一个代码块中。如果与当前的变量重名的是外层代码块中的变量,那么就是另外一种含义了。
  3. 变量的重声明只有在使用短变量声明时才会发生,否则也无法通过编译。如果要声明新变量,就使用var关键字声明,用新的变量名。
  4. 被“声明并赋值”的变量必须有多个,并且其中至少有一个是新的变量。这时才可以说对其中的旧变量进行了重声明。

重声明只在短变量声明中出现,并且是多个变量的声明中出现。给新的变量赋值,给旧的变量赋新值。
变量重声明,允许我们再使用短变量声明时不用理会被赋值的多个变量中是否有包含旧变量。好处是写代码时的便利。

package main

import (
    "os"
    "io"
    "fmt"
)

func main() {
    n1, err := io.WriteString(os.Stdout, "Test1")
    if err != nil {
        fmt.Printf("ERROR: %v\n", err)
    }
    fmt.Println("写入字节(byte)数:", n1)

    n2, err := io.WriteString(os.Stdout, "测试2")  // 对err进行了重声明
    if err != nil {
        fmt.Printf("ERROR: %v\n", err)
    }
    fmt.Println("写入字节(byte)数:", n2)

    n2, err = io.WriteString(os.Stdout, "测试三")  // 这里都是旧变量,没有新变量,所以用的是赋值=
    if err != nil {
        fmt.Printf("ERROR: %v\n", err)
    }
    fmt.Println("写入字节(byte)数:", n2)
}

小结

使用关键字var和短变量声明,都可以实现对变量的“声明并赋值”。
前者可以被用在任何地方,而后者只能被用在函数或者其他更小的代码块中。
前者无法对已有的变量进行声明,就是无法处理新旧变量混在一起的情况。可以使用后者的变量重声明实现。
共同点是,都是基于“类型推断”。

中篇

一个程序实体的作用域总是会被限制在某个代码块中。而这个作用域最大的用处,就是对程序实体的访问权限的控制。对“高内聚,低耦合”这种程序设计思想的实践恰恰可以从这里开始。

变量作用域

变量重名的示例:

package main

import "fmt"

var block = "package"

func main() {
    block := "function"
    {
        block := "inner"
        fmt.Printf("block here is %s\n", block)
    }
    fmt.Printf("block here is %s\n", block)
}

/* 执行结果
PS H:\Go\src\Go36\article05\example01> go run main.go
block here is inner
block here is function
PS H:\Go\src\Go36\article05\example01>
*/

上面的代码中有4个代码块:

  • 全域代码块
  • main包代表的代码块,var block = "package"
  • main函数代表的代码块,block := "function"
  • main函数中用大括号包起来的代码块,block := "inner"

在后3个代码块中都声明了一个block的变量,赋值为不同的字符串。
声明重名的变量是无法编译通过的,但是这是对同一代码块内部而言的。上面的例子中是在不同的代码块中进行的声明。

引用变量时的查找过程
首先,会在当前代码块中查找变量。不包含任何的子代码块。
其次,如果当前代码块没有什么此变量名,一层一层往上层的代码块查找。
最后,如果都找不到,则编译器会报错。

下篇

不同代码块的变量可以重名,并且类型也可以不同。必要时,在使用之前,要先对变量的类型进行检查。

示例

下面代码中的container变量,虽然类型不同,但是都可以使用下标[0]、[1]、[2],获取到值:

package main

import "fmt"

var container = []string{"ZERO", "ONE", "TWO"}

func main() {
    container := map[int]string{0: "zero", 1: "one", 2: "two"}
    fmt.Println(container[0], container[1], container[2])
}

如果,要判断变量的类型,就要使用“类型断言”表达式。

类型断言

语法:x.(T)。
其中的x代表要被判断类型的那个值。T是要判断的类型。针对上面示例中的类型断言:

value, ok := interface{}(container).([]string)

上面是一条赋值语句,赋值符号的右边,就是一个类型断言表达式。
先把变量container的值转换为空接口的值interface{}(container)。然后再判断他的类型是否为后面.()中的类型。
有2个返回值,value和ok。ok是布尔类型,代码类型判断的结果:

  • 如果是true,被判断的值自动转换为.()中的类型的值,并且赋值给value。
  • 如果是false,value会赋值为nil,就是空。

不接收ok
这里ok也是可以没有的:

value := interface{}(container).([]string)

这样的话,如果类型不对,就是引发异常panic。

转为空接口的语法
在Go语言中,interface{}代表空接口。任何类型的值都可以很方便地被转换成空接口的值,语法:interface{}(x)。
一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。

字面量
小括号中[]string是一个类型字面量。所谓类型字面量,就是用来表示数据类型本身的若干个字符。
比如:string是表示字符串类型的字面量,uint8是表示8位无符号整数类型的字面量。

优化示例代码

修改开始的示例,在打印前,先对变量的类型进行判断,只有map或切片类型才进行打印:

package main

import "fmt"

var container = []string{"ZERO", "ONE", "TWO"}

func main() {
    container := map[int]string{0: "zero", 1: "one", 2: "two"}
    // 打印之前先要做判断,只有map或者切片类型才能通过
    _, ok1 := interface{}(container).([]string)
    _, ok2 := interface{}(container).(map[int]string)
    if !(ok1 || ok2) {
        fmt.Printf("ERROR: 类型断言失败 %T\n", container)
        return
    }
    fmt.Println(container[0], container[1], container[2])
}

另外还有一种switch语句的实现形式:

package main

import "fmt"

var container = []string{"ZERO", "ONE", "TWO"}

func main() {
    container := map[int]string{0: "zero", 1: "one", 2: "two"}
    switch v := interface{}(container).(type) {
    case []string:
        fmt.Println("[]string:", v)
    case map[int]string:
        fmt.Println("map[int]string:", v)
    default:
        fmt.Printf("ERROR: 类型断言失败 %T\n", container)
        return
    }
}

类型转换的坑

类型转换表达式的语法:T(x)。
其中的x可以是一个变量,也可以是一个代表值的字面量(比如1.23和struct{}),还可以是一个表达式。如果是表达式,表达式的结果只能是一个值。
x被叫做源值,它的类型就是源类型。T代表的类型是目标类型。

数值类型间互转

对于整数类型值、整数常量之间的类型转换,原则上只要源值在目标类型的可表示范围内就是合法的。
上面说的只是语法上合法,但是转换后的结果可能是可坑。比如,如果源整数类型的可表示范围大,而目标类型的可表示范围小:

package main

import "fmt"

func main() {
    var srcInt = int16(-255)  // 1111111100000001
    dstInt := int8(srcInt)  // 00000001,简单粗暴的截掉最前面的8位
    fmt.Println(srcInt, dstInt)
}
/* 执行结果
PS H:\Go\src\Go36\article06\example04> go run main.go
-255 1
PS H:\Go\src\Go36\article06\example04>
*/

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。补码就是原码的各位求反再加1。比如-255:
原码: 1000 0000 1111 1111
反码: 1111 1111 0000 0000 最高位是符号位,不反转。
补码: 1111 1111 0000 0001
类型转换的很简单粗暴,直接把最高的8位截掉,并不处理符号位,结果就是0000 0001,所以转换后的值就变成1了。

浮点类型转换
如果把浮点数转换为整数,则小数部分会被全部截掉:

package main

import "fmt"

func main() {
    var x = float64(1.9999999)
    y := int(x)
    fmt.Println(x, y)
}
/* 执行结果
PS H:\Go\src\Go36\article06\example05> go run main.go
1.9999999 1
PS H:\Go\src\Go36\article06\example05>
*/

整数转字符串

直接把一个整数值转换为一个string类型的值是可行的。但是,被转换的整数值应该是一个有效的Unicode码点,否则转换的结果将会是"�"。字符'�'的Unicode码点是U+FFFD。它是Unicode标准中定义的Replacement Character,专用于替换那些未知的、不被认可的以及无法展示的字符。无效的码点有很多,如果自己要搞一个测试,那么就用-1吧:

package main

import "fmt"

func main() {
    fmt.Println(string(-1))  // 一个无效的Unicode码点
    fmt.Println(string(65))  // 字符A
    fmt.Println(string(24464))  // 中文
}

字符串与切片

一个值在从string类型转为[]byte类型时,其中UTF-8编码的字符串会被拆分成零散、独立的字节。这样只有ASCII码的那部分字符是一个字节代码一个字符的。而其他字符,比如中文(UTF-8里中文字符用3个字节表示),会被拆开成3个字节。而且由于UTF-8的长度是可变的,这样还要想办法判断那几个字节应该是一个字符。
可以转为[]rune类型,这样转换时,每个字符会被拆开成一个个的Unicode字符。

package main

import "fmt"

func main() {
    s := "你好"
    s1 := []byte(s)
    fmt.Println(s1)
    s2 := []rune(s)
    fmt.Println(s2)
    for _, v := range(s1) {
        fmt.Print(string(v))  // 乱码
    }
    fmt.Println()
    for _, v := range(s2) {
        fmt.Print(string(v))
    }
    fmt.Println()
}
/* 执行结果
PS H:\Go\src\Go36\article06\example07> go run main.go
[228 189 160 229 165 189]
[20320 22909]
ä½ å¥½
你好
PS H:\Go\src\Go36\article06\example07>
*/

别名类型 和 潜在类型

别名类型声明与类型再定义之间的区别,以及由此带来的它们的值在类型转换、判等、比较和赋值操作方面的不同。

别名类型

可以用关键字type声明自定义的各种类型。比如,可以声明别名类型:

type MyString = string

上面的声明语句表示,MyString是string类型的别名类型。别名类型与其源类型除了在名称上以外,都是完全相同的。别名类型主要是为了代码重构而存在的。
Go语言的基本类型中就存在两个别名类型。byte是uint8的别名类型,而rune是int32的别名类型。

潜在类型

另外一种声明:

type MyString2 string  // 注意,这里没有等号

这种方式也可以被叫做对类型的再定义。这里MyString2是一个新的类型,和string是不同的类型。string可以被称为MyString2的潜在类型。
潜在类型相同的不同类型的值之间是可以进行类型转换的。因此,MyString2类型的值与string类型的值可以使用类型转换表达式进行互转。
但是,[]MyStrings 和 []string 是不同的潜在类型,不能做类型转换。
另外,即使是相同的潜在类型,也不能进行判等或比较,变量之间不能赋值。


有疑问加站长微信联系(非本文作者)

本文来自:51CTO博客

感谢作者:骑士救兵

查看原文:Go36-4,5,6-变量

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

510 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传