当前位置: 首页 > 工具软件 > Ciao-Go > 使用案例 >

Go语言学习笔记(二)------Go的基本结构和要素

籍弘伟
2023-12-01

一、文件名、关键字、标识符

1.Go 的源文件以 .go 为后缀名存储在计算机中,这些文件名均由小写字母组成,如 hello.go 。如果文件名由多个部分组成,则使用下划线 _ 对它们进行分隔,如 hello_test.go 。

2.Go 代码中的几乎所有东西都有一个名称或标识符。另外,Go 语言也是区分大小写的,有效的标识符必须以字母(可以使用任何 UTF-8 编码的字符或 _ )开头,然后紧跟着 0 个或多个字符或Unicode 数字,如:X56、group1、_x23、i、өԑ12。不能以数字、关键字开头,不能包括运算符。

3._ 本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。

4.程序一般由关键字、常量、变量、运算符、类型和函数组成。程序中可能会使用到这些分隔符:括号 () ,中括号 [] 和大括号 {} 。程序中可能会使用到这些标点符号: . 、 , 、 ; 、 : 和 …。单个语句无需;,如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

5.25个关键字:break default func interface selectcase defer go map structchan else goto package switchconst fallthrough if range typecontinue for import return var

36 个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数:append bool byte cap close complex complex64 complex128 uint16copy false float32 float64 imag int int8 int16 uint32int32 int64 iota len make new nil panic uint64print println real recover string true uint uint8 uintp

二、Go 程序的基本结构和要素

1.每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。

2.每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。所有的包名都应该使用小写字母。

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

4.一个应用程序可以包含不同的包,而且即使你只使用 main 包也不必把所有的代码都写在一个巨大的文件里:你可以用一些较小的文件,并且在每个文件非注释的第一行都使用 package main 来指明这些文件都属于 main 包。如果你打算编译包名不是为 main 的源文件,如 pack1 包,编译后产生的对象文件将会是 pack1.a 而不是可执行程序。

5.在 Go 的安装文件里包含了一些可以直接使用的包,即标准库。在 Windows 下,标准库的位置在 Go 根目录下的子目录
pkg\windows_386 中;在 Linux 下,标准库在 Go 根目录下的子目录 pkg\linux_amd64 中(如果是安装的是 32 位,则在
linux_386 目录中)。一般情况下,标准包会存放在 $GOROOT/pkg/$GOOS_$GOARCH/ 目录下。

6.Go 的标准库包含了大量的包(如:fmt 和 os),但是你也可以创建自己的包。如果想要构建一个程序,则包和包内的文件都必须以正确的顺序进行编译。包的依赖关系决定了其构建顺序。属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,每个目录都只包含一个包。如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编。

7.Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 .o 的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。例:如果 A.go 依赖 B.go ,而 B.go 又依赖 C.go :
编译 C.go , B.go , 然后是 A.go .
为了编译 A.go , 编译器读取的是 B.o 而不是 C.o .

8.一个 Go 程序是通过 import 关键字将一组包链接在一起,导入多个包,导入包即等同于包含了这个包的所有的代码对象。如果包名以 ./开头,则 Go 会在相对目录中查找;如果包名以 / 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。

import (
"fmt"
"os"
)

9.当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Test1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出。标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的。所以,在导入一个外部包后,能够且只能够访问该包中导出的对象。

10.以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,如: import fm "fmt" 。如果你导入了一个包却没有使用它,则会在构建程序时引发错误。

package main
import fm "fmt" // 包的别名
func main() {
fm.Println("hello, world")
}

11.main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。main 函数既没有参数,也没有返回类型。在程序开始执行并完成初始化后,第一个调用(程序的入口点)的函数是 main.main() ,该函数一旦返回就表示程序已成功执行并立即退出。左大括号 { 必须与方法的声明放在同一行,这是编译器的强制规定。(避免产生func main();)。右大括号 } 需要被放在紧接着函数体的下一行。如果你的函数非常简短,你也可以将它们放在同一行:

func Sum(a, b int) int { return a + b }

12.fmt.Println与fmt.Print自动使用格式化标识符 %v 对字符串进行格式化,两者都会在每个参数之间自动增加空格,区别在于前者将字符串或变量打印并换行,后者不换行。函数 fmt.Sprintf 在程序中可以使用包含变量的字符串,fmt.Printf 函数主要用于打印输出到控制台。使用的格式化字符串作为第一个参数:func Printf(format string, list of variables to be printed) ,例如:"The operating system is: %s\n" 。这个格式化字符串可以含有一个或多个的格式化标识符,例如: %.. ,其中 .. 可以被不同类型所对应的标识符替换,如 %s 代表字符串标识符、 %v 代表使用类型的默认输出格式的标识符。这些标识符所对应的值从格式化字符串后的第一个逗号开始按照相同顺序添加,如果参数超过 1 个则同样需要使用逗号分隔。使用这些占位符可以很好地控制格式化输出的文本。

13.单纯地打印一个字符串或变量甚至可以使用预定义的方法来实现,如: print 、 println:print("ABC") 、 println("ABC") 、 println(i) (带一个变量 i)。这些函数只可以用于调试阶段,在部署程序的时候务必将它们替换成 fmt 中的相关函数。

14.程序正常退出的代码为 0 即 Program exited with code 0 ;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序。

15.在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。每一个包应该有相关注释,在 package 语句之前的块注释将被默认认为是这个包的文档说明,其中应该提供一些相关信息并对整体功能做简要的介绍。一个包可以分散在多个文件中,但是只需要在其中一个进行注释说明即可。在首行的简要注释之后可以用成段的注释来进行更详细的说明,而不必拥挤在一起。另外,在多段注释之间应以空行分隔加以区分。示例:

// Package superman 是一个包名。
//
//具体解释: Experience has shown that a small number of procedures can prove
// helpful when attempting to save the world.
package superman

16.几乎所有全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释。如果这种注释(称为文档注释)出现在函数前面,例如函数 Abcd,则要以 "Abcd..." 作为开头。示例:

// enterOrbit 是一个函数。
func enterOrbit() error {
...
}

17.变量(或常量)包含数据,这些数据可以有不同的数据类型,简称类型,类型定义了某个变量的值的集合与可对其进行操作的集合。使用 var 声明的变量的值会自动初始化为该类型的零值。类型可以是基本类型,如:int、float、bool、string;结构化(复合的),结构化的类型没有真正的值,它使用 nil 作为默认值,如:struct、array、slice、map、channel;只描述类型的行为的,如:interface。

18.使用 type 关键字可以定义你自己的类型,你可能想要定义一个结构体(第 10 章),但是也可以定义一个已经存在的类型的别名,如:
type IZ int
这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。
然后我们可以使用下面的方式声明变量:
var a IZ = 5
这里我们可以看到 int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能。

19.---不太懂---函数也可以是一个确定的类型,就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后,例如:
func FunctionName (a typea, b typeb) typeFunc
你可以在函数体中的某处返回使用类型为 typeFunc 的变量 var:
return var
一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 () 将它们括起来,如:
func FunctionName (a typea, b typeb) (t1 type1, t2 type2)
示例:

func Atoi(s string) (i int, err error)

返回的形式:

return var1, var2

这种多返回值一般用于判断某个函数是否执行成功(true/false)或与其它返回值一同返回错误消息(详见之后并行赋值)。

20.所有的结构将在这一章或接下来的章节中进一步地解释说明,但总体思路如下:
在完成包的 import 之后,开始对常量、变量和类型的定义或声明。
如果存在 init 函数的话,则对该函数进行定义(这是一个特殊的函数,每个含有该函数的包都会首先执行这个函数)。
如果当前包是 main 包,则定义 main 函数。
然后定义其余的函数,首先是类型的方法,接着是按照 main 函数中先后调用的顺序来定义相关函数,如果有很多函数,则可以按照字母顺序来进行排序。
示例 gotemplate.go

package main
import (
"fmt"
)
const c = "C"
var v int = 5
type T struct{}
func init() { // 包的初始化函数,第一个执行
}
func main() {
var a int
Func1()
// ...
fmt.Println(a)
}
func (t T) Method1() {
//...
}
func Func1() { // Func1导出函数
//...
}

21.类型转换:在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于 Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样(类型在这里的作用可以看作是一种函数):valueOfTypeB = typeB(valueOfTypeA)类型 B 的值 = 类型 B(类型 A 的值)示例:

a := 5.0 

b := int(a)

三、常量

1.存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。常量的定义格式: const identifier [type] = value 。隐式类型定义: const b = "abc",未定义类型的常量会在必要时刻根据上下文来获得相关类型。常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。

var n int
f(n + 5) // 无类型的数字型常量 “5” 它的类型在这里变成了 int

2.数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出,,反斜杠 \ 可以在常量表达式中作为多行的连接符使用:

const Ln2= 0.693147180559945309417232121458\
176568075500134360255254120680009

3.常量也允许使用并行赋值的形式,常量还可以用作枚举:

const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5
const (
Unknown = 0
Female = 1
Male = 2
)

4.iota 可以被用作枚举值,第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1,简单地讲,每遇到一次 const 关键字,iota 就重置为 0 :

const (
a = iota //0
b = iota //1
c = iota //2
)
//简写
const (
a = iota
b
c
)

四、变量

1.声明变量的一般形式是使用 var 关键字: var identifier type ,声明与赋值(初始化)语句也可以组合起来,var identifier [type] = val,因式分解关键字的写法一般用于声明全局变量:

var (
a int
b bool
str string
)
var str string = "Go says hello to the world

2.你可以在某个代码块的内层代码块中使用相同名称的变量,则此时外部的同名变量将会暂时隐藏(结束内部代码块的执行后隐藏的外部同名变量又会出现,而内部同名变量则被释放),你任何的操作都只会影响内部代码块的局部变量。

3.支持自动推断。变量的类型也可以在运行时实现自动推断,例如:

var a = 3
var (
HOME = os.Getenv("HOME")
USER = os.Getenv("USER")
GOROOT = os.Getenv("GOROOT")
)

这种写法主要用于声明包级别的全局变量,当你在函数体内声明局部变量时,应使用简短声明语法 := ,例如:

a := 1

4.程序中所用到的内存在计算机中使用一堆箱子来表示(这也是人们在讲解它的时候的画法),这些箱子被称为 “ 字 ”。根据不同的处理器以及操作系统类型,所有的字都具有 32 位(4 字节)或 64 位(8 字节)的相同长度;所有的字都使用相关的内存地址来进行表示(以十六进制数表示)。也就是说每一个字同一平台上具有相同的长度,不同平台长度不同。int、float、bool 和 string 这些基本类型和数组和结构这些复合类型都属于值类型,使用这些类型的变量直接指向存在内存中的值。当使用等号 = 将一个变量的值赋值给另一个变量时,如: j = i ,实际上是在内存中将 i 的值进行了拷贝,通过 &i 来获取变量 i 的内存地址,每次的地址都可能不一样,值类型的变量的值存储在栈中,相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同。

5.复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字)或内存地址中第一个字所在的位置,这个内存地址被称之为指针,指针实际上也被存在另外的某一个字中,当使用赋值语句 r2 = r1 时,只有引用(地址)被复制,如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响,引用类型还包括 指针,slices,maps和channel。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。

6.局部变量必须使用,全局变量是允许声明但不使用。同一类型的多个变量可以声明在同一行,如:var a, b, c int,多变量可以在同一行进行赋值,如:a, b, c = 5, 7, "abc",你想要交换两个变量的值,则可以简单地使用 a, b = b, a 。空白标识符 _ 也被用于抛弃值,如值 5在: _, b = 5, 7 中被抛弃。_ 实际上是一个只写变量,你不能得到它的值。是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回,并行赋值也被用于当一个函数返回多个返回值时,比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到: val, err = Func1(var1) 。

7.变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。init 函数也经常被用在当一个程序开始之前调用后台执行的 goroutine,如下面这个例子当中的 backend() :

func init() {
// setup preparations
go backend()
}

五、基本类型和运算符

1.一元运算符只可以用于一个值的操作(作为后缀),而二元运算符则可以和两个值或者操作数结合(作为中缀),只有两个类型相同的值才可以和二元运算符结合。如果值的类型是接口(interface),它们也必须都实现了相同的接口。

2.逻辑值可以被用于条件结构中的条件语句,以便测试某个条件是否满足。另外和 && 、或 || 与相等== 或不等 != 属于二元运算符,而非 ! 属于一元运算符。逻辑值是快捷性质的运算符,当运算符左边表达式的值已经能够决定整个表达式的值的时候(&&左边的值为 false,|| 左边的值为 true),运算符右边的表达式将不会被执行。格式化输出时 %t 来表示你要输出的值为布尔型。布尔值的好的命名能够很好地提升代码的可读性,例如以 is 或者 Is 开头的isSorted 、 isFinished 、 isVisible。 

3.Go 语言原生支持复数,其中位的运算采用补码。基于架构的类型,例如:int、uint 和 uintptr。int 和 uint 在 32 位操作系统上,它们均使用 32 位(4 个字节),在 64 位操作系统上,它们均使用 64 位(8 个字节),uintptr 的长度被设定为足够存放一个指针即可,没有float类型。与操作系统架构无关的类型都有固定的大小,例如int8,float32等,float32 精确到小数点后 7 位,float64 精确到小数点后 15 位,尽可能地使用 float64,因为 math 包中所有有关数学运算的函数都会要求接收这个类型。你可以通过增加前缀 0 来表示 8 进制数(如:077),增加前缀 0x 来表示 16 进制数(如:0xFF),以及使用 e 来表示10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)。Go 中不允许不同类型之间的混合使用,但是对于常量的类型限制非常少,因此允许常量之间的混合使用,示例:

package main
func main() {
var a int
var b int32
a = 15
b = a + a // 编译错误
b = b + 5 // 因为 5 是常量,所以可以通过编译
}

可以显示转换:

package main
import "fmt"
func main() {
var n int16 = 34
var m int32
// compiler error: cannot use n (type int16) as type int32 in assignment
//m = n
m = int32(n)
fmt.Printf("32 bit int is: %d\n", m)
fmt.Printf("16 bit int is: %d\n", n)
}

4.格式化说明符, %d 用于格式化整数( %x 和 %X 用于格式化 16 进制表示的数字), %g 用于格式化浮点型,( %f 输出浮点数, %e 输出科学计数表示法), %0d 用于规定输出定长的整数,其中开头的数字 0 是必须的。%n.mg 用于表示数字 n 并精确到小数点后 m 位,除了使用 g 之外,还可以使用 e 或者 f,例如:使用格式化字符串%5.2e 来输出 3.4 的结果为 3.40e+00 。安全地从 float64 转换为 int:
 

func IntFromFloat64(x float64) int {
if math.MinInt32 <= x && x <= math.MaxInt32 { // x lies in the integer range
whole, fraction := math.Modf(x)
if fraction >= 0.5 {
whole++
}
return int(whole)
}
panic(fmt.Sprintf("%g is out of the int32 range", x))
}

5.Go 拥有以下复数类型:complex64 (32 位实数和虚数),complex128 (64 位实数和虚数),复数使用 re+imI 来表示,其中 re 代表实数部分, im 代表虚数部分,I 代表根号负 1。示例:var c1 complex64 = 5 + 10i, re 和 im 的类型均为 float32,那么类型为 complex64 的复数 c 可以通过以下方式来获得:c = complex(re, im),函数 real(c) 和 imag(c) 可以分别获得相应的实数和虚数部分。格式化说明符时,可以使用 %v 来表示复数,但当你希望只表示其中的一个部分的时候需要使用 %f,你对内存的要求不是特别高,最好使用 complex128 作为计算类型,因为相关函数都使用这个类型的参数。

6.位运算只能用于整数类型的变量,且需当它们拥有等长位模式时。%b 是用于表示位的格式化标识符。二元运算符:按位与 & 、按位或 | 、按位异或 ^(不同为1)、位清除 &^ :将指定位置上的值设置为 0。

 ^作一元运算符表示是按位取反

    ^0001 0100 = 1110 1011

   故结果为235

  请思想下面代码的结果:

func main() {
     var    i  uint8  = 20
     fmt.Println(^i,^20)
}

结果是:235   -21

其实原因很简单,一个是有符号的数一个是无符号的数

20在编译器中默认为int类型,故最高位是符号位,符号位取反,所以得到的结果是负数

串联理解:负数的二进制数怎么表示?

负数的二进制数是它对应的正数按位取反得到反码,再加上1得到的补码

例如:3的二进制为00000000 00000000 00000000 00000011

反码:            11111111 11111111 11111111 11111100

补码:反码加1:  11111111 11111111 11111111 11111101

故-3的二进制为11111111 11111111 11111111 11111101

所以,一个有符号位的^操作为 这个数+1的相反数 

&^

作用:将运算符左边数据相异的位保留,相同位清零

1&^1  得0

1&^0  得1

0&^1  得0

0&^0  得0

0001 0100 &^ 0000 1111 = 0001 0000,故结果为16

>>右移和 <<左移

左移规则: 右边空出的位用0填补,高位左移溢出则舍弃该高位。如果 n 等于 2,则结果是 2 的相应倍数,即 2 的 n 次方。例如:
1 << 10 // 等于 1 KB
1 << 20 // 等于 1 MB
1 << 30 // 等于 1 GB

右移规则: 左边空出的位用0或者1填补,正数用0填补,负数用1填补。注:不同的环境填补方式可能不同,低位右移溢出则舍弃该位。如果 n 等于 2,则结果是当前值除以 2 的 n 次方。

例:0001 0100 >> 1得0000 1010   转成十进制为10             0001 0100 << 1 得0010 1000   转成十进制为40

当希望把结果赋值给第一个操作数时,可以简写为 a <<= 2 或者 b ^= a & 0xffffffff
6.使用位左移与 iota 计数配合可优雅地实现存储单位的常量枚举:

type ByteSize float64
const (
_ = iota // 通过赋值给空白标识符来忽略值
KB ByteSize = 1<<(10*iota)
MB
GB
TB
PB
EB
ZB
YB
)

7.语句 b = b + a 简写为 b+=a ,同样的写法也可用于 -= 、 *= 、 /= 、 %= ,对于整数和浮点数,可以使用一元运算符 ++ (递增)和 -- (递减),但只能用于后缀。

8.函数 rand.Float32 和 rand.Float64 返回介于 [0.0, 1.0) 之间的伪随机数,其中包括 0.0 但不包括 1.0。函数 rand.Intn返回介于 [0, n) 之间的伪随机数。可以使用 Seed(value) 函数来提供伪随机数的生成种子,一般情况下都会使用当前时间的纳秒级数字。

package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
for i := 0; i < 10; i++ {
a := rand.Int()
fmt.Printf("%d / ", a)
}
for i := 0; i < 5; i++ {
r := rand.Intn(8)
fmt.Printf("%d / ", r)
}
fmt.Println()
timens := int64(time.Now().Nanosecond())
rand.Seed(timens)
for i := 0; i < 10; i++ {
fmt.Printf("%2.2f / ", 100*rand.Float32())
}
}

9.类型定义:当你在使用某个类型时,你可以给它起另一个名字,用于简化名称或解决名称冲突。实际上,类型定义得到的新类型并非和原类型完全相同,新类型不会拥有原类型所附带的方法;TZ 可以自定义一个方法用来输出更加人性化的时区信息。类型别名是完全相同,只是一个小名,例如TZ=int

package main
import "fmt"
type TZ int
func main() {
var a, b TZ = 3, 4
c := a + b
fmt.Printf("c has the value: %d", c) // 输出:c has the value: 7
}

10. byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题。例如: var ch byte = 'A' ;字符使用单引号括起来。var ch byte = 65 (ASCII A为65)或 var ch byte = '\x41'(16进制两位)或var ch byte = ' \377 '(8进制3位)。

11.Go 同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。其实 rune 也是 Go 当中的一个类型,并且是int32 的别名。书写 Unicode 字符时,需要在 16 进制数之前加上前缀 \u 或者 \U 。因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则会加上\U 前缀;前缀 \u 则总是紧跟着长度为 4 的 16 进制数,前缀 \U 紧跟着长度为 8 的 16 进制数。

var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // 打印对应的整数
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // 打印对应的字符
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 byte字节
fmt.Printf("%U - %U - %U", ch, ch2, ch3) // UTF-8 code point Unicode代码点

包 unicode 包含了一些针对测试字符的非常有用的函数(其中 ch 代表字符):
判断是否为字母: unicode.IsLetter(ch)
判断是否为数字: unicode.IsDigit(ch)
判断是否为空白符号: unicode.IsSpace(ch)
这些函数返回一个布尔值。包 utf8 拥有更多与 rune 相关的函数。

六、字符串

1.字符串是字节的定长数组,Go 支持两种字面值,解释字符串:该类字符串使用双引号括起来,其中的相关的转义字符将被替换,这些转义字符包括:\n :换行符;\r :回车符;\t :tab 键;\u 或 \U :Unicode 字符;\\ :反斜杠自身。非解释字符串:该类字符串使用反引号括起来,支持换行,例如:`This is a raw string \n` 中的 `\n\` 会被原样输出。

2.Go 中的字符串是根据长度限定,而非特殊字符 \0 。string 类型的零值为长度为零的字符串,即空字符串 "" 。

3.通过函数 len() 来获取字符串所占的字节长度,例如: len(str) 。字符串的内容(纯字节)可以通过标准索引法来获取,在中括号 [] 内写入索引,索引从 0 开始计数:字符串 str 的第 1 个字节: str[0]、第 i 个字节: str[i - 1]、最后 1 个字节: str[len(str)-1]。

4.两个字符串 s1 和 s2 可以通过 s := s1 + s2 拼接在一起。s2 追加在 s1 尾部并生成一个新的字符串 s ,编译器行尾自动补全分号的缘故,加号 + 必须放在第一行,拼接的简写形式 += 也可以用于字符串。

5.循环中使用 + 拼接字符串不是最高效的做法,更好的办法是使用函数 strings.Join() ,使用字节缓冲( bytes.Buffer )拼接更加给力。

七、 strings 和 strconv 包

1.HasPrefix 判断字符串 s 是否以 prefix 开头:strings.HasPrefix(s, prefix string) bool,HasSuffix 判断字符串 s 是否以 suffix 结尾:
strings.HasSuffix(s, suffix string) bool。示例:

package main
import (
"fmt"
"strings"
)
func main() {
var str string = "This is an example of a string"
fmt.Printf("T/F? Does the string \"%s\" have prefix %s? ", str, "Th")
fmt.Printf("%t\n", strings.HasPrefix(str, "Th"))
}

2.Contains 判断字符串 s 是否包含 substr :strings.Contains(s, substr string) bool。

3.Index 返回字符串 str 在字符串 s 中的索引( str 的第一个字符的索引),-1 表示字符串 s 不包含字符串str :strings.Index(s, str string) int。

4.LastIndex 返回字符串 str 在字符串 s 中最后出现位置的索引( str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str :
strings.LastIndex(s, str string) int。

5.如果需要查询非 ASCII 编码的字符在父字符串中的位置,建议使用以下函数来对字符进行定位:strings.IndexRune(s string, r rune) int。实际使用中的第二个参数 rune 可以是 rune 或 int, 例如 strings.IndexRune("chicken", 99) 或 strings.IndexRune("chicken",rune('k'))。示例:

package main
import (
"fmt"
"strings"
)
func main() {
var str string = "Hi, I'm Marc, Hi."
fmt.Printf("The position of \"Marc\" is: ")
fmt.Printf("%d\n", strings.Index(str, "Marc"))
fmt.Printf("The position of the first instance of \"Hi\" is: ")
fmt.Printf("%d\n", strings.Index(str, "Hi"))
fmt.Printf("The position of the last instance of \"Hi\" is: ")
fmt.Printf("%d\n", strings.LastIndex(str, "Hi"))
fmt.Printf("The position of \"Burger\" is: ")
fmt.Printf("%d\n", strings.Index(str, "Burger"))
}

6.Replace 用于将字符串 str 中的前 n 个字符串 old 替换为字符串 new ,并返回一个新的字符串,如果 n = -1 则替换所有字符串 old 为字符串 new :strings.Replace(str, old, new, n) string。

7.统计字符串出现次数Count 用于计算字符串 str 在字符串 s 中出现的非重叠次数:strings.Count(s, str string) int。示例:

package main
import (
"fmt"
"strings"
)
func main() {
var str string = "Hello, how is it going, Hugo?"
var manyG = "gggggggggg"
fmt.Printf("Number of H's in %s is: ", str)
fmt.Printf("%d\n", strings.Count(str, "H"))
fmt.Printf("Number of double g's in %s is: ", manyG)
fmt.Printf("%d\n", strings.Count(manyG, "gg"))
}

8.重复字符串:Repeat 用于重复 count 次字符串 s 并返回一个新的字符串:strings.Repeat(s, count int) string,示例:

package main
import (
"fmt"
"strings"
)
func main() {
var origS string = "Hi there! "
var newS string
newS = strings.Repeat(origS, 3)
fmt.Printf("The new repeated string is: %s\n", newS)
}

9.修改字符串大小写:ToLower 将字符串中的 Unicode 字符全部转换为相应的小写字符:strings.ToLower(s) string。ToUpper 将字符串中的 Unicode 字符全部转换为相应的大写字符:strings.ToUpper(s) string。

package main
import (
"fmt"
"strings"
)
func main() {
var orig string = "Hey, how are you George?"
var lower string
var upper string
fmt.Printf("The original string is: %s\n", orig)
lower = strings.ToLower(orig)
fmt.Printf("The lowercase string is: %s\n", lower)
upper = strings.ToUpper(orig)
fmt.Printf("The uppercase string is: %s\n", upper)
}

10.修剪字符串:使用 strings.TrimSpace(s) 来剔除字符串开头和结尾的空白符号;剔除指定字符,使用strings.Trim(s, "cut") 来将开头和结尾的 cut 去除掉。该函数的第二个参数可以包含任何字符;只想剔除开头或者结尾的字符串,使用 TrimLeft 或者 TrimRight 来实现。

11. 分割字符串:strings.Fields(s) 将会利用 1 个或多个空白符号来作为动态长度的分隔符将字符串分割成若干小块,并返回一个slice,如果字符串只包含空白符号,则返回一个长度为 0 的 slice。strings.Split(s, sep) 用于自定义分割符号来对指定字符串进行分割,同样返回 slice。因为这 2 个函数都会返回 slice,所以习惯使用 for-range 循环来对其进行处理。

12.拼接 slice 到字符串:Join 用于将元素类型为 string 的 slice 使用分割符号来拼接组成一个字符串:strings.Join(sl []string, sep string) string。示例:

package main
import (
"fmt"
"strings"
)
func main() {
str := "The quick brown fox jumps over the lazy dog"
sl := strings.Fields(str)
fmt.Printf("Splitted in slice: %v\n", sl)
for _, val := range sl {
fmt.Printf("%s - ", val)
}
fmt.Println()
str2 := "GO1|The ABC of Go|25"
sl2 := strings.Split(str2, "|")
fmt.Printf("Splitted in slice: %v\n", sl2)
for _, val := range sl2 {
fmt.Printf("%s - ", val)
}
fmt.Println()
str3 := strings.Join(sl2,";")
fmt.Printf("sl2 joined by ;: %s\n", str3)
}

输出:

Splitted in slice: [The quick brown fox jumps over the lazy dog]
The - quick - brown - fox - jumps - over - the - lazy - dog -
Splitted in slice: [GO1 The ABC of Go 25]
GO1 - The ABC of Go - 25 -
sl2 joined by ;: GO1;The ABC of Go;25

13.从字符串中读取内容:函数 strings.NewReader(str) 用于生成一个 Reader 并读取字符串中的内容,然后返回指向该 Reader 的指针,从其它类型读取内容的函数还有:Read() 从 []byte 中读取内容。ReadByte() 和 ReadRune() 从字符串中读取下一个 byte 或者 rune。

14.字符串与其它类型的转换:与字符串相关的类型转换都是通过 strconv 包实现的。 该包包含了一些变量用于获取程序运行的操作系统平台下 int 类型所占的位数,如: strconv.IntSize 。针对从数字类型转换到字符串,Go 提供了以下函数:strconv.Itoa(i int) string 返回数字 i 所表示的字符串类型的十进制数。strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int) string 将 64 位浮点型的数字转换为字符串,其中 fmt 表示格式(其值可以是 'b' 、 'e' 、 'f' 或 'g' ), prec 表示精度, bitSize 则使用 32 表示float32,用 64 表示 float64。

15.针对从字符串类型转换为数字类型,Go 提供了以下函数:strconv.Atoi(s string) (i int, err error) 将字符串转换为 int 。strconv.ParseFloat(s string, bitSize int) (f float64, err error) 将字符串转换为 float64 型。利用多返回值的特性,这些函数会返回 2 个值,第 1 个是转换后的结果(如果转换成功),第 2 个是可能出现的错误,因此,我们一般使用以下形式来进行从字符串到其它类型的转换:
val, err = strconv.Atoi(s)。示例:

package main
import (
"fmt"
"strconv"
)
func main() {
var orig string = "666"
var an int
var newS string
fmt.Printf("The size of ints is: %d\n", strconv.IntSize)
an, _ = strconv.Atoi(orig)
fmt.Printf("The integer is: %d\n", an)
an = an + 5
newS = strconv.Itoa(an)
fmt.Printf("The new string is: %s\n", newS)
}

输出:

64 位系统:
The size of ints is: 64
32 位系统:
The size of ints is: 32
The integer is: 666
The new string is: 671

八、 时间和日期

1.time 包为我们提供了一个数据类型 time.Time (作为值使用)以及显示和测量时间和日期的功能函数。当前时间可以使用 time.Now() 获取,或者使用 t.Day() 、 t.Minute() 等等来获取时间的一部分;你甚至可以自定义时间格式化字符串,例如: fmt.Printf("%02d.%02d.%4d\n", t.Day(), t.Month(), t.Year()) 将会输出 21.07.2011 。
Duration 类型表示两个连续时刻所相差的纳秒数,类型为 int64。Location 类型映射某个时区的时间,UTC 表示通用协调世界时间。
包中的一个预定义函数 func (t Time) Format(layout string) string 可以根据一个格式化字符串来将一个时间 t 转换为相应格式的字符串,使用一些预定义的格式,如: time.ANSIC 或 time.RFC822 ,一般的格式化设计是通过对于一个标准时间的格式化描述来展现的。示例:

fmt.Println(t.Format("02 Jan 2006 15:04"))

输出:

18 fed 2019 10:48

2.time函数使用示例:

package main
import (
"fmt"
"time"
)
var week time.Duration
func main() {
t := time.Now()
fmt.Println(t) // e.g. Wed Dec 21 09:52:14 +0100 RST 2011
fmt.Printf("%02d.%02d.%4d\n", t.Day(), t.Month(), t.Year())
// 21.12.2011
t = time.Now().UTC()
fmt.Println(t) // Wed Dec 21 08:52:14 +0000 UTC 2011
fmt.Println(time.Now()) // Wed Dec 21 09:52:14 +0100 RST 2011
// calculating times:
week = 60 * 60 * 24 * 7 * 1e9 //  必须为纳秒
week_from_now := t.Add(week)
fmt.Println(week_from_now) // Wed Dec 28 08:52:14 +0000 UTC 2011
// formatting times:
fmt.Println(t.Format(time.RFC822)) // 21 Dec 11 0852 UTC
fmt.Println(t.Format(time.ANSIC)) // Wed Dec 21 08:56:34 2011
fmt.Println(t.Format("02 Jan 2006 15:04")) // 21 Dec 2011 08:52
s := t.Format("20060102")
fmt.Println(t, "=>", s)
// Wed Dec 21 08:52:14 +0000 UTC 2011 => 20111221
}

3.如果你需要在应用程序在经过一定时间或周期执行某项任务(事件处理的特例),则可以使用 time.After 或者time.Ticker 。 另外, time.Sleep(Duration d) 可以实现对某个进程(实质上是 goroutine)时长为 d 的暂停。

九、指针

1.程序在内存中存储它的值,每个内存块(或字)有一个地址,通常用十六进制数表示,如: 0x6b0820。Go 语言的取地址符是 & ,放到一个变量前使用就会返回相应变量的内存地址。代码片段:

var i1 = 5
\\可能输出 An integer: 5, its location in memory: 0x6b0820,指针的格式化标识符为 %p 
fmt.Printf("An integer: %d, it's location in memory: %p\n", i1, &i1)

这个地址可以存储在一个叫做指针的特殊数据类型中,在本例中这是一个指向 int 的指针,即 i1 :此处使用 *int 表示。如果我们想调用指针 intP,我们可以这样声明它:var intP *int。然后使用 intP = &i1 是合法的,此时 intP 指向 i1。

2.你可以在指针类型前面加上 号(前缀)来获取指针所指向的内容,这里的 *号是一个类型更改器。使用一个指针引用一个值被称为间接引用。当一个指针被定义后没有分配到任何变量时,它的值为 nil 。一个指针变量通常缩写为 ptr 。对于任何一个变量 var, 如下表达式都是正确的: var == *(&var)。示例:

package main
import "fmt"
func main() {
var i1 = 5
fmt.Printf("An integer: %d, its location in memory: %p\n", i1, &i1)
var intP *int
intP = &i1
fmt.Printf("The value at memory location %p is %d\n", intP, *intP)
}

输出:

An integer: 5, its location in memory: 0x24f0820
The value at memory location 0x24f0820 is 5

3.指针可以改变引用地址的值,通过对 *p 赋另一个值来更改“对象”,这样 s 也会随之更。

package main
import "fmt"
func main() {
s := "good bye"
var p *string = &s
*p = "ciao"
fmt.Printf("Here is the pointer p: %p\n", p) // prints address
fmt.Printf("Here is the string *p: %s\n", *p) // prints string
fmt.Printf("Here is the string s: %s\n", s) // prints same string
}

输出:

Here is the pointer p: 0x2540820
Here is the string *p: ciao
Here is the string s: ciao

4.指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。指针也可以指向另一个指针,并且可以进行任意深度的嵌套,导致你可以有多级的间接引用,但在大多数情况这会使你的代码结构不清晰。对一个空指针的反向引用是不合法的,并且会使程序崩溃,示例:

package main
func main() {
var p *int = nil
*p = 0
}
// in Windows: stops only with: <exit code="-1073741819" msg="process crashed"/>
// runtime error: invalid memory address or nil pointer dereference

 

 类似资料: