Golang 基础语法速通

概览

  • go 属于 c 家族
  • go 不是 oop 语言
  • go 严格统一编码风格
  • go 是静态语言,编译后,生成机器可运行的二进制指令
  • go 原生支持 Unicode,天生采用 utf-8 编码(go 的作者之一 为 utf-8 编码的作者)
  • go 天生支持并发
  • go 自带内存管理、GC 机制,这意味着 go 其实运行在某种意义上的虚拟机上
  • go 没有引用传递(当然,有引用类型)
  • go 中有指针,但不支持指针运算,这避免了一些潜在的 bug(可见于 c/c++ 程序中)
  • go 很快(尤其编译,默认静态编译,所以另一方面会导致编译出的二进制文件体积较大)

数据类型

类型 关键词
布尔型 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 中:

  • 值类型包括 int、float、bool、string、struct 以及数组
  • 引用类型包括指针、切片、map、chan(通道)

再外,可以通过 math.MaxInt64math.MinInt64 的方式得到预定义的某类型最大最小值。

变量

一个平淡无奇的变量声明如下:

var foo string = bar

其中,bar 可以为表达式或者字面量。

另外,stringbar 可以有一者省略,比如:

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"}
make 方式
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 切片与空切片

所谓 nil 切片 以及 空切片 即:

var foo []string // nil 切片 
foo := []string{
     } // 空切片

从声明中可以感受到这二者的区别,虽然二者的长度容量都是 0,但前者指向底层数组的指针为 nil,而后者指向一个值为空的地址。

切片的 append

当切片容量大于长度时进行 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

所谓 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

声明

顾名思义,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

在管道上运用 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

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

timer 与 ticker 是两个特殊的管道,功能上可以分别参考 JavaScript 里的 setTimeoutsetInterval

timer

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!

ticker

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 因而终止

Make 与 New

golang 中,可用函数 newmake 进行内存分配,具体如下:

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

你比如:

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 中的条件必须是布尔类型

Switch

golang 的 switch 与传统意义上的 switch 相比,显得更加便捷:

  • 不必写 break,默认自动跳出
foo := 1984
switch foo {
     
  case 1984:
  	fmt.Println("1984")
  default:
  	fmt.Println("default")
}
  • 每个 case 可以有多个条件,以逗号分割
foo := 1984
switch foo {
     
  case 1984, 1987:
  	fmt.Println("1984")
  case 1884, 1887:
  	fmt.Println("1884")
  default:
  	fmt.Println("default")
}
  • 不限定 case 条件的类型,在 switch 后不接表达式的情况下,可以在每个 case 中分别写表达式,但需要保证表达式其值的类型相同
foo := 1984
switch {
     
  case foo == 1984:
  	fmt.Println("1984") // 1984
  case false:
  	fmt.Println("false")
  default:
  	fmt.Println("default")
}
  • 新增了 fallthrough 关键字,在匹配的 case 中 fallthrough,可以往下继续匹配 case
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 的写法如下:

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

Goroutine 是协程的 go 语言实现,所谓协程,又被称为微线程,即是一种比进程更轻量的存在。goroutine 所需的开销非常小,可以轻松创建出上万个 goroutine,golang 通过 goroutine 实现对并发编程的支持。一般而言,golang 运行库最多会启动 $GOMAXPROCS 个线程来运行 goroutine。

启动一个 goroutine 非常简单,你比如:

foo := func() {
      fmt.Println("bar") }
go foo()

Goroutine 间通信

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

Goshed

类似于线程的调度,有时某个 goroutine 中的操作可能非常耗时(单纯的耗时,而非通过管道阻塞在原地等待),这就有可能需要主动让出 CPU,从而尽可能地提高全局效率, Goshed 函数便是为此而生:

go func() {
     
  for i := 0; i <= 10000; i++ {
     
    runtime.Gosched()
    fmt.Println(i)
  }
}()

others

  • go 中变量的声明必须使用空格隔开
  • go 包名由小写字母组成
  • go 中公有和私有的区别在于第一个字母是否大写(大写则公)
  • go 中没有左自增/减,只有右自增/减
  • go 新增了 &^ 操作符,通常称为按位清零运算符,意为将左边数中,所有右边数对应为 1 的位清零,你比如 5&^4 其值为 1,即是 101 按 100 进行清零得到的。

参考

  • Go语言基础(二)—— 基本常用语法
  • Go Channel 详解
  • Go 语言 make 与 new 的区别
  • Go 语言的核心 Routine-Channel

你可能感兴趣的:(Golang,golang)