青训营第2节GO语言基础总结 | 青训营

91 阅读11分钟

Go语言变量的声明(使用var关键字) Go语言是静态类型语言,因此变量(variable)是有明确类型的,编译器也会检查变量类型的正确性。在数学概念中,变量表示没有固定值且可改变的数。但从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。

声明变量的一般形式是使用 var 关键字:

var name type

其中,var 是声明变量的关键字,name 是变量名,type 是变量的类型。

需要注意的是,Go语言和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。这样做的好处就是可以避免像C语言中那样含糊不清的声明形式,例如:int* a, b; 。其中只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写。而在 Go 中,则可以和轻松地将它们都声明为指针类型:

var a, b *int

Go语言的基本类型有:

bool string int、int8、int16、int32、int64 uint、uint8、uint16、uint32、uint64、uintptr byte // uint8 的别名 rune // int32 的别名 代表一个 Unicode 码 float32、float64 complex64、complex128 当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil 等。所有的内存在 Go 中都是经过初始化的。

变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShips 和 startDate 。

变量的声明有几种形式,通过下面几节进行整理归纳。

标准格式 Go语言的变量声明的标准格式为:

var 变量名 变量类型

变量声明以关键字 var 开头,后置变量类型,行尾无须分号。

批量格式 觉得每行都用 var 声明变量比较烦琐?没关系,还有一种为懒人提供的定义变量的方法:

var ( a int b string c []float32 d func() bool e struct { x int } ) 1 2 3 4 5 6 7 8 9 使用关键字 var 和括号,可以将一组变量定义放在一起。

简短格式 除 var 关键字外,还可使用更加简短的变量定义和初始化语法。

名字 := 表达式

需要注意的是,简短模式(short variable declaration)有以下限制:

定义变量,同时显式初始化。 不能提供数据类型。 只能用在函数内部。 和 var 形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量:

i, j := 0, 1

下面通过一段代码来演示简短格式变量声明的基本样式。

func main() { x:=100 a,s:=1, "abc" } 1 2 3 4 因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var 形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

Go语言变量的初始化 正如上一节《Go语言变量声明》中提到的Go语言在声明变量时,自动对变量对应的内存区域进行初始化操作。每个变量会初始化其类型的默认值,例如:

整型和浮点型变量的默认值为 0 和 0.0。 字符串变量的默认值为空字符串。 布尔型变量默认为 bool。 切片、函数、指针变量的默认为 nil。 当然,依然可以在变量声明时赋予变量一个初始值。

回顾C语言 在C语言中,变量在声明时,并不会对变量对应内存区域进行清理操作。此时,变量值可能是完全不可预期的结果。开发者需要习惯在使用C语言进行声明时要初始化操作,稍有不慎,就会造成不可预知的后果。

在网络上只有程序员才能看懂的“烫烫烫”和“屯屯屯”的梗,就来源于 C/C++ 中变量默认不初始化。

微软的 VC 编译器会将未初始化的栈空间以 16 进制的 0xCC 填充,而未初始化的堆空间使用 0xCD 填充,而 0xCCCC 和 0xCDCD 在中文的 GB2312 编码中刚好对应“烫”和“屯”字。

因此,如果一个字符串没有结束符\0,直接输出的内存数据转换为字符串就刚好对应“烫烫烫”和“屯屯屯”。

变量初始化的标准格式 var 变量名 类型 = 表达式

例如,游戏中,玩家的血量初始值为100。可以这样写:

var hp int = 100 1 这句代码中,hp 为变量名,类型为 int,hp 的初始值为 100。

上面代码中,100 和 int 同为 int 类型,int 可以认为是冗余信息,因此可以进一步简化初始化的写法。

编译器推导类型的格式 在标准格式的基础上,将 int 省略后,编译器会尝试根据等号右边的表达式推导 hp 变量的类型。

var hp = 100 1 等号右边的部分在编译原理里被称做右值(rvalue)。

下面是编译器根据右值推导变量类型完成初始化的例子。

var attack = 40 var defence = 20 var damageRate float32 = 0.17 var damage = float32(attack-defence) * damageRate fmt.Println(damage) 1 2 3 4 5 代码说明如下:

第 1 和 2 行,右值为整型,attack 和 defence 变量的类型为 int。

第 3 行,表达式的右值中使用了 0.17。由于Go语言和C语言一样,编译器会尽量提高精确度,以避免计算中的精度损失。所以这里如果不指定 damageRate 变量的类型,Go语言编译器会将 damageRate 类型推导为 float64,我们这里不需要 float64 的精度,所以需要强制指定类型为 float32。

第 4 行,将 attack 和 defence 相减后的数值结果依然为整型,使用 float32() 将结果转换为 float32 类型,再与 float32 类型的 damageRate 相乘后,damage 类型也是 float32 类型。

提示:damage 变量的右值是一个复杂的表达式,整个过程既有 attack 和 defence 的运算还有强制类型转换。强制类型转换会在后面的章节中介绍。

第 5 行,输出 damage 的值。

以上代码输出结果为:

3.4

短变量声明并初始化 var 的变量声明还有一种更为精简的写法,例如:

hp := 100 1 这是Go语言的推导声明写法,编译器会自动根据右值类型推断出左值的对应类型。

注意:由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。

如果 hp 已经被声明过,但依然使用:=时编译器会报错,代码如下:

// 声明 hp 变量 var hp int // 再次声明并赋值 hp := 10 1 2 3 4 编译报错如下:

no new variables on left side of :=

意思是,在“:=”的左边没有新变量出现,意思就是“:=”的左边变量已经被声明了。

短变量声明的形式在开发中的例子较多,比如:

conn, err := net.Dial("tcp","127.0.0.1:8080") 1 net.Dial 提供按指定协议和地址发起网络连接,这个函数有两个返回值,一个是连接对象(conn),一个是错误对象(err)。如果是标准格式将会变成:

var conn net.Conn var err error conn, err = net.Dial("tcp", "127.0.0.1:8080") 1 2 3 因此,短变量声明并初始化的格式在开发中使用比较普遍。

注意:在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错,代码如下:

conn, err := net.Dial("tcp", "127.0.0.1:8080") conn2, err := net.Dial("tcp", "127.0.0.1:8080") 1 2 上面的代码片段,编译器不会报 err 重复定义。

Go语言多个变量同时赋值 编程最简单的算法之一,莫过于变量交换。交换变量的常见算法需要一个中间变量进行变量的临时保存。用传统方法编写变量交换代码如下:

var a int = 100 var b int = 200 var t int t = a a = b b = t fmt.Println(a, b) 1 2 3 4 5 6 7 在计算机刚发明时,内存非常“精贵”。这种变量交换往往是非常奢侈的。于是计算机“大牛”发明了一些算法来避免使用中间变量:

var a int = 100 var b int = 200 a = a ^ b b = b ^ a a = a ^ b fmt.Println(a, b) 1 2 3 4 5 6 这样的算法很多,但是都有一定的数值范围和类型要求。

到了Go语言时,内存不再是紧缺资源,而且写法可以更简单。使用 Go 的“多重赋值”特性,可以轻松完成变量交换的任务:

var a int = 100 var b int = 200 b, a = a, b fmt.Println(a, b) 1 2 3 4 多重赋值时,变量的左值和右值按从左到右的顺序赋值。

多重赋值在Go语言的错误处理和函数返回值中会大量地使用。例如使用Go语言进行排序时就需要使用交换,代码如下:

type IntSlice []int func (p IntSlice) Len() int { return len(p) } func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] } func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 1 2 3 4 代码说明如下:

第 1 行,将 IntSlice 声明为 []int 类型。 第 3 行,为 IntSlice 类型编写一个 Len 方法,提供切片的长度。 第 4 行,根据提供的 i、j 元素索引,获取元素后进行比较,返回比较结果。 第 5 行,根据提供的 i、j 元素索引,交换两个元素的值。 Go语言匿名变量(没有名字的变量) 在编码过程中,可能会遇到没有名称的变量、类型或方法。虽然这不是必须的,但有时候这样做可以极大地增强代码的灵活性,这些变量被统称为匿名变量。

匿名变量的特点是一个下画线“”,“”本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。例如:

func GetData() (int, int) { return 100, 200 } func main(){ a, _ := GetData() _, b := GetData() fmt.Println(a, b) } 1 2 3 4 5 6 7 8 代码运行结果:

100 200

GetData() 是一个函数,拥有两个整型返回值。每次调用将会返回 100 和 200 两个数值。

代码说明如下:

第 5 行只需要获取第一个返回值,所以将第二个返回值的变量设为下画线(匿名变量)。 第 6 行将第一个返回值的变量设为匿名变量。 匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

提示:在 Lua 等编程语言里,匿名变量也被叫做哑元变量。

Go语言变量的作用域 一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。

了解变量的作用域对我们学习Go语言来说是比较重要的,因为Go语言会在编译时检查每个变量是否使用过,一旦出现未使用的变量,就会报编译错误。如果不能理解变量的作用域,就有可能会带来一些不明所以的编译错误。

根据变量定义位置的不同,可以分为以下三个类型:

函数内定义的变量称为局部变量 函数外定义的变量称为全局变量 函数定义中的变量称为形式参数 下面就来分别介绍一下。

局部变量 在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。

局部变量不是一直存在的,它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁。

【示例】下面的 main() 函数中使用到了局部变量 a、b、c。

package main import ( "fmt" ) func main() { //声明局部变量 a 和 b 并赋值 var a int = 3 var b int = 4 //声明局部变量 c 并计算 a 和 b 的和 c := a + b fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c) } 1 2 3 4 5 6 7 8 9 10 11 12 运行结果如下所示:

a = 3, b = 4, c = 7

全局变量 在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源文件需要使用“import”关键字引入全局变量所在的源文件之后才能使用这个全局变量。

全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。

【示例】下面代码中,第 6 行定义了全局变量 c。

package main import "fmt" //声明全局变量 var c int func main() { //声明局部变量 var a, b int //初始化参数 a = 3 b = 4 c = a + b fmt.Printf("a = %d, b = %d, c = %d\n", a, b, c) } 1 2 3 4 5 6 7 8 9 10 11 12 13 运行结果如下所示:

a = 3, b = 4, c = 7

Go语言程序中全局变量与局部变量名称可以相同,但是函数体内的局部变量会被优先考虑。

package main import "fmt" //声明全局变量 var a float32 = 3.14 func main() { //声明局部变量 var a int = 3 fmt.Printf("a = %d\n", a) } 1 2 3 4 5 6 7 8 9 运行结果如下所示:

a = 3

形式参数 在定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。

形式参数会作为函数的局部变量来使用。

【示例】下面代码中第 21 行定义了形式参数 a 和 b。

package main import ( "fmt" ) //全局变量 a var a int = 13 func main() { //局部变量 a 和 b var a int = 3 var b int = 4 fmt.Printf("main() 函数中 a = %d\n", a) fmt.Printf("main() 函数中 b = %d\n", b) c := sum(a, b) fmt.Printf("main() 函数中 c = %d\n", c) } func sum(a, b int) int { fmt.Printf("sum() 函数中 a = %d\n", a) fmt.Printf("sum() 函数中 b = %d\n", b) num := a + b return num }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 运行结果如下所示:

main() 函数中 a = 3 main() 函数中 b = 4 sum() 函数中 a = 3 sum() 函数中 b = 4 main() 函数中 c = 7

Go语言整型(整数类型) Go语言的数值类型分为以下几种:整数、浮点数、复数,其中每一种都包含了不同大小的数值类型,例如有符号整数包含 int8、int16、int32、int64 等,每种数值类型都决定了对应的大小范围和是否支持正负符号。本节我们主要介绍一下整数类型。

Go语言同时提供了有符号和无符号的整数类型,其中包括 int8、int16、int32 和 int64 四种大小截然不同的有符号整数类型,分别对应 8、16、32、64 bit(二进制位)大小的有符号整数,与此对应的是 uint8、uint16、uint32 和 uint64 四种无符号整数类型。

此外还有两种整数类型 int 和 uint,它们分别对应特定 CPU 平台的字长(机器字大小),其中 int 表示有符号整数,应用最为广泛,uint 表示无符号整数。实际开发中由于编译器和计算机硬件的不同,int 和 uint 所能表示的整数大小会在 32bit 或 64bit 之间变化。

大多数情况下,我们只需要 int 一种整型即可,它可以用于循环计数器(for 循环中控制循环次数的变量)、数组和切片的索引,以及任何通用目的的整型运算符,通常 int 类型的处理速度也是最快的。

用来表示 Unicode 字符的 rune 类型和 int32 类型是等价的,通常用于表示一个 Unicode 码点。这两个名称可以互换使用。同样,byte 和 uint8 也是等价类型,byte 类型一般用于强调数值是一个原始的数据而不是一个小的整数。

最后,还有一种无符号的整数类型 uintptr,它没有指定具体的 bit 大小但是足以容纳指针。uintptr 类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。

尽管在某些特定的运行环境下 int、uint 和 uintptr 的大小可能相等,但是它们依然是不同的类型,比如 int 和 int32,虽然 int 类型的大小也可能是 32 bit,但是在需要把 int 类型当做 int32 类型使用的时候必须显示的对类型进行转换,反之亦然。

Go语言中有符号整数采用 2 的补码形式表示,也就是最高 bit 位用来表示符号位,一个 n-bit 的有符号数的取值范围是从 -2(n-1) 到 2(n-1)-1。无符号整数的所有 bit 位都用于表示非负数,取值范围是 0 到 2n-1。例如,int8 类型整数的取值范围是从 -128 到 127,而 uint8 类型整数的取值范围是从 0 到 255。

哪些情况下使用 int 和 uint 程序逻辑对整型范围没有特殊需求。例如,对象的长度使用内建 len() 函数返回,这个长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用 int 来表示。

反之,在二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用 int 和 uint。

Go语言浮点类型(小数类型) Go语言提供了两种精度的浮点数 float32 和 float64,它们的算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。

这些浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:

常量 math.MaxFloat32 表示 float32 能取到的最大数值,大约是 3.4e38; 常量 math.MaxFloat64 表示 float64 能取到的最大数值,大约是 1.8e308; float32 和 float64 能表示的最小值分别为 1.4e-45 和 4.9e-324。 一个 float32 类型的浮点数可以提供大约 6 个十进制数的精度,而 float64 则可以提供约 15 个十进制数的精度,通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。

var f float32 = 16777216 // 1 << 24 fmt.Println(f == f+1) // "true"! 1 2 浮点数在声明的时候可以只写整数部分或者小数部分,像下面这样:

const e = .71828 // 0.71828 const f = 1. // 1 1 2 很小或很大的数最好用科学计数法书写,通过 e 或 E 来指定指数部分:

const Avogadro = 6.02214129e23 // 阿伏伽德罗常数 const Planck = 6.62606957e-34 // 普朗克常数 1 2 用 Printf 函数打印浮点数时可以使用“%f”来控制保留几位小数

package main import ( "fmt" "math" ) func main() { fmt.Printf("%f\n", math.Pi) fmt.Printf("%.2f\n", math.Pi) } 1 2 3 4 5 6 7 8 9 运行结果如下所示:

3.141593 3.14

Go语言复数 在计算机中,复数是由两个浮点数表示的,其中一个表示实部(real),一个表示虚部(imag)。

Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。

复数的值由三部分组成 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,RE 和 IM 均为 float 类型,而最后的 i 是虚数单位。

声明复数的语法格式如下所示:

var name complex128 = complex(x, y)

其中 name 为复数的变量名,complex128 为复数的类型,“=”后面的 complex 为Go语言的内置函数用于为复数赋值,x、y 分别表示构成该复数的两个 float64 类型的数值,x 为实部,y 为虚部。

上面的声明语句也可以简写为下面的形式:

name := complex(x, y)

对于一个复数z := complex(x, y),可以通过Go语言的内置函数real(z)来获得该复数的实部,也就是 x;通过imag(z)获得该复数的虚部,也就是 y。

【示例】使用内置的 complex 函数构建复数,并使用 real 和 imag 函数返回复数的实部和虚部:

var x complex128 = complex(1, 2) // 1+2i var y complex128 = complex(3, 4) // 3+4i fmt.Println(xy) // "(-5+10i)" fmt.Println(real(xy)) // "-5" fmt.Println(imag(x*y)) // "10" 1 2 3 4 5 如果大家对复数的运算法则不是很了解,可以查阅《复数运算法则》,其中详细的讲解了复数的加减乘除操作。

复数也可以用==和!=进行相等比较,只有两个复数的实部和虚部都相等的时候它们才是相等的。

Go语言内置的 math/cmplx 包中提供了很多操作复数的公共方法,实际操作中建议大家使用复数默认的 complex128 类型,因为这些内置的包中都使用 complex128 类型作为参数。

Go语言输出正弦函数(Sin)图像 在Go语言中,正弦函数由 math 包提供,函数入口为 math.Sin,正弦函数的参数为 float64,返回值也是 float64。在使用正弦函数时,根据实际精度可以进行转换。

Go语言的标准库支持对图片像素进行访问,并且支持输出各种图片格式,如 JPEG、PNG、GIF 等。

首先给出本节完整的代码:

package main import ( "image" "image/color" "image/png" "log" "math" "os" ) func main() { // 图片大小 const size = 300 // 根据给定大小创建灰度图 pic := image.NewGray(image.Rect(0, 0, size, size)) // 遍历每个像素 for x := 0; x < size; x++ { for y := 0; y < size; y++ { // 填充为白色 pic.SetGray(x, y, color.Gray{255}) } } // 从0到最大像素生成x坐标 for x := 0; x < size; x++ { // 让sin的值的范围在0~2Pi之间 s := float64(x) * 2 * math.Pi / size // sin的幅度为一半的像素。向下偏移一半像素并翻转 y := size/2 - math.Sin(s)*size/2 // 用黑色绘制sin轨迹 pic.SetGray(x, int(y), color.Gray{0}) } // 创建文件 file, err := os.Create("sin.png") if err != nil { log.Fatal(err) } // 使用png格式将数据写入文件 png.Encode(file, pic) //将image信息写入文件中 // 关闭文件 file.Close() }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 设置图片背景色 以下是设置图片背景的代码:

// 图片大小 const size = 300 // 根据给定大小创建灰度图 pic := image.NewGray(image.Rect(0, 0, size, size)) // 遍历每个像素 for x := 0; x < size; x++ { for y := 0; y < size; y++ { // 填充为白色 pic.SetGray(x, y, color.Gray{255}) } } 1 2 3 4 5 6 7 8 9 10 11 代码说明如下:

第 2 行,声明一个 size 常量,值为 300。 第 5 行,使用 image 包的 NewGray() 函数创建一个图片对象,使用区域由 image.Rect 结构提供,image.Rect 描述一个方形的两个定位点 (x1,y1) 和 (x2,y2),image.Rect(0,0,size,size) 表示使用完整灰度图像素,尺寸为宽 300,长 300。 第 8 行和第 9 行,遍历灰度图的所有像素。 第 11 行,将每一个像素的灰度设为 255,也就是白色。 灰度图是一种常见的图片格式,一般情况下颜色由 8 位组成,灰度范围为 0~255,0 表示黑色,255 表示白色。

初始化好的灰度图默认的灰度值都是 0,对的是黑色,由于显示效果的效果不是很好,所以这里将所有像素设置为 255,也就是白色。

绘制正弦函数轨迹 正弦函数是一个周期函数,定义域是实数集,取值范围是 [-1, 1]。用编程的通俗易懂的话来说就是:math.Sin 函数的参数支持任意浮点数范围,函数返回值的范围总是在 -1~1 之间(包含 1、-1)。

要将正弦函数放在图片上需要考虑以下一些因素:

math.Sin 的返回值在 -1~1 之间,需要考虑将正弦的输出幅度变大,可以将 math.Sin 的返回值乘以一个常量进行放大。 图片的坐标系原点在左上角,而 math.Sin 基于笛卡尔坐标系原点在左下角,需要对图像进行上下翻转和平移。 将这些处理逻辑汇总为代码如下:

// 从0到最大像素生成x坐标 for x := 0; x < size; x++ { // 让sin的值的范围在0~2Pi之间 s := float64(x) * 2 * math.Pi / size // sin的幅度为一半的像素。向下偏移一半像素并翻转 y := size/2 - math.Sin(s)*size/2 // 用黑色绘制sin轨迹 pic.SetGray(x, int(y), color.Gray{0}) } 1 2 3 4 5 6 7 8 9 代码说明如下:

第 2 行,生成 0 到 size(300)的 x 坐标轴。

第 5 行,计算 math.Sin 的定义域,这段代码等效为:

rate := x / size s := rate * 2 * math.Pi 1 2 x 的范围是 0 到 size,因此除以 size 后,rate 的范围是 0~1 之间,再乘以 2π 后,s 的范围刚好是 0~2π 之间。

float64(x) 表示将整型的 x 变量转换为 float64 类型,之后运算的所有表达式将以 float64 类型进行。

第 8 行中,math.Sin(s)*size/2 表示将正弦函数的返回值幅度从 1 扩大到二分之一的 size。负号表示将正弦函数图形以图形中心上下翻转。叠加 size/2 表示将图形在 y 轴上向下偏移二分之一的 size(图片坐标系的 y 向下)。

第 11 行将计算好的 x 轴和 y 轴数据,以灰度为 0(黑色)使用 SetGray() 方法填充到像素中。

写入图片的正弦函数图像如下图所示:

写入图片文件 内存中的正弦函数图形是不可见的,我们选用 PNG 格式将图形输出为文件,Go语言提供了文件创建函数和 PNG 格式写入函数,代码如下:

// 创建文件 file, err := os.Create("sin.png") if err != nil { log.Fatal(err) } // 使用PNG格式将数据写入文件 png.Encode(file, pic) //将image信息写入文件中 // 关闭文件 file.Close() 1 2 3 4 5 6 7 8 9 代码说明如下:

第 2 行,创建 sin.png 的文件。 第 4 行,如果创建文件失败,返回错误,打印错误并终止。 第 8 行,使用 PNG 包,将图形对象写入文件中。 第 11 行,关闭文件。 Go语言bool类型(布尔类型) 一个布尔类型的值只有两种:true 或 false。if 和 for 语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。

一元操作符!对应逻辑非操作,因此!true的值为 false,更复杂一些的写法是(!true==false) ==true,实际开发中我们应尽量采用比较简洁的布尔表达式,就像用 x 来表示x==true。

var aVar = 10 aVar == 5 // false aVar == 10 // true aVar != 5 // true aVar != 10 // false 1 2 3 4 5 Go语言对于值之间的比较有非常严格的限制,只有两个相同类型的值才可以进行比较,如果值的类型是接口(interface),那么它们也必须都实现了相同的接口。如果其中一个值是常量,那么另外一个值可以不是常量,但是类型必须和该常量类型相同。如果以上条件都不满足,则必须将其中一个值的类型转换为和另外一个值的类型相同之后才可以进行比较。

布尔值可以和 &&(AND)和 ||(OR)操作符结合,并且有短路行为,如果运算符左边的值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值,因此下面的表达式总是安全的:

s != "" && s[0] == 'x' 1 其中 s[0] 操作如果应用于空字符串将会导致 panic 异常。

因为&&的优先级比||高(&& 对应逻辑乘法,|| 对应逻辑加法,乘法比加法优先级要高),所以下面的布尔表达式可以不加小括号:

if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { // ...ASCII字母或数字... } 1 2 3 4 5 布尔值并不会隐式转换为数字值 0 或 1,反之亦然,必须使用 if 语句显式的进行转换:

i := 0 if b { i = 1 } 1 2 3 4 如果需要经常做类似的转换,可以将转换的代码封装成一个函数,如下所示:

// 如果b为真,btoi返回1;如果为假,btoi返回0 func btoi(b bool) int { if b { return 1 } return 0 } 1 2 3 4 5 6 7 数字到布尔型的逆转换非常简单,不过为了保持对称,我们也可以封装一个函数:

// itob报告是否为非零。 func itob(i int) bool { return i != 0 } 1 2 Go语言中不允许将整型强制转换为布尔型,代码如下:

var n bool fmt.Println(int(n) * 2) 1 2 编译错误,输出如下:

cannot convert n (type bool) to type int

布尔型无法参与数值运算,也无法与其他类型进行转换。

Go语言字符串 一个字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。

UTF-8 是一种被广泛使用的编码格式,是文本文件的标准编码,其中包括 XML 和 JSON 在内也都使用该编码。由于该编码对占用字节长度的不定性,在Go语言中字符串也可能根据需要占用 1 至 4 个字节,这与其它编程语言如 C++、Java 或者 Python 不同(Java 始终使用 2 个字节)。Go语言这样做不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。

字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。

定义字符串 可以使用双引号""来定义字符串,字符串中可以使用转义字符来实现换行、缩进等效果,常用的转义字符包括:

\n:换行符 \r:回车符 \t:tab 键 \u 或 \U:Unicode 字符 \:反斜杠自身 package main import ( "fmt" ) func main() { var str = "C语言中文网\nGo语言教程" fmt.Println(str) } 1 2 3 4 5 6 7 8 运行结果为:

C语言中文网 Go语言教程

一般的比较运算符(==、!=、<、<=、>=、>)是通过在内存中按字节比较来实现字符串比较的,因此比较的结果是字符串自然编码的顺序。字符串所占的字节长度可以通过函数 len() 来获取,例如 len(str)。

字符串的内容(纯字节)可以通过标准索引法来获取,在方括号[ ]内写入索引,索引从 0 开始计数:

字符串 str 的第 1 个字节:str[0] 第 i 个字节:str[i - 1] 最后 1 个字节:str[len(str)-1] 需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。

注意:获取字符串中某个字节的地址属于非法行为,例如 &str[i]。

字符串拼接符“+” 两个字符串 s1 和 s2 可以通过 s := s1 + s2 拼接在一起。将 s2 追加到 s1 尾部并生成一个新的字符串 s。

可以通过下面的方式来对代码中多行的字符串进行拼接:

str := "Beginning of the string " + "second part of the string" 1 2 提示:因为编译器会在行尾自动补全分号,所以拼接字符串用的加号“+”必须放在第一行末尾。

也可以使用“+=”来对字符串进行拼接:

s := "hel" + "lo," s += "world!" fmt.Println(s) //输出 “hello, world!” 1 2 3 字符串实现基于 UTF-8 编码 Go语言中字符串的内部实现使用 UTF-8 编码,通过 rune 类型,可以方便地对每个 UTF-8 字符进行访问。当然,Go语言也支持按照传统的 ASCII 码方式逐字符进行访问。

关于字符串的 UTF-8 字符访问的详细方法,后面的章节将会详细介绍。

定义多行字符串 在Go语言中,使用双引号书写字符串的方式是字符串常见表达方式之一,被称为字符串字面量(string literal),这种双引号字面量不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用```反引号,代码如下:

const str = 第一行 第二行 第三行 \r\n fmt.Println(str) 1 2 3 4 5 6 代码运行结果:

第一行 第二行 第三行 \r\n

反引号```,是键盘上 1 键左边的键,两个反引号间的字符串将被原样赋值到 str 变量中。

在这种方式下,反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

多行字符串一般用于内嵌源码和内嵌数据等,代码如下:

const codeTemplate = // Generated by github.com/davyxu/cellnet/ protoc-gen-msg // DO NOT EDIT!{{range .Protos}} // Source: {{.Name}}{{end}} package {{.PackageName}} {{if gt .TotalMessages 0}} import ( "github.com/davyxu/cellnet" "reflect" _ "github.com/davyxu/cellnet/codec/pb" ) {{end}} func init() { {{range .Protos}} // {{.Name}}{{range .Messages}} cellnet.RegisterMessageMeta("pb","{{.FullName}}", reflect.TypeOf((*{{.Name}})(nil)).Elem(), {{.MsgID}}) {{end}} {{end}} }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 这段代码只定义了一个常量 codeTemplate,类型为字符串,使用```定义,字符串的内容为一段代码生成中使用到的 Go 源码格式。