浅谈Go语言(2) - 程序实体

1. 前言

  Go 语言中的程序实体包括变量、常量、函数、结构体和接口。

  Go 是静态类型的编程语言,在声明变量或常量的时候,需要指定类型,或给予足够的信息,才可以让 Go 语言推导出它们的类型。

2. 声明变量

  国际惯例,上代码,下面主要介绍比较典型的2种方式

package main

import (
	"flag"
	"fmt"
)

func main() {
	var name string // [1]
	flag.StringVar(&name, "name", "everyone", "The greeting object.") // [2]
	fmt.Printf("Hello, %v!\n", name)
}

(1) var name = xxx

  [1][2]合并[3],把被调用的函数由flag.StringVar改为flag.String,传参的列表也需要随之修改

var name = flag.String("name", "everyone", "The greeting object.") // [3]
fmt.Printf("Hello, %v!\n", *name)

  原因:flag.String函数返回的结果值的类型是*string,是字符串的指针类型。因此,变量name代表一个指向字符串值的指针。关于Go 语言的指针,后面会专门介绍。

(2) name := xxx

  [3]改为[4],两种方式非常类似,基于[3],赋值符号=右边的代码不动,左边只留下name,再把=变成:=

name := flag.String("name", "everyone", "The greeting object.") // [4]
fmt.Printf("Hello, %v!\n", *name)

(3) 知识点

  • Go 语言自身的类型推断,而省去了对该变量的类型的声明。
  • 短变量声明,Go 语言的类型推断再加上一点点语法糖。

image

  我们只能在函数体内部使用短变量声明。在编写if、for或switch语句的时候,我们经常把它安插在初始化子句中,并用来声明一些临时的变量。

  相比之下,第一种方式更加通用,它可以被用在任何地方。

类型推断的好处

  • 少敲代码(当然,这不是最重要的)
  • 便于代码重构

  对于重构的理解,先看一段代码

package main

import (
  "flag"
  "fmt"
)

func main() {
  var name = getTheFlag() // [5]
  flag.Parse()
  fmt.Printf("Hello, %v!\n", *name)
}

func getTheFlag() *string {
  return flag.String("name", "everyone", "The greeting object.")
}

  如果代码[3]函数不是flag.String,而是自定义的函数,比如[5]中的getTheFlag

  用getTheFlag函数包裹对flag.String函数的调用,把结果直接作为getTheFlag函数的结果

我们通常把不改变某个程序与外界的任何交互方式和规则,而只改变其内部实现”的代码修改方式,叫做对该程序的重构。重构的对象可以是一行代码、一个函数、一个功能模块,甚至一个软件系统。

  可以随意改变getTheFlag函数的内部实现,及其返回结果的类型,不用修改main函数中的任何代码。

  name的类型,是在构建程序的时候,自动更新,可以理解成类型是在编译的时候确定的。

变量的重声明

  涉及了短变量声明,可以对同一个代码块中的变量进行重声明,算是一个语法糖(或者叫便利措施)。

  • 代码块:一个由花括号括起来的区域,{}称为空代码块
  • 变量的重声明只可能发生在某一个代码块中
  • 变量的重声明只有在使用短变量声明时才会发生,否则也无法通过编译
  • 被“声明并赋值”的变量必须是多个,并且其中至少有一个是新的变量。这时我们才可以说对其中的旧变量进行了重声明。

  重要代码展示:

var err error
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n")
fmt.Printf("n=%v, err=%v\n", n, err)

  执行结果:

n=17, err=<nil>

  由于变量的类型在其初始化时就已经确定了,所以对它再次声明时赋予的类型必须与其原本的类型相同,否则会产生编译错误。

var err int
n, err := io.WriteString(os.Stdout, "Hello, everyone!\n") // cannot assign error to err (type int) in multiple assignment
fmt.Printf("n=%v, err=%v\n", n, err)

(4) 特点

  两种方式各有千秋,有着各自的特点和适用场景。

  前者可以被用在任何地方,而后者只能被用在函数或者其他更小的代码块中。

3. 作用域

程序实体的访问权限有三种:

  • 包级私有
  • 模块级私有
  • 公开

(1) 一个变量与其外层代码块中的变量重名

  继续上代码

package main

import "fmt"

var block = "package"

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

func print() {
	fmt.Printf("The block is %s.\n", block)
}

  代码输出结果:

The block is inner.
The block is function.
The block is package.

  看到这里,或许会觉得奇怪,声明重名的变量是无法通过编译的,用短变量声明对已有变量进行重声明除外,但这只是对于同一个代码块而言的。

  对于不同的代码块来说,变量重名依然可以通过编译。

(2) 引用变量的机制

  • 首先,代码引用变量的时候总会最优先查找当前代码块中的那个变量
  • 其次,如果当前代码块中没有声明以此为名的变量,那么程序会沿着代码块的嵌套关系,从直接包含当前代码块的那个代码块开始,一层一层地查找。
  • 一般情况下,程序会一直查到当前代码包代表的代码块。如果仍然找不到,那么 Go 语言的编译器就会报错了。

4. 变量类型相关

(1) 类型断言

  依旧先上一段Demo代码

package main

import "fmt"

var container = []string{"zero", "one", "two"}

func main() {

	value1, ok := interface{}(container).([]string)
	fmt.Printf("value1:%v, ok:%v.\n", value1, ok)

	container := map[int]string{0: "zero", 1: "one", 2: "two"} // [1]

	value2, ok := interface{}(container).([]string)
	fmt.Printf("value2:%v, ok:%v.\n", value2, ok)

	// [2]

	fmt.Printf("The element is %q.\n", container[1])
}

  运行结果:

value1:[zero one two], ok:true.
value2:[], ok:false.
The element is "one".

  根据以上情况,继续在[2]的位置添加代码,使[1]的类型判断正确:

value3, ok := interface{}(container).(map[int]string)
fmt.Printf("value3:%v, ok:%v.\n", value3, ok)

  运行结果:

value1:[zero one two], ok:true.
value2:[], ok:false.
value3:map[0:zero 1:one 2:two], ok:true.
The element is "one".

类型断言表达式

image

interface{}(x)

  • interface{}代表空接口,任何类型都是它的实现类型
  • 任何类型的值都可以很方便地被转换成空接口的值
  • 对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)
  • interface{}(x)即上面代码中的interface{}(container)

x.(T)

  • 类型断言表达式的语法形式是x.(T)
  • x代表要被判断类型的值,这个值当下的类型必须是接口类型的

表达式结果

  • 表达式的结果可以被赋给两个变量,在这里由valueok代表。
  • 变量ok是布尔bool类型的,它将代表类型判断的结果,truefalse
  • 如果oktrue,那么被判断的值将会被自动转换为[]string类型的值,并赋给变量value
  • 如果okfalse,那么value将被赋予nil(即“空”)。

(2) 类型转换

  原则上只要源值在目标类型的可表示范围内就是合法的。

  所以uint8(255)可以把无类型的常量255转换为uint8类型的值,是因为255在[0, 255]的范围内。

int16转换为int8

  代码示例:

package main

import "fmt"

func main() {
	var srcInt = int16(-255)
	fmt.Printf("srcInt:%v\n", srcInt)
	dstInt := int8(srcInt)
	fmt.Printf("dstInt:%v\n", dstInt)
}

要想知道正确的结果,需要知道以下内容:

  • 整数在 Go 语言以及计算机中都是以补码的形式存储的
    • 原因:为了简化计算机对整数的运算过程。
    • 补码:原码各位求反再加 1。
  • int16类型的值-255的补码是1111111100000001
  • 转换为int8类型,会把高位(最左边位置)上的 8 位二进制数截掉,即为:00000001
  • 00000001,最左边一位是0,表示正整数
  • 正整数的补码就等于其原码,所以dstInt的值就是1

  运行结果:

srcInt:-255
dstInt:1

总结:

  • 当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉一定数量的高位二进制数即可。
  • 当把一个浮点数类型的值转换为整数类型值时,前者的小数部分会被全部截掉。

整数转string

fmt.Printf("string(-1):%v\n", string(-1)) // 执行结果 string(-1):�

fmt.Printf("str:%v\n", strconv.Itoa(-1)) // 执行结果 str:-1

  字符'�'Unicode 代码点是U+FFFD

  它是 Unicode 标准中定义的 Replacement Character,专用于替换未知的、不被认可的以及无法展示的字符。

  提这个是因为在排查问题时也可能会遇到,你需要知道这可能是由于什么引起的。

string与切片

  一个值在从string类型向[]byte类型转换时代表着以 UTF-8 编码的字符串会被拆分成零散、独立的字节。

string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}) // 你好

UTF-8 编码的三个字节\xe4、\xbd和\xa0合在一起才能代表字符’你’,而\xe5、\xa5和\xbd合在一起才能代表字符’好’。

  一个值在从string类型向[]rune类型转换时代表着字符串会被拆分成一个个 Unicode 字符。

string([]rune{'\u4F60', '\u597D'}) // 你好

(3) 别名类型/潜在类型

  关键字type声明自定义的各种类型

别名类型

type MyString = string

  作用:主要是为了代码重构而存在的

潜在类型

type MyString2 string // 注意,这里没有等号。
  • 这种方式也可以被叫做对类型的再定义
  • MyString2string是两个不同的类型
  • MyString2是一个新的类型
  • string被称为MyString2的潜在类型

  为了便于理解,上一段代码:

func main() {
	type MyString2 string
	var sttr1 MyString2
	sttr1 = "sttr1"
	fmt.Printf("sttr1:%v\n", sttr1)

	var sttr2 string
	sttr2 = "sttr2"
	fmt.Printf("sttr2:%v\n", sttr2)

    // [1]
	sttr2 = sttr1 // cannot use sttr1 (type MyString2) as type string in assignment 
	fmt.Printf("sttr2:%v\n", sttr2)
}

  以上代码无法编译,报错如[1]所示,如果换成type MyString2 = string则ok

  潜在类型相同的不同类型的值之间是可以进行类型转换的,如下代码,可正常编译

func main() {
	type MyString2 string
	var sttr1 MyString2
	sttr1 = "sttr1"

	type MyString3 string
	var sttr2 MyString3
	sttr2 = "sttr2"

	sttr2 = MyString3(sttr1) // 如果是 sttr2 = string(sttr1) 无法编译通过
	fmt.Printf("sttr2:%v\n", sttr2)
}

  相互关系见下图:

image

  Reference:

https://golang.google.cn/ref/spec#Conversions
极客时间 - Go语言核心36讲
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小爱玄策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值