Go 语言入门指南:基础语法和常用特性解析 | 青训营

80 阅读19分钟

引子

首先来看一个简单的go程序:

package main

import "fmt"

func main() {
   // 这是一个简单go程序
   fmt.Println("Hello, World!")
}

这段代码中包含了以下基本信息

  1. 第1行代码 package main 包含了package这个关键字,定义了当前这个go文件的归属——一个名称为main的包。在源文件的非注释的第一行,必须指明文件属于哪个包。而main包更特殊一点,因为每个 go 应用程序都包含一个名为 main 的包。
  2. 第3行的 import "fmt"  告诉 go 编译器,该程序需要使用 fmt 包中的函数或其他元素,import关键字负责导入当前go文件需要用到的,隶属于其他包的内容。而fmt 包实现了格式化 IO(输入/输出)的功能。
  3. 第5行的 func main()  是程序开始执行的函数。main 函数是每个可执行程序必须包含的函数,通常是在启动后最先执行的函数(如果有 init() 函数,会先执行 init() 函数)。
  4. 第6行的 / ... / 是注释,程序在执行时会忽略这段注释。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也称为块注释,以 /* 开头,并以 */ 结尾,且不可嵌套使用。多行注释通常用于包的文档描述或注释多行代码片段。
  5. 第7行的 fmt.Println(...)  ,对应上第3行的函数导入使用,可以将字符串输出到控制台,并在末尾自动添加换行符 \n。因为go程序有自己的规范,对于引用但是没有使用到的其他包,在保存文件时会自动删除这些未使用的包的引用语句。
  6. 当标识符以大写字母开头时(包括常量、变量、类型、函数名、结构字段等),该标识符可被外部包的代码访问(客户端程序需要先导入该包),称为导出(类似于面向对象语言中的 public);而以小写字母开头的标识符在包外部是不可见的,但在整个包内部是可见和可用的(类似于面向对象语言中的 protected)。
  7. 文件名和包名没有直接关联,它们不一定要相同。文件夹名和包名没有直接关系,它们不需要保持一致。
  8. 同一文件夹下的文件只能有一个包名,否则会导致编译错误。

基础语法和常用特性

  1. 基础语法:
  • go的函数、变量、常量、自定义类型、包(package)的命名方式遵循以下规则:1)首字符可以是任意的Unicode字符或者下划线;2)剩余字符可以是Unicode字符、下划线、数字;3)字符长度不限

  • go有25个关键字37个保留字

关键字

     break        default      func         interface    select
     case         defer        go           map          struct
     chan         else         goto         package      switch
     const        fallthrough  if           range        type
     continue     for          import       return       var

保留字

     Constants:    true  false  iota  nil
 ​
     Types:    int  int8  int16  int32  int64  
               uint  uint8  uint16  uint32  uint64  uintptr
               float32  float64  complex128  complex64
               bool  byte  rune  string  error
 ​
     Functions:   make  len  cap  new  append  copy  close  delete
                  complex  real  imag
                  panic  recover
  • go变量声明的可见性

1)声明在函数内部,是函数的本地值,类似private;

2)声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect;

3)声明在函数外部且首字母大写是所有包可见的全局值,类似public。

声明主要包括有四种主要方式:

var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)。

Go的程序是保存在多个.go文件中,文件的第一行就是package XXX声明,用来说明该文件属于哪个包(package),package声明下来就是import声明,再下来是类型,变量,常量,函数的声明。

  • 编译

go的编译使用命令 go build , go install;除非仅写一个main函数,否则还是准备好目录结构; GOPATH=工程根目录;其下应创建src,pkg,bin目录,bin目录中用于生成可执行文件,pkg目录中用于生成.a文件; go中的import name,实际是到GOPATH中去寻找name.a, 使用时是该name.a的源码中声明的package名字

  • 内置类型和函数
     bool
     int(32 or 64), int8, int16, int32, int64
     uint(32 or 64), uint8(byte), uint16, uint32, uint64
     float32, float64
     string
     complex64, complex128
     array    -- 固定长度的数组
     slice   -- 序列数组(最常用)
     map     -- 映射
     chan    -- 管道

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。

  1. 数据类型:
  • 基本数据类型包括整型、浮点型、布尔型和字符串类型。
  • 复合数据类型包括数组、切片、字典、结构体和接口等。
  • 变量声明方式 var关键字 变量名 返回值类型,和java一样,不声明的时候有默认值即零值。没有明确初始值的变量声明会被赋予它们的 零值
    • 数值类型为 0
    • 布尔类型为 false
    • 字符串为 ""(空字符串)

而且var 语句可以出现在包或函数级别。

 package main
 ​
 import "fmt"
 ​
 var c, python, java bool
 ​
 ​
 func main() {
     var i int
     fmt.Println(i, c, python, java)
 }
 ​

变量的初始化、变量声明可以包含初始值,每个变量对应一个初始值。如果初始值已存在,则可以省略类型;变量会从初始值中获得类型。

注意变量不可以这样声明 var test1 int, test2 string= 0, "test2",但是这样 var test1, test2= 0, "test2" 可以

  • 短变量声明

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用,因此这个短变量声明可以用在接收函数的返回值,前提是这个函数的调用要写在某个函数的方法体内

 package main
 ​
 import "fmt"
 ​
 func addOrDefault(x, y int) (result int) {
     result = x + y
     add := 9
     result = result + add
     return
 }
 ​
 func main() {
     var i, j int = 1, 2
     k := 3
     c, python, java := true, false, "no!"
 ​
     result := addOrDefault(i, j)
 ​
     fmt.Println(i, j, k, c, python, java, result)
 }
 ​
  • 基本类型

int, uintuintptr 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。 当你需要一个整数值时应使用 int 类型,除非你有特殊的理由使用固定大小或无符号的整数类型。

 bool
 ​
 string
 ​
 int  int8  int16  int32  int64
 uint uint8 uint16 uint32 uint64 uintptr
 ​
 byte // uint8 的别名
 ​
 rune // int32 的别名
     // 表示一个 Unicode 码点
 ​
 float32 float64
 ​
 complex64 complex128
  • 类型推导

在声明一个变量而不指定其类型时(即使用不带类型的 := 语法或 var = 表达式语法),变量的类型由右值推导得出。当右值声明了类型时,新变量的类型与其相同:

 var i int
 j := i // j 也是一个 int

不过当右边包含未指明类型的数值常量时,新变量的类型就可能是 int, float64complex128 了,这取决于常量的精度:

 i := 42           // int
 f := 3.142        // float64
 g := 0.867 + 0.5i // complex128
  1. 常量:
  • 常量的声明与变量类似,只不过是使用 const 关键字。

  • 常量可以是字符、字符串、布尔值或数值。

  • 常量不能用 := 语法声明。

  1. 条件语句:

if语句用于执行条件判断,例如:

        if num > 0 {
            fmt.Println("Positive")
        } else if num < 0 {
            fmt.Println("Negative")
        } else {
            fmt.Println("Zero")
        }
  1. 循环语句:

for循环用于重复执行一段代码,例如:

        for i := 0; i < 5; i++ {
            fmt.Println(i)
        }
  1. 函数:
  • 使用func关键字定义函数,函数定义规范如下,参数类型写在参数的定义之后,函数返回值写在函数定义之后
 package main
 ​
 import "fmt"
 ​
 func add(x int, y int) int {
     return x + y
 }
 ​
 func subtract(x int, y int) int {
     return x - y
 }
 ​
 func main() {
     fmt.Println(add(42, 13))
     fmt.Println(subtract(0,5))
 }
 ​

如果函数中的参数接受类型是一致的,可以省略到只写一个参数类型在最后,而且函数可以有多个返回值

 package main
 ​
 import "fmt"
 ​
 func swap(x, y string) (string, string) {
     return y, x
 }
 ​
 func main() {
     a, b := swap("hello", "world")
     fmt.Println(a, b)
 }
 ​

而且可以把函数需要的返回值提前写到函数的定义上,在函数的处理过程中直接赋值处理,最后返回的时候直接return

 package main
 ​
 import "fmt"
 ​
 func split(sum int) (x, y int) {
     x = sum * 4 / 9
     y = sum - x
     return
 }
 ​
 func getOrdefault(x, y string) (x1, y1 string) {
     x1 = x + "1"
     y1 = x1 +y+ "2"
     return
 }
 ​
 func main() {
     fmt.Println(split(17))
     fmt.Println(getOrdefault("cur x", "cur y"))
 }
 结果
 7 10
 cur x1 cur x1cur y2
 ​
  1. 数组:
  • 声明数组。go 语言数组声明需要指定元素类型及元素个数,语法格式如下:
 var arrayName [size]dataType

其中,arrayName 是数组的名称,size 是数组的大小,dataType 是数组中元素的数据类型

  • 初始化数组。假设要声明一个名为 numbers 的整数数组,其大小为 5,在声明时,数组中的每个元素都会根据其数据类型进行默认初始化,对于整数类型,初始值为 0。
 var numbers [5]int

还可以使用初始化列表来初始化数组的元素:

 var numbers = [5]int{1, 2, 3, 4, 5}

另外,还可以使用 := 简短声明语法来声明和初始化数组:

 numbers := [5]int{1, 2, 3, 4, 5}

注意: 在 Go 语言中,数组的大小是类型的一部分,因此不同大小的数组是不兼容的,也就是说 [5]int[10]int 是不同的类型。

也可以通过字面量在声明数组的同时快速初始化数组:如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:

 var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
 或
 balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

如果设置了数组的长度,我们还可以通过指定下标来初始化元素:

 //  将索引为 1 和 3 的元素初始化
 balance := [5]float32{1:2.0,3:7.0}
  • 访问数组元素。数组元素可以通过索引(位置)来读取。格式为数组名后加中括号,中括号中为索引的值。例如读取数组 balance 第 10 个元素的值。
 var salary float32 = balance[9]
  1. 指针:
  • 变量是一种使用方便的占位符,用于引用计算机内存地址。go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

一个指针变量指向了一个值的内存地址。类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:

 var var_name *var-type

var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。以下是有效的指针声明:

 var ip *int        /* 指向整型*/
 var fp *float32    /* 指向浮点型 */
 package main
 ​
 import "fmt"
 ​
 func main() {
    var a int= 20   /* 声明实际变量 */
    var ip *int        /* 声明指针变量 */
 ​
    ip = &a  /* 指针变量的存储地址 */
 ​
    fmt.Printf("a 变量的地址是: %x\n", &a  )
 ​
    /* 指针变量的存储地址 */
    fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
 ​
    /* 使用指针访问值 */
    fmt.Printf("*ip 变量的值: %d\n", *ip )
 }
  • 空指针。当一个指针被定义后没有分配到任何变量时,它的值为 nil。nil 指针也称为空指针。nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
  1. 结构体:
  • 它类似java中的类的定义。结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:
 type struct_variable_type struct {
    member definition
    member definition
    ...
    member definition
 }

一旦定义了结构体类型,它就能用于变量的声明,语法格式如下:

 variable_name := structure_variable_type {value1, value2...valuen}
 或
 variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
  • 访问成员变量使用 . 注意结构体内的成员变量大小写问题,如果是首字母大写,相当于public,如果是小写,相当于private。而且如果需要对结构体进行JSON序列化,不是首字母大写的字段不会被序列化,如果需要修改JSON中的字段回首字母消息,可以使用struct的tag别名。
 type Person  struct{
      Name  string   `json:"name"`   //标记json名字为name   
      Age    int     `json:"age"`
      Time int64    `json:"-"`        // 标记忽略该字段
 ​
 }
 ​
 func main(){
   person:=Person{"小明",18, time.Now().Unix()}
   if result,err:=json.Marshal(&person);err==nil{
    fmt.Println(string(result))
   }
 }
 // 结果
 {"name":"小明","age":18}
  • 结构体作为函数参数。go当中,结构体也是值传递的形式,即在方法内部修改结构体的内容,不会影响到方法外结构体的变化,除非使用指针作为函数输入参数
  1. 切片:
  • go当中的数组,由于数组的长度也属于它定义的一部分,所以不够灵活,切片类似java中的list,长度是动态的。切片,实际的是获取数组的某一部分,len切片<=cap切片<=len数组,切片由三部分组成:指向底层数组的指针、len、cap。

注意:slice 的底层是数组指针,所以 slice a 和 s 指向的是同一个底层数组,所以当修改 s[0] 时,a 也会被修改。

可以声明一个未指定大小的数组来定义切片,且切片不需要指明长度:

 var identifier []type

或使用 make() 函数来创建切片:

 var slice1 []type = make([]type, len)
 ​
 也可以简写为
 ​
 slice1 := make([]type, len)
  • 切片的初始化和数组差不多,不需要填写切片的长度,也可以从数组中直接切片出来
 s := arr[:] 

初始化切片 s,是数组 arr 的引用。

 s :=make([]int,len,cap) 

通过内置函数 make() 初始化切片s[]int 标识为其元素类型为 int 的切片。

一个切片在未初始化之前默认为 nil,长度为 0

切片的区间限制也是左闭右开的

  1. 接口:
  • go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。接口可以让我们将不同的类型绑定到一组公共的方法上,从而实现多态和灵活的设计。
  • Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态。而且这种规则体现了go接口的非侵入式特性,即只要实现了接口规定的方法即可,不需要显式地实现接口,这样即便这个接口的定义后续删除了,也不会影响程序的正常使用,因为它没有显式地像java一样使用 implements关键字 关联起来
 /* 定义接口 */
 type interface_name interface {
    method_name1 [return_type]
    method_name2 [return_type]
    method_name3 [return_type]
    ...
    method_namen [return_type]
 }
 ​
 /* 定义结构体 */
 type struct_name struct {
    /* variables */
 }
 ​
 /* 实现接口方法 */
 func (struct_name_variable struct_name) method_name1() [return_type] {
    /* 方法实现 */
 }
 ...
 func (struct_name_variable struct_name) method_namen() [return_type] {
    /* 方法实现*/
 }

注意它和java完全是相反的,因为go的方法将返回值和返回类型定义在方法签名的最后,所以要体现结构体实现了某个接口的所有方法,必须要把这个接口体的相关内容,写在方法名称之前,代表是哪个结构体实现的接口方法。而java是实现接口通过implements关键字写在方法名之后

 package main
 ​
 import (
     "fmt"
 )
 ​
 type Phone interface {
     call()
 }
 ​
 type NokiaPhone struct {
 }
 ​
 func (nokiaPhone NokiaPhone) call() {
     fmt.Println("I am Nokia, I can call you!")
 }
 ​
 type IPhone struct {
 }
 ​
 func (iPhone IPhone) call() {
     fmt.Println("I am iPhone, I can call you!")
 }
 ​
 func main() {
     // 注意这里可以定义一个接口类型的 变量phone,根据不同的实现类(nokia iphone)实现多态
     var phone Phone
 ​
     phone = new(NokiaPhone)
     phone.call()
 ​
     phone = new(IPhone)
     phone.call()
 ​
 }
 ​
 // 结果
 I am Nokia, I can call you!
 I am iPhone, I can call you!
 ​
  1. 错误处理:
  • go 语言通过内置的错误接口提供了非常简单的错误处理机制。error 类型是一个接口类型,这是它的定义:
 type error interface {
     Error() string
 }

我们可以在编码中通过实现 error 接口类型来生成错误信息。函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个错误信息:

 func Sqrt(f float64) (float64, error) {
     if f < 0 {
         return 0, errors.New("math: square root of negative number")
     }
     // 实现
 }
  1. 并发:
  • go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可.goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。goroutine 语法格式:
 go 函数名( 参数列表 )

例如:

 go f(x, y, z)
  • go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
 package main
 ​
 import (
         "fmt"
         "time"
 )
 ​
 func say(s string) {
         for i := 0; i < 5; i++ {
                 time.Sleep(100 * time.Millisecond)
                 fmt.Println(s)
         }
 }
 ​
 func main() {
         go say("world")
         say("hello")
 }
  • 通道(channel)是用来传递数据的一个数据结构。通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。goroutine 是 golang 中在语言级别实现的轻量级线程,仅仅利用 go 就能立刻起一个新线程。多线程会引入线程之间的同步问题,在 golang 中可以使用 channel 作为同步的工具。
  • 通过 channel 可以实现两个 goroutine 之间的通信。创建一个 channel, make(chan TYPE {, NUM}) TYPE 指的是 channel 中传输的数据类型,第二个参数是可选的,指的是 channel 的容量大小。向 channel 传入数据, CHAN <- DATA , CHAN 指的是目的 channel 即收集数据的一方, DATA 则是要传的数据。从 channel 读取数据, DATA := <-CHAN ,和向 channel 传入数据相反,在数据输送箭头的右侧的是 channel,形象地展现了数据从隧道流出到变量里。
 ch <- v    // 把 v 发送到通道 ch
 v := <-ch  // 从 ch 接收数据
            // 并把值赋给 v

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:

 ch := make(chan int)

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

 package main
 ​
 import "fmt"
 ​
 func sum(s []int, c chan int) {
         sum := 0
         for _, v := range s {
                 sum += v
         }
         c <- sum // 把 sum 发送到通道 c
 }
 ​
 func main() {
         s := []int{7, 2, 8, -9, 4, 0}
 ​
         c := make(chan int)
         go sum(s[:len(s)/2], c)
         go sum(s[len(s)/2:], c)
         x, y := <-c, <-c // 从通道 c 中接收
 ​
         fmt.Println(x, y, x+y)
 }

因此通道可以通过 make 的第二个参数指定缓冲区大小:

 ch := make(chan int, 100)

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

与Java的对比

从第二部分的一些关键特性中,也能看出Java和go还是有所区别的。这里简单总结对比一下,仅代表个人看法。

特性Go语言Java
语法静态类型、C风格语法、丰富的内置数据类型、自动垃圾回收静态类型、C++风格语法、丰富的类库、自动垃圾回收
并发编程通过goroutine和通道实现轻量级并发通过线程和锁实现并发
错误处理使用error类型返回错误信息,推崇返回多值方式使用Exception处理异常
包管理使用go mod进行依赖管理,支持版本管理使用Maven或Gradle进行依赖管理,支持版本管理
面向对象支持面向对象编程,但没有传统的类和继承的概念强支持面向对象编程,支持类和接口的继承关系
平台支持移植性强,可编译成可执行文件,适用于各种平台需要Java虚拟机(JVM)来运行,适用于各种平台
性能原生编译,性能较高通过JVM运行字节码,性能相对较低
社区生态较小的社区规模,但发展迅速大规模的开发者社区和丰富的第三方类库
开发效率简洁的语法,快速编译和部署复杂的语法和繁多的类库,编译和部署相对较慢
平台特性适用于系统和网络编程,如服务器后端开发适用于桌面应用程序和大规模企业级应用开发