类型 | 关键词 |
---|---|
布尔型 | bool |
字符型 | string |
整型 | int、int8、int16、int32、int64 |
无负号整型 | uint、uint8、uint16、uint32、uint64、uintptr(用于存储指针) |
浮点型 | float32、float64 |
字节型 | byte(uint8 的别名) |
rune型 | rune(int32 的别名) |
复数型 | complex64、complex128 |
其中,字节型主要用以处理 ascii 字符,你比如:
var foo byte = 128
bar := string(foo)
fmt.Println(bar)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YFtpcWqj-1611470445713)(http://qiniu.taoqingqiu.com/type_byte_example_output_2.png)]
而如尼型主要用以处理 unicode 字符,你比如:
var foo rune = 23628
bar := string(foo)
fmt.Println(bar)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bEQFLH0Y-1611470445714)(http://qiniu.taoqingqiu.com/type_rune_example_output.png)]
众所周知,ascii 编码中的字符占 1 字节、unicode 中占 2 字节,而作为 unicode 一种可变长实现的 utf-8,虽然字符所占字节数不定,但是中文通常占 3 字节。在 golang 中,string 类型是一种值类型,其本质是只读的 byte 数组的切片
,所以,当 len
作用在一个字符串上时,默认是得到其底层 byte 数组的 length,再加上 golang 默认编码 utf-8,所以你比如:
foo := "屌"
fmt.Println(len(foo)) // 3
那么想要得到类似于 python 中 len 的效果,则可以这样:
foo := "屌"
fmt.Println(len([]rune(foo))) // 1
或者欲得到字符串长度,也可以此般:
foo := "屌"
fmt.Println(utf8.RuneCountInString(foo)) // 1,需要 import "unicode/utf8"
此外,由于 string 类型为值类型,其零值为空字符串而非 nil。
另外,golang 全然不允许 隐式类型转换 ,只支持 显式类型转换, 别名类型 与 原有类型 之间也不行。你比如:
var foo int32 = 18
var bar int
bar = foo // 隐式类型转换,会报编译错误错误。
bar = int(foo) // 显式类型转换,成功。
此外,在 golang 中:
再外,可以通过 math.MaxInt64
、math.MinInt64
的方式得到预定义的某类型最大最小值。
一个平淡无奇的变量声明如下:
var foo string = bar
其中,bar
可以为表达式或者字面量。
另外,string
和 bar
可以有一者省略,比如:
var foo = bar
go 会根据 bar
自动推断 foo 的类型,再如:
var foo string
在没有指定初值的情况下,go 会自动以零值初始变量。至于零值,可想而知,数字类型的零值为 0,字符串的零值为 “”,布尔的零值为 false,指针的零值为 nil。而对于引用类型来说,所引用的底层数据结构会被初始为对应的零值,但是被声明为其零值的引用类型的变量,会返回 nil 作为其零值。
而当声明局部变量时,崇尚 大道至简
的 golang,则可以这样:
foo := bar
此外,多个变量的声明可以这样:
var (
foo = 12
bar = 13
)
fmt.Println(foo, bar) // 12 13
golang 中常量只能是数字、字符串、布尔值,其声明就像这样:
const foo = 10086
同样也可以批量声明,你比如:
const (
foo = 123
bar string = "淦"
)
另外,golang 中除了 true、false、nil 之外,还有一个名曰 iota
的常量,其意指微量、极少量,在常量声明时可用以生成连续常量:
const (
foo = iota + 1
bar
)
fmt.Println(foo, bar) // 1 2
或者连续位移:
const (
foo = 2 << iota
bar
)
fmt.Println(foo, bar) // 2 4
golang 中的数组为 c 语言意义上的数组,长度在声明后不可变,你比如:
var foo [2]string = [...]string{
"123", "456"}
// 局部可简化为
// foo := [...]string{"123", "456"}
数组间可以直接判断是否相等,你比如:
foo := [...]int{
1, 2, 3}
bar := [...]int{
1, 3, 2}
biu := [...]int{
1, 2, 3}
fmt.Println(foo == bar, foo == biu) // false true
而切片(slice)则是数组的一个扩展,是一种引用类型,其本质是一个指向某数组的 胖指针
,可以通过如下方式进行声明:
foo := []string{
"123", "456"}
foo := make([]string, 10, 100)
通过 make
进行切片声明时,需提供类型、长度,也可以提供容量,当容量缺省时,默认容量与长度相等。
其底层实现包含了三个必要元素:指向底层数组的指针、切片长度、切片容量。通过内置函数 len
以及 cap
可以获取到切片的长度与容积。通过一个例子可以更清晰地理解长度、容量究竟为何物:
foo := []string{
"123", "456", "789", "101112"}
bar := foo[1:3] // {"456", "789"}
fmt.Println(len(bar)) // 2
fmt.Println(cap(bar)) // 3
声明时不指定 end index,则容量为切片底层数组 start index(此例的 1) 到最后元素的个数。当然,声明时是可以指定 end index 的,比如:
foo := []string{
"123", "456", "789", "101112"}
bar := foo[1:3:3] // {"456", "789"}
fmt.Println(len(bar)) // 2
fmt.Println(cap(bar)) // 2
此外,既然切片是引用类型,则通过其对值进行的修改,最终会作用到底层数组上,那么可想而知,其他基于该数组的切片也会受到影响,这意思是:
foo := []string{
"123", "456", "789", "101112"}
bar := foo[1:3] // ["456", "789"]
biu := foo[2:4] // ["789", "101112"]
bar[1] = "七八九"
fmt.Println(foo) // ["123", "456", "七八九", "101112"]
fmt.Println(bar) // ["456", "七八九"]
fmt.Println(biu) // ["七八九", "101112"]
同样的道理,当切片作为参数传递的时候,类似于 c/c++ 中的指针传递(本质上也是一种值传递),所做的修改,将同样作用到底层数组中。
所谓 nil 切片
以及 空切片
即:
var foo []string // nil 切片
foo := []string{
} // 空切片
从声明中可以感受到这二者的区别,虽然二者的长度容量都是 0,但前者指向底层数组的指针为 nil,而后者指向一个值为空的地址。
当切片容量大于长度时进行 append,则修改会直接作用到底层数组上,而当容量等于长度时进行 append,则修改会发生在一个新的底层数组上(一般来说新数组会二倍扩容)。这意思是:
foo := []string{
"123", "456", "789", "101112"}
bar := foo[:] // ["123", "456", "789", "101112"]
bar = append(bar, "131415", "")
bar[5] = "161718"
fmt.Println(bar) // ["123", "456", "789", "101112", "131415", "161718"]
fmt.Println(foo) // ["123", "456", "789", "101112"]
所谓 map
,即广为人知的键值对。在 golang 中,map 为引用类型,其声明可以是:
foo := map[string]int{
"bar": 1024} // 或不提供初值 map[string]int{}
fmt.Println(foo) // map[bar:1024]
也可以:
foo := make(map[string]int, 10) // 或不指定容积 make(map[string]int)
fmt.Println(foo) // map[]
不同于切片,map 的 key 需有意义(而不能零值填充),所以 make 构造时,无法指定长度, make 出的 map length 均为 0。
另外,golang map 中,当 key 不存在时,返回声明类型的零值,你比如:
foo := make(map[string]int, 10)
fmt.Println(foo["bar"]) // 0
然而,当 key 不存在时,不单返回零值,若要判断某 key 是否存在于某 map 中,则可以利用如下 biu
:
foo := map[string]int{
"bar": 1024}
bar, biu := foo["bar"]
fmt.Println(bar, biu) // 1024 true
按照惯例,map 可以通过赋值方式进行新增、修改,而删除则通过内置函数 delete
,你比如:
foo := map[string]int{
"bar": 1024}
delete(foo, "bar")
foo["bar"] = 2048
fmt.Println(foo) // map[bar:2048]
特殊地,当 map 的 value 类型是函数时,可用以实现一个简易的工厂模式;当类型为布尔时,稍加完善,可实现集合的效果。
顾名思义,channel 充当 golang 中的管道,数据在其中流动,其声明如是:
foo := make(chan int)
管道的操作采用可以表示流向的操作符 <-
,你比如:
foo := make(chan int, 1) // 第二个参数为管道容量,0 容量管道的发送、接收操作会被一直阻塞
foo <- 6 // 向管道中发送数据
fmt.Println(<-foo) // 从管道中读一把数据
同样,在声明时也可以指定方向(使之只可发送或者只可接收),声明缺省方向则表示管道为双向管道。你比如:
foo := make(chan<- int, 1) // 只能向其发送
bar := make(<-chan int, 1) // 只能从起接收
管道的容量又称之为缓存大小,当管道中数据小于容量时,向其发送数据不会发生阻塞;当管道中存在数据时,从其中接收数据不会发生阻塞。
关闭管道使用内置函数 close
当管道未被关闭且其中无数据时,表达式 <-foo
会一直阻塞,而若目标管道处于关闭状态,该表达式会返回相应类型的零值。另外,只有当管道中数据全部被接收后,管道才会被真正关闭,在接收时可使用一个额外变量用以检查管道是否已关闭。你比如:
foo := make(chan int, 3)
foo <- 1
foo <- 2
close(foo)
bar, biu := <-foo
fmt.Println(bar, biu) // 1 true
bar, biu = <-foo
fmt.Println(bar, biu) // 2 true
bar, biu = <-foo
fmt.Println(bar, biu) // 0 false
此外,向已关闭的管道发送数据会引起异常,你比如:
foo := make(chan int, 2)
foo <- 1
close(foo)
foo <- 2 // panic: send on closed channel 尽管此时仍可以从管道接收数据
在管道上运用 for … range 语法,会一直迭代到管道被关闭的时候,你比如:
foo := make(chan int, 2)
foo <- 1
foo <- 2
close(foo)
for bar := range foo {
fmt.Println(bar)
} // 1 2
若上述没有 close(foo)
,则程序会阻塞在 for 中,直到天荒地老或者 fatal error: all goroutines are asleep
。
Select 可以理解为管道专用的 switch,其 case 上条件通常为一组管道的接收、发送操作(以及一个 default),你比如:
foo := make(chan int, 1)
for i := 1; i <= 3; i++ {
select {
case foo <- 10:
fmt.Println("->")
case <-foo:
fmt.Println("<-")
default:
fmt.Println("not matched")
}
}// -> <- ->
第一次匹配,管道中为空,故发送操作生效;第二次,管道中有一个 10,故接收生效;接收完管道中再次为空,故第三次发送生效。特殊地,当缺省 default 操作时,若无 case 可以成功执行,则程序会阻塞在 select 语句中,直到有一个 case 可以成功执行。
此外,select 的 case 条件可以是一个 timeout 操作,以此可实现,在若干秒后仍无 case 响应则直接进行某些操作的功能,你比如:
foo := make(chan int, 1)
select {
case <-time.After(time.Second * 5): // time.After 返回一个单向(<-chan Time)的管道,可用于获取指定间隔后的时间
fmt.Println("timeout")
case <-foo:
fmt.Println("<-")
} // 五秒后打印 timeout
timer 与 ticker 是两个特殊的管道,功能上可以分别参考 JavaScript 里的 setTimeout
与 setInterval
。
foo := time.NewTimer(time.Second * 3)
fmt.Println(<-foo.C) // 三秒后打印形如 2020-10-10 09:30:37.222475 +0800 CST m=+3.005188752
timer 以及 ticker 管道的关闭,需采用 Stop
方法,你比如:
foo := time.NewTimer(time.Second * 1)
bar := make(chan string, 1)
go func() {
<-foo.C
bar <- "timeout"
}()
foo.Stop() // 在 1 秒内关闭掉 timer 管道,则执行到 `<-foo.C` 直接返回,bar 中将不会有数据
fmt.Println(<-bar) // 死锁 fatal error: all goroutines are asleep - deadlock!
foo := time.NewTicker(time.Second * 1)
go func() {
for f := range foo.C {
fmt.Println(f)
} // 每隔一秒打印一把时间
}()
time.Sleep(time.Second * 3)
foo.Stop() // 三秒后 ticker 管道关闭,for ... range 因而终止
golang 中,可用函数 new
与 make
进行内存分配,具体如下:
new 函数使用时,返回指向传入类型零值的指针,你比如:
foo := new(int)
fmt.Println(foo, *foo) // 0xc0000b4008 0
而 make,只用于 slice、map、channel 的初始化,使用时可以指定相应结构的长度、容量,在以零值构建完成底层数据后,返回相应的引用,你比如:
foo := make([]int, 10)
fmt.Println(foo) // [0 0 0 0 0 0 0 0 0 0]
你比如:
if true {
fmt.Println("true") // true
}
加上声明以及 else 则形如:
if foo := true; !foo {
fmt.Println("true")
} else {
fmt.Println("foo is true") // foo is true
}
值得注意的是,golang if 中的条件必须是布尔类型。
golang 的 switch 与传统意义上的 switch 相比,显得更加便捷:
foo := 1984
switch foo {
case 1984:
fmt.Println("1984")
default:
fmt.Println("default")
}
foo := 1984
switch foo {
case 1984, 1987:
fmt.Println("1984")
case 1884, 1887:
fmt.Println("1884")
default:
fmt.Println("default")
}
foo := 1984
switch {
case foo == 1984:
fmt.Println("1984") // 1984
case false:
fmt.Println("false")
default:
fmt.Println("default")
}
foo := 1984
switch {
case foo > 1983:
fmt.Println("1984") // 1984
fallthrough
case foo > 1883:
fmt.Println("1884") // 1884,此 case 不贯穿,所以直接跳出
case foo > 1783:
fmt.Println("1784")
default:
fmt.Println("default")
}
for 的写法如下:
for foo := 0; foo < 2; foo++ {
fmt.Println(foo)
} // 0, 1
golang 中没有 while 关键词,但是功能上 for 也可以实现:
foo := 2
for foo > 0 {
fmt.Println(foo)
foo--
} // 2, 1
另外,golang 中死循环写起来也更简洁:
for {
fmt.Println("never end")
}
而当 for 与 range 配合使用的时候,可以对数组、map 等进行遍历,你比如:
foo := [...]int{
1, 2, 3}
for index, value := range foo {
fmt.Println(index, value)
}
基本的函数声明、调用如下:
func foo(a int, b int) int {
return a + b
}
func main() {
fmt.Println(foo(1, 2)) // 3
}
golang 统一编码规范,将左侧大括号放在函数名同一行(而不是另起一行),与任何函数式编程的语言类似,在 golang 中函数是一等公民,函数可作为参数和返回值。
Goroutine 是协程的 go 语言实现,所谓协程,又被称为微线程,即是一种比进程更轻量的存在。goroutine 所需的开销非常小,可以轻松创建出上万个 goroutine,golang 通过 goroutine 实现对并发编程的支持。一般而言,golang 运行库最多会启动 $GOMAXPROCS
个线程来运行 goroutine。
启动一个 goroutine 非常简单,你比如:
foo := func() {
fmt.Println("bar") }
go foo()
goroutine 间通过 channel 进行通信,这也是所谓“管道”的意义。你比如:
foo := make(chan string, 1)
bar := make(chan string, 1)
biu := make(chan string, 1)
go func() {
<-foo
biu <- "sth"
}()
go func() {
<-bar
foo <- "sth"
}()
bar <- "nothing"
fmt.Println(<-biu, len(bar), len(foo)) // sth 0 0
类似于线程的调度,有时某个 goroutine 中的操作可能非常耗时(单纯的耗时,而非通过管道阻塞在原地等待),这就有可能需要主动让出 CPU,从而尽可能地提高全局效率, Goshed
函数便是为此而生:
go func() {
for i := 0; i <= 10000; i++ {
runtime.Gosched()
fmt.Println(i)
}
}()
&^
操作符,通常称为按位清零运算符,意为将左边数中,所有右边数对应为 1 的位清零,你比如 5&^4
其值为 1,即是 101 按 100 进行清零得到的。