Go语言中的布尔类型与其他语言基本一致,关键字也为bool,可赋值为预定义的true和false,示例代码如下:
var v1 bool //声明后默认为false
v1 = true
v2 := (1 == 2) // v2也会被推导为bool类型
布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。以下的示例是一些错误的用法,会导致编译错误:
var b bool
b = 1 // 编译错误
b = bool(1) // 编译错误
以下的用法才是正确的:
var b bool
b = (1!=0) // 编译正确
fmt.Println("Result:", b) // 打印结果为Result: true
布尔可以做3种逻辑运算,&&(逻辑且),||(逻辑或),!(逻辑非)。
比较操作符:<,>, ==,!=, <=, >=。
int8 int16 int32 int64
uint8 uint16 uint32 uint64
int uint
Unicode字符rune类型等价int32
byte等价uint8
uintptr,无符号整型,
由系统决定占用位大小,足够存放指针即可,和C库或者系统接口交互
取值范围
具体类型 | 取值范围 |
---|---|
int8 | -128到127 |
uint8 | 0到255 |
int16 | -32768到32767 |
uint16 | 0到65535 |
int32 | -2147483648到2147483647 |
uint32 | 0到4294967295 |
int64 | -9223372036854775808到9223372036854775807 |
uint64 | 0到18446744073709551615 |
需要注意的是,int和int32在Go语言里被认为是两种不同的类型,编译器也不会帮你做自动类型转换,比如以下的例子:
var value2 int32 //声明后默认为0
value1 := 64 //value1将会被自动推导为int类型
value2 = value1 //编译错误
使用强制类型转换可以解决这个编译错误:
value2 = int32(value1) //编译通过
Go语言支持以下的常规整数运算:+、-、*、/、%。%是求余运算。
5 % 3 //结果为:2
Go语言支持以下几种比较运算符:>、<、==、>=、<=、!=。
Go语言支持以下所示的位运算
运算 | 含义 | 样例 |
---|---|---|
x << y | 左移 | 124 << 2 //结果为496 |
x >> y | 右移 | 124 >> 2 //结果为31 |
x ^ y | 异或 | 124 ^ 2 //结果为 126 |
x & y | 与 | 124 & 2 //结果为 0 |
x | y | 或 | 124 | 2 //结果为 126 |
^x | 取反 | ^ 124 //结果为-3 |
浮点型用于表示包含小数点的数据,Go语言中的浮点类型采用IEEE-754标准的表达式。
取值范围
类型 | 最大值 | 最小非负数 |
---|---|---|
float32 | 3.402823466385288598117041834516925440e+38 | 1.401298464324817070923729583289916131280e-45 |
float64 | 1.797693134862315708145274237317043567981e+308 | 4.940656458412465441765687928682213723651e-324 |
在Go语言中,定义一个浮点数变量的代码如下:
var fvalue1 float32 //声明后默认为0
fvalue2 := 12.0 //如果不加小数点,fvalue2会被推导为整型而不是浮点型
因为浮点数不是一种精确的表达方式,所以像整型那样直接用==来判断两个浮点数是否相等是不可行的,这可能会导致不稳定的结果。下面是一种推荐的替代方案:
import "math"
func IsEqual(f1,f2,p float64) bool {
return math.Abs(f1-f2) < p
}
复数实际上由两个实数(在计算机中用浮点数表示)构成,一个表示实部(real),一个表示虚部(imag)。
func main() {
var v1 complex64 //声明后默认为(0+0i)
v1 = 3.2 + 12i
v2 := 3.2 + 12i
v3 := complex(3.2, 12)
fmt.Println(v1, v2, v3)//输出结果为:(3.2+12i) (3.2+12i) (3.2+12i)
}
对于一个复数 z=complex(x,y),我们可以通过GO语言内置函数real(z)获取复数z的实部,imag(z)获取复数z的虚部。
func main() {
var v1 complex64
v1 = 3.2 + 12i
fmt.Println(real(v1), imag(v1))//输出结果为:3.2 12
}
在所有编程语言中都涉及到大量的字符串操作,可见熟悉对字符串的操作是何等重要。 Go中的字符串和C#中的一样(java也是),字符串内容在初始化后不可修改。 需要注意的是在Go中字符串是有UTF-8编码的,请注意保存文件时将文件编码格式改成UTF-8(特别是在windows下)。
平时常用的字符串操作如表2-3所示。
运算 | 含义 | 样例 |
---|---|---|
x+y | 字符串连接 | “Hello”+”123” //结果为Hello123 |
len(s) | 字符串长度 | len(“Hello”) //结果为5 |
s[i] | 取字符 | “hello”[1] //结果为’e’ |
更多的字符串操作,请参考标准库strings包。
Go语言支持两种方式遍历字符串。一种是以字节数组的方式遍历:
var str string //声明后默认为"",注意Go语言中没有null的定义
str ="Hello,世界"
n := len(str)
for i := 0; i< n; i ++ {
ch := str[i] //依据下标取字符串中的字符,类型为byte
fmt.Println(i,ch)
}
输出结果为:
0 72
1 101
2 108
3 108
4 111
5 44
6 228
7 184
8 150
9 231
10 149
11 140
这个字符串的长度为12,因为每个中文字符在UTF-8中占3个字节。
另一种是以Unicode字符遍历:
str:="Hello,世界"
for i,ch := range str {
fmt.Println(i,ch)//ch的类型为rune
}
输出结果为:
0 72
1 101
2 108
3 108
4 111
5 44
6 19990
9 30028
以Unicode字符方式遍历时,每个字符的类型是rune,而不是byte。
在Go语言中支持两个字符类型,要给是byte(实际上是uint8的别名),代表UTF-8字符串的单个字节的值;另一个是rune,代表单个Unicode字符。
关于rune相关操作,可查阅Go标准库的unicode包。另外unicode/utf8包也提供了UTF-8和Unicode之间的转换。
error类型本身就是一个预定义好的接口,里面定义了一个method。
type error interface {
Error() string
}
生成一个新的error并返回,一般有以下几种处理方式:
package main
import (
"errors"
"fmt"
)
type Customerror struct {
infoa string
infob string
Err error //声明后默认为nil
}
func (cerr Customerror) Error() string {
errorinfo := fmt.Sprintf("infoa : %s , infob : %s , original err info : %s ", cerr.infoa, cerr.infob, cerr.Err.Error())
return errorinfo
}
func main() {
//方法一:
//采用errors包的New方法 返回一个err的类型
var err error = errors.New("this is a new error")
//由于已经实现了error接口的方法 因此可以直接调用对应的方法
fmt.Println(err.Error())
//方法二:
//采用fmt.Errof 将string信息转化为error信息 并返回
err = fmt.Errorf("%s", "the error test for fmt.Errorf")
fmt.Println(err.Error())
//方法三:
//采用自定义的方式实现一个error的 一个duck 类型
err = &Customerror{
infoa: "err info a",
infob: "err info b",
Err: errors.New("test custom err"),
}
fmt.Println(err.Error())
}
/*output:
this is a new error
the error test for fmt.Errorf
infoa : err info a , infob : err info b , original err info : test custom err
*/
golang中的 error package 内容也比较简单,这个package中实现了error中所声明的method(Error)相当于是一个error接口的duck类型。
// Package errors implements functions to manipulate errors.
package errors
// New returns an error that formats as the given text.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
采用fmt.Errorf方法把string类型转化为error类型,在这个方法的内部,先调用fmt包中的Sprintf方法把格式化的输入转化为字符串,在使用 errors.New 方法返回error类型。
采用自定义的error类型可以先判断err的动态类型,再进行下一层的判断。
比如net.Error接口,是一个对原先error接口的再封装。在读取文件的时候判断读取器读取结束的时候的io.EOF。
//io.EOF
var EOF = errors.New("EOF")
//net.Error
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
Go中的指针是很容易学习,也是比较有趣的。一些Go编程任务使用指针更容易执行,并且不使用指针不能执行其他任务,例如通过引用调用。 如众所知,每个变量都是一个内存位置,每个内存位置都有其定义的地址,可以使用”和”号(&)运算符来访问它,这个运算符表示内存中的地址。参考下面的例子,它将打印定义的变量的地址:
package main
import "fmt"
func main() {
var a int = 10
var b *int //声明后默认为nil
fmt.Printf("Address of a variable: %x\n", &a )//输出结果:Address of a variable: c42000e2b0
}
如何使用指针?
有几个重要的操作,将非常频繁地使用指针来实现。
这是通过使用一元运算符*来返回位于操作数指定的地址的变量的值。下面的例子使用这些操作:
package main
import "fmt"
func main() {
var a int= 20 // actual variable declaration
var ip *int // pointer variable declaration
ip = &a // store address of a in pointer variable
fmt.Printf("Address of a variable: %x\n", &a) // address stored in pointer variable
fmt.Printf("Address stored in ip variable: %x\n", ip )// access the value using the pointer
fmt.Printf("Value of *ip variable: %d\n", *ip )
}
当上面的代码编译和执行时,它产生结果如下:
Address of var variable: 10328000
Address stored in ip variable: 10328000
Value of *ip variable: 20
在Go语言中的nil指针
Go编译器为指针变量分配一个Nil值,以防指针没有确切的地址分配。这是在变量声明的时候完成的。指定为nil值的指针称为nil指针。
nil指针是在几个标准库中定义的值为零的常量。参考下面的程序:
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("The value of ptr is : %x\n", ptr )//输出结果为:The value of ptr is 0,但不能直接做比较
}
在大多数操作系统上,程序不允许访问地址0处的内存,因为该内存是由操作系统保留的。 然而,存储器地址0具有特殊意义; 它表示指针不打算指向可访问的存储器位置。但是按照惯例,如果指针包含nil(零)值,则假设它不指向任何东西。
要检查是否为nil指针,可以使用if语句,如下所示:
if(ptr != nil) // succeeds if p is not nil
if(ptr == nil) // succeeds if p is null
数组就是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素(element),一个数组包含的元素的个数被称为数组的长度。
可以使用数组下标来访问数组中的元素。下面示例遍历数组并逐个打印。
for i := 0; i< len(array); i++ {
fmt.Println("Element",i,"of array is",array[i])
}
Go语言海提供了一个关键字range,用于便捷地遍历容器中的元素。上面的遍历可以简化如下:
for i,v := range array {
fmt.Println("Array element[",i,"]=",v)//数组的range打印出来是有序的
}
需要特别注意的是,在Go语言中数组是一个值类型(value type)。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。
数组的缺点有:数组的长度在定义之后无法再次修改且数组是值类型,每次传递都将产生一份副本。数组切片(slice)可以弥补数组的不足。初看起来,数组切片就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是个指针。数组切片的数据结构可以抽象为以下3个变量:
基于数组
mySlice1 := myArray[:]//基于myArray的所有元素创建数组切片
mySlice2 := myArray[:5]//基于myArray的前5个元素创建数组切片
mySlice2 := myArray[5:]//基于myArray第5个元素开始的所有元素创建数组切片
直接创建
mySlice1:= make([]int,5)//创建一个初始元素个数为5的数组切片,元素初始值为0
mySlice2:= make([]int,5,10)//创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间
mySlice3:= []int{1,2,3,4,5}//直接创建并初始化包含5个元素的数组切片
传统元素遍历方法如下:
for i := 0; i< len(slice); i++ {
fmt.Println("Element",i,"of slice is",slice[i])
}
使用range关键字遍历:
for i,v := range slice {
fmt.Println("slice[",i,"]=",v)//数组切片的range打印出来是有序的
}
与数组相比,数组切片多了一个存储能力(capacity)的概率,即元素个数和分配的空间可以是两个不同的值。数组切片支持Go语言内置的cap()函数和len()函数分别返回数组切片分配的空间大小和存储的元素个数。例如:
mySlice := make([]int,5,10)//cap(mySlice)为10,len(mySlice)为5
mySlice = append(mySlice,1,2,3)//在原mySlice的尾部添加3个元素
mySlice2 := []int{8,9,10}
mySlice = append(mySlice,mySlice2...)
需要注意的是,在的哥参数mySlice2后面加了三个点,即一个省略号,如果没有这个省略号的话,会有编译错误,因为按appand()的语义,从第二个参数起的所有参数都是待附加的元素。mySlice中的元素类型为int,直接传递mySlice2是行不通的。加上省略号相当于把mySlice2包含的所有元素打散后传入。
数组切片会自动处理存储空间不足的问题。如果追加的内容长度超过当前已分配的存储空间,数组切片会自动分配一块足够大的内存。
oldSlice := []int{1,2,3,4,5}
newSlice := oldSlice[:3] //基于oldSlice的前3个元素构建新数组切片
有意思的是,选择的oldSlice元素范围甚至可以超过所包含的元素个数,只要这个选择的范围不超过oldSlice存储能力,那么创建程序就是合法的。newSlice中超出oldSlice元素的部分都会填上0。
slice1 := []int{1,2,3,4,5}
slice2 := []int{5,4,3}
copy(slice2,slice1)//只会复制slice1的前3个元素到slice2中
copy(slice1,slice2)//只会复制slice2的前3个元素到slice1的前3个位置
map是一堆键值对的未排序集合。
var myMap map[string]PersonInfo
myMap是声明的变量名,string是键的类型,PersonInfo是值的类型。
myMap = make(map[string]PersonInfo)
myMap = make(map[string]PersonInfo,100)//myMap初始存储能力100
myMap = map[string]PersonInfo{
"1234":PersonInfo{"1","Jack","Room 101,..."},
}//创建并初始map
myMap["1234"]=PersonInfo{"1","Jack","Room 101,..."}
delete(myMap,"1234")//从myMap中删除键为"1234"的键值对。
如果”1234”这个键不存在,那么这个调用将什么都不发生,也不会有什么副作用。但如果传入的map变量的值是nil,该调用将导致程序抛出异常。
value,ok := myMap["1234"]
if ok {//找到了
//处理找到的value
}
或者
if value,ok := myMap["1234"];ok {//找到了
//处理找到的value
}
channel是Go语言在语言级别提供的goroutine间的通信方式。我们可以使用channel在两个或 多个goroutine之间传递消息。channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。如果需要跨进程通信,我们建议用 分布式系统的方法来解决,比如使用Socket或者HTTP等通信协议。Go语言对于网络方面也有非常完善的支持。 channel是类型相关的。也就是说,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。如果对Unix管道有所了解的话,就不难理解channel,可以将其认为是一种类 型安全的管道。
声明
//var chanName chan ElementType
var ch chan int
var m map[string] chan bool
定义
ch := make(chan int)
写入
ch <- value
读取
value := <- ch
读取和写入都会导致程序阻塞。
Go语言直接在语言级别支持select关键字,用于处理异步IO问题。
select {
case <- chan1:
//如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
//如果成功向chan2写入数据,则进行该case处理语句
default:
//如果上面都没有成功,则进入default处理流程
}
select类似switch语句,但不同在于,select后面不带判断条件,直接去查看case语句,且case语句里必须是一个channel操作。
给channel带上缓冲,可达到消息队列的效果。
c := make(chan int,1024)
上面创建了一个大小为1024的int类型channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。
读取数据时候可以用range关键字实现简便的循环读取。
for i := range c {
fmt.Println("Received:",i)
}
//首先,我们实现并执行一个匿名的超时等待函数
timeout := make(chan bool,1)
go func(){
time.sleep(1e9)
timeout <- true
}()
//然后我们把timeout这个channel利用起来
select {
case <- ch:
//从ch中读取数据
case <- timeout:
//一直没有从ch中读取到数据,但从timeout中读取到了数据
}
channel本身也是原生类型,与map之类的类型地位一样,也可以通过channel传递。
type PipeData struct {
value int
handler func(int) int
next chan int
}
func handle(queue chan *PipeData){
for data := range queue {
data.next <- data.handler(data.value)
}
}
流式处理
var ch2 chan<- float64 //ch2是单向channel,只用于写float64数据
var ch3 <-chan int //ch3是单向channel,只用于读取int数据
channel支持类型转换
ch4 := make(chan int)
ch5 := <-chan int(ch4) //ch5就是一个单向的读取channel
ch6 := chan<- int(ch4) //ch6是一个单向的写入channel
基于ch4可以进行初始化,实现最小权限原则
close(ch)
如何判断channel是否已经关闭:
if x,ok := <- ch;!ok{
//关闭处理程序
}
注意:以上判断如果ok为true,则会从ch中取出一个数据,可能会影响数据的连续性。
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。