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

59 阅读7分钟

Go语言作为一门简洁、高效、安全的编程语言,在后端开发中被广泛应用。今天我将结合代码案例,记录Go语言编程中的一些实践经验。

一、程序结构

Go语言程序的基本结构包括包声明、导入包和程序入口函数:

package main

import "fmt"

func main() {
  fmt.Println("Hello World!") 
}

包声明说明了代码所在的包,导入包引入了外部包。main包中的main函数是程序入口。

二、基础数据类型

Go语言支持多种基础数据类型,包括数值、字符串、布尔等:

  1. 数值类型
var i int = 10 // int整型
var f float32 = 3.14 // float32浮点型 
  1. 字符串
var s string = "Hello"

字符串使用双引号合拢。

  1. 布尔类型
var b bool = true

Go语言使用bool表示布尔类型。

我们还可以使用var关键字进行批量变量声明:

var (
    name string = "Bob"
    age int = 30
    male bool = true
)

三,复合数据类型

Go语言还提供了数组、切片、结构体、map等复合数据类型,可以表示更复杂的数据。现在我们来看一下这些复合类型的使用。

1.数组

数组是同一类型元素的集合,在Go语言中可以这样定义:

var a [3]int // 元素类型和长度
a[0] = 1 
a[1] = 2
a[2] = 3

数组的长度是类型的一部分,因此数组一旦定义就无法更改长度。

2.切片(Slice)

切片是可变长度的序列,比数组更灵活。可以这样定义:

var s []int 
s = append(s, 1) // 追加元素

内置的append可以动态地向切片添加元素。

3.Map

Map是一种无序的键值对集合:

var m map[string]int
m = make(map[string]int)
m["one"] = 1  // 添加键值对

需要使用make初始化才能使用。

4.结构体

结构体可以自定义复合类型:

type User struct {
  Id int
  Name string
  Age int
}

var u User 
u.Id = 1
u.Name = "Bob"
u.Age = 30

结构体是一个自定义的类型,可以包含多种不同类型的字段。

复合数据类型允许我们在Go语言中表示复杂的实体,是后续程序开发的重要组成部分。

四,函数

函数是组织代码的基本方式之一。Go语言中函数的定义格式如下:

func functionName(input1 type1, input2 type2) (output1 type1, output2 type2) {
  // 函数体
  return value1, value2 
}

函数可以有多个输入参数和返回值。例如:

func add(x int, y int) int {
  return x + y
}

func main() {
  result := add(1, 2)
  fmt.Println(result) 
}

add函数有两个int类型输入,返回一个int类型结果。

Go语言中函数也可以作为一等公民使用:

func apply(op func(int, int) int, a int, b int) int {
  return op(a, b)
}

func main() {
  add := func(x, y int) int {
    return x + y
  }
  result := apply(add, 1, 2)
  fmt.Println(result) // 3
}

上面示例中,apply函数接收另一个函数作为参数。

函数在Go语言中使用广泛,复杂的业务逻辑通常拆分为多个函数进行处理。掌握函数的定义和使用是Go语言编程的重要一环。

五,方法

方法是一种特殊的函数,它在一个类型上定义,并且可以访问该类型的字段。在Go语言中,方法的定义格式如下:

func (t Type) methodName(params) returnTypes {
  // 方法体
}

例如,在Person结构体上定义一个SayHello方法:

type Person struct {
  Name string
}

func (p Person) SayHello() {
  fmt.Println("Hello", p.Name)
}

func main() {
  p := Person{"John"}
  p.SayHello()
}

SayHello方法可访问Person类型的Name字段。

方法与函数的区别在于方法与具体类型绑定,而函数则是一个独立的实体。

方法必须在同一个包内定义,而不能为外部类型定义方法。想为外部类型定义方法,需使用接口。

绑定到类型的方法对实现面向对象编程很重要。通过组合类型和方法,Go语言可以实现一些面向对象的编程技巧。

六,接口

接口是一种抽象类型,是方法签名的集合。在Go语言中接口的定义如下:

type InterfaceName interface {
  Method1(params) returnTypes 
  Method2(params) returnTypes
  //...
}

一个类型只要实现了接口中的所有方法,就实现了这个接口。例如:

type Reader interface {
  Read(p []byte) (n int, err error)
}

type File struct {
  //...
}

func (f File) Read(p []byte) (n int, err error) {
  //...
}

func main() {
  var r Reader
  r = File{} 
  r.Read()
}  

File实现了Reader接口的Read方法,所以File类型实现了Reader接口。

接口在Go语言中很重要,它提供了一种非侵入式的扩展已有类型的方式。标准库定义了许多接口,在组合不同类型的代码时就可以通过接口建立联系。

掌握接口的用法可以编写出更灵活、可扩展的Go语言程序。

七,基于共享变量的并发

Go语言内置了强大的并发支持,使用goroutine和channel可以轻松实现并发程序。除此之外,Go语言还可以通过共享变量和同步原语来实现并发。

当goroutine共享某个变量时,需要使用同步原语来 coordinate对变量的访问,确保数据完整性。Go语言中提供了两种同步原语:互斥锁(Mutex)和读写锁(RWMutex)。

使用互斥锁的例子:

var count int 
var mutex sync.Mutex

func increment() {
  mutex.Lock()
  count++
  mutex.Unlock()
}

func decrement() {
  mutex.Lock()
  count--
  mutex.Unlock() 
}

调用increment和decrement的goroutine将互斥访问共享变量count。

RWMutex允许多个读操作并发,但写操作互斥:

var rwMutex sync.RWMutex 

func read() {
  rwMutex.RLock()
  //读数据
  rwMutex.RUnlock()
}

func write() {
  rwMutex.Lock()
  //写数据
  rwMutex.Unlock()
}

通过共享变量和同步原语可以在Go语言中实现复杂的并发访问控制。

八,包和工具

包是Go语言组织和重用代码的方式。我们可以将代码按功能组织在不同的包中,然后通过import导入和使用。

示例:

// file path/to/stringutil/reverse.go

package stringutil

func Reverse(s string) string {
  // 实现反转字符串的函数
}

其他代码可以通过import导入stringutil包:

import "path/to/stringutil"

func main() {
  s := stringutil.Reverse("Hello") 
}

Go语言的标准库也采用包组织,提供了丰富的功能。

Go语言还内置了一些重要的工具:

  • fmt - 格式化IO操作的包
  • testing - 提供测试功能的包
  • net/http - HTTP网络操作包
  • flag - 命令行参数解析包

这些内置的包和工具大大简化了Go语言的开发工作。并且Go语言还有强大的第三方包生态系统。

通过有效利用包,我们可以编写出简洁、模块化的Go语言程序。掌握Go语言的包机制和常用的标准包对Go语言开发很重要。

九,测试

Go语言内置了测试功能,可以方便地对代码进行测试。

测试函数名以Test开头,参数为t *testing.T:

func TestAdd(t *testing.T) {
  total := Add(1, 2)
  if total != 3 {
    t.Errorf("Add(1, 2) failed, got %d", total) 
  }
}

把测试代码放在xxx_test.go文件中,然后通过go test命令运行测试:

$ go test
PASS  
ok      path/to/pkg    0.002s

table-driven测试允许构建一次设置,多次执行不同测试用例:

func TestSplit(t *testing.T) {
  type test struct {
    input string
    sep string
    want []string
  }

  tests := []test{
    {input: "a:b:c", sep: ":", want: []string{"a", "b", "c"}},
    {input: "a,b,c", sep: ",", want: []string{"a", "b", "c"}},
  }

  for _, tc := range tests {
    got := strings.Split(tc.input, tc.sep)
    if !reflect.DeepEqual(got, tc.want) {
      t.Errorf("Split(%q, %q) == %q, want %q", 
        tc.input, tc.sep, got, tc.want)
    }
  }
}

Go语言内置的测试功能可以很好地支持测试驱动开发。编写测试代码是Go语言开发中的重要实践。

十,反射

反射是Go语言的一个重要特性,它允许程序在运行时检查类型和变量,非常灵活和强大。

可以通过reflect包使用反射:

import "reflect"

反射主要包含了以下几个函数和类型:

  • reflect.TypeOf:获取接口值的类型
t := reflect.TypeOf(3) // t is reflect.Type = int
  • reflect.ValueOf:获取接口值的反射值
v := reflect.ValueOf(3) // v is reflect.Value = 3
  • reflect.Kind:表示类型的常量
t := reflect.TypeOf(3)
k := t.Kind() // k = reflect.Int  

reflect.StructField类型提供了对结构体字段的反射能力。

示例:

type User struct {
  Id   int
  Name string
}

u := User{1, "Bob"}
t := reflect.TypeOf(u)

for i := 0; i < t.NumField(); i++ {
  f := t.Field(i)
  fmt.Printf("%v %v\n", f.Name, f.Type)  
}

// output:
// Id int
// Name string

反射虽然有一定性能损耗,但可以大大提高程序的灵活性。需要动态检查类型信息时反射是非常有用的。