第六届字节跳动青训营实践第二篇 | 青训营

63 阅读7分钟

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

特点

Go 语言的语法简洁到只有几个关键字,便于记忆。Go 语言的编译器速度非常快,有时甚至会让人感觉不到在编译。所以,Go 开发者能显著减少等待项目构建的时间。因为Go 语言内置并发机制,所以不用被迫使用特定的线程库,就能让软件扩展,使用更多的资源。Go 语言的类型系统简单且高效,不需要为面向对象开发付出额外的心智,让开发者能专注于代码复用。Go 语言还自带垃圾回收器,不需要用户自己管理内存。

特性:

(1)开发速度:Go 语言使用了更加智能的编译器,并简化了解决依赖的算法,最终提供了更快的编译速度。编译Go 程序时,编译器只会关注那些直接被引用的库,而不是像Java、C 和C++那样,要遍历依赖链中所有依赖的库。

(2)并发:goroutine 很像线程,但是它占用的内存远少于线程,使用它需要的代码更少。通道(channel)是一种内置的数据结构,可以让用户在不同的goroutine 之间同步发送具有类型的消息。这让编程模型更倾向于在goroutine之间发送消息,而不是让多个goroutine 争夺同一个数据的使用权。

举例说明: 假设我们有一个任务,需要同时打印数字和字母,模拟两个不同的任务并发执行。代码如下:

package main
import (
    "fmt"
    "time"
)
func printNumbers() {
    for i := 1; i <= 5; i++ {
        fmt.Println("Number:", i)
        time.Sleep(500 * time.Millisecond)
    }
}
func printLetters() {
    letters := []string{"A", "B", "C", "D", "E"}
    for _, letter := range letters {
        fmt.Println("Letter:", letter)
        time.Sleep(700 * time.Millisecond)
    }
}
func main() {
    go printNumbers() // 启动一个 goroutine 来执行 printNumbers
    go printLetters() // 启动另一个 goroutine 来执行 printLetters
    time.Sleep(3500 * time.Millisecond) // 等待一段时间,以确保 goroutine 有足够的时间来执行
}

这个例子展示了 Go 语言中的两个核心概念:goroutine 和通道。在上面的代码中,printNumbers() 和 printLetters() 函数分别在自己的 goroutine 中执行,允许它们并发地运行。我们没有使用通道,而是使用了 time.Sleep 来等待一段时间。

基础语法:

(1)变量与数据类型:

GO 是静态类型语言,变量在使用之前必须先声明,并且需要指定变量的数据类型。举例如下:

package main
import "fmt"
func main() {
    // 声明一个整数变量
    var num int
    num = 42
    fmt.Println("Number:", num)
    // 声明并初始化一个字符串变量
    var name string = "Alice"
    fmt.Println("Name:", name)
    // 使用 := 语法来声明并初始化变量
    age := 30
    fmt.Println("Age:", age)
}

在赋值给多个变量时,如果存在不需要接收值的变量,可以使用匿名变量来代替。举例来说:

package main
import "fmt"
func ReturnData() (int, int) {
  return 10,20
 }
func main() {
  a,_ := ReturnData()
  _,b := ReturnData()
  fmt.Println(a, b)
}

ReturnData()是我们自定义的一个函数,对于它的每次调用,会返回10和20两个整型数值。

调用ReturnData()函数,使用变量a接收第一个返回值,第二个返回值由匿名变量接收。调用ReturnData()函数,使用变量b接收第二个返回值,第一个返回值由匿名变量接收。由于匿名变量不占用命名空间,也不会分配系统内存,匿名变量与匿名变量之间不会因为多次声明而无法使用。

数据类型很多与其他编程语言类似,不过多赘述。下面举两个实例:

结构体类型:结构体用于定义自定义的复合数据类型。

package main
import "fmt"
type Person struct {
    FirstName string
    LastName  string
    Age       int
}
func main() {
    person := Person{
        FirstName: "Alice",
        LastName"Smith",
        Age:       30,
    }
    fmt.Println("Person:", person)
}

接口类型:接口定义了一组方法的集合,用于实现多态性。

package main
import "fmt"
type Shape interface {
    Area() float64
}
type Circle struct {
    Radius float64
}
func (c Circle) Area() float64 {
    return 3.14 * c.Radius * c.Radius
}
 
func main() {
    circle := Circle{Radius: 5}
    var shape Shape = circle
    fmt.Println("Area:", shape.Area())
}

我们首先定义了一个接口类型 Shape,其中包含一个方法 Area(),该方法返回一个 float64 类型的值。接口是一种抽象的类型,它定义了一组方法的规范,而不关心具体的实现。接下来,我们定义了一个结构体 Circle,它具有一个名为 Radius 的字段,表示圆的半径。在结构体 Circle 上定义了一个 Area() 方法,该方法计算并返回圆的面积。注意,我们通过接收者 (c Circle) 来将方法绑定到结构体类型上。在 main 函数中,我们创建了一个 Circle 类型的变量 circle,并初始化其半径为 5。然后,我们声明一个接口类型变量 shape,将 circle 赋值给它。这是因为 Circle 类型实现了 Shape 接口中的 Area() 方法,所以 Circle 类型可以被赋值给 Shape 类型变量。最后,我们通过 shape.Area() 调用 Area() 方法来计算圆的面积,并使用 fmt.Println 输出结果。

指针:

在 Go 语言中,指针是一种非常重要的概念,用于处理变量在内存中的地址。通过使用指针,你可以直接操作变量的内存地址,而不是复制变量的值。举例说明:

package main
import " fmt "
func main( ) {
  num := 42
  fmt.Println( "Value of num:" ,num )
  fmt . Println("Address of num:" ,&num )
  //定义一个指针变量
  var ptr * int
  ptr = &num
  fmt.Println( "Value at ptr:" ,★ ptr)
  //修改指针指向的值会影响原始变量
  *ptr = 100
  fmt.Println( "Value of num after modification:", num)
}

num := 42:我们定义一个整数变量 num,并赋值为 42。

fmt.Println("Value of num:", num):输出变量 num 的值。

fmt.Println("Address of num:", &num):输出变量 num 的内存地址,使用 & 运算符来获取。

var ptr *int:我们声明一个指针变量 ptr,它可以指向一个整数值,类型为 *int,表示指向整数的指针。

ptr = &num:将变量 num 的地址赋值给指针变量 ptr。

fmt.Println("Value at ptr:", *ptr):输出指针变量 ptr 指向的值,使用 * 运算符来获取指针指向位置的值。

*ptr = 100:通过指针修改变量的值。因为 ptr 指向了 num 的内存地址,所以修改 *ptr 的值也会影响 num 的值。

fmt.Println("Value of num after modification:", num):输出修改后的 num 的值,可以看到它已经变成了 100。

(2)常量和运算符

12.jpg

常量的声明以关键字const开头,后接变量类型并进行赋值,行尾没有其他标点符号。

常量的显式声明格式如下:const 常量名 常量类型 = value

显式常量定义是一种逐个定义常量的方式。每个常量都可以在其名称后面赋予一个特定的值。

package main
import " fmt "
funcmain(){
  const pi float64 = 3.14159
  const daysInWeek int = 7
  fmt.Println("Pi:",pi)
  fmt.Println( "Days in a week: ", daysInWeek )
}

在这个示例中,我们使用 const 关键字定义了两个常量 pi 和 daysInWeek。每个常量都有其特定的数据类型和值。

隐式常量组定义允许我们一次性定义多个相关常量,它们的值可以相同,也可以不同。

package main
import ”fmt"
  func main( ) {
    const (
      apple = "Apple"
      orange = "Orange ”
      banana =” Banana"
      )
  fmt. Println( "Fruits:", apple, orange, banana)
}

在这个示例中,我们使用 const 关键字定义了一个常量组,其中包含了三个相关的水果常量:apple、orange 和 banana。因为它们在同一个常量组中,所以可以一起定义,而且类型和值都可以省略,由编译器推断出来。

无论是显式常量定义还是隐式常量组定义,常量在定义后不能被修改,它们是固定的值。常量在编程中常用于存储不会发生改变的值,比如数学常数、枚举值等。