GO语言简单入门学习

2021/4/17 10:55:23

本文主要是介绍GO语言简单入门学习,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录
  • 包package
    • 导入
    • 导出
  • 函数
    • 简写参数类型
    • 命名返回值(传出参数)
  • 变量
    • 定义
    • 变量初始化
    • 短变量声明
    • 默认零值
    • 类型转换
  • for循环
    • for 是 Go 中的 “while”
  • if
  • switch
    • 没有条件的 switch
  • defer
  • 指针
  • 结构体
  • 数组
  • 切片
    • 先创建数组再切片
    • 直接创建切片
    • 切片的长度与容量
    • nil 切片
    • 用 make 创建切片(动态数组)
    • 切片的内部结构
    • 切片的生长(copy and append 函数)
      • copy
      • append
  • Range
  • 映射map
  • 函数闭包
  • 方法
    • 指针接收者
    • 指针与函数
    • 选择值还是指针作为接收者
  • Go程
  • 信道
    • 带缓冲的信道
    • 信道的range 和 close

包package

每个Go程序都是由包构成的, 程序从main包开始运行, 按照约定, 报名与导入路径的最后一个元素一致, 例如"match/rand"包中的源码均以package rand语句开始

导入

单行导入

import "fmt"
import "match"

括号分组导入(注意没有逗号)

import (
	"fmt"
	"match"
)

导出

在Go中, 如果一个名字以大写字母开头, 那么他就是已导出的, 例如, PizzaPi都是导出名, 导出自math

同理, 如果想让包中的函数等对象能够导出并被其他包调用, 那么这个对象必须要以大写字母开头. 否则会报错

函数

通过关键字func定义

简写参数类型

当连续两个或多个函数的已命名形参类型相同时, 除最后一个类型以外, 其他都可以省略, 传入参数和传出参数都适用

package main
import "fmt"
func add(x, y int) int {
    return x + y
}

func main(){
    fmt.Println(add(42.13))
}

命名返回值(传出参数)

Go的返回值可以被命名, 它们会被视作定义在函数顶部的返回参数的变量.

return语句后面没有跟上参数, 那么就会返回函数顶部定义的参数

package main
import "fmt"
func split(sum int)(x, y int){
	x = sum * 4 / 9
	y = sum -x
	return
}
func main(){
	fmt.Println(split(17))
}

变量

定义

通过关键字var定义, 跟函数的参数列表一样, 类型在最后

package main
import "fmt"
func main() {
	//定义三个bool类型的变量
	var c, python, java bool
	fmt.Println(c, python, java)
}
//结果
false false false

变量初始化

变量声明可以包含初始值, 每个变量对应一个, 如果定义了初始值, 那么可以省略类型

package main
import "fmt"
var i, j int = 1, 2
func main() {
	var c, python, java = true, false, "no!"
	fmt.Println(i, j, c, python, java)
}

短变量声明

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

函数外的每个语句都必须以关键字开始(var, func等等), 因此:=结构不能在函数外使用

syntax error: non-declaration statement outside function body

默认零值

没有明确初始值的变量声明会被赋予它们的零值

数值类型为0, 布尔类型为false, 字符串为""(空字符串)

package main
import "fmt"
func main() {
	var i int
	var f float64
	var b bool
	var s string
	fmt.Printf("%v %v %v %q", i, f, b, s)
}
//结果
0 0 false ""

类型转换

Go在不同类型的项之间赋值时需要显示转换

for循环

Go只有一种循环结构, for循环, 基本的for循环有三部分组成, 他们用分号隔开:

  • 初始化语句(可选/可省略): 在第一次迭代前执行
  • 条件表达式: 在每次迭代前求值
  • 后置语句(可选/可省略): 在每次迭代的结尾执行

注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。

package main
import "fmt"

func main() {
    //完整
	for i := 0; i < 5; i++ {
		fmt.Println(i)
	}
    //省略
    sum := 1
    for ; sum < 10 ; {
        sum += sum
    }
    fmt.Println(sum)
}

for 是 Go 中的 “while”

package main
import "fmt"

func main() {
    //用for表示C语言的while
    sum := 1
    for sum < 10 {
        sum += sum
    }
    fmt.Println(sum)
    //无限循环
    for {
        
    }
}

if

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

for 一样, if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内。

package main
import "fmt"
func main() {
	if i := 1; i < 10 {
		fmt.Printf("%v 小于 10", i)
	} else {
		fmt.Printf("%v 大于 10", i)
	}
}

switch

switch 是编写一连串 if - else 语句的简便方法。它运行第一个值等于条件表达式的 case 语句。

Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。

package main
import "fmt"
func main() {
	switch o := runtime.GOOS; o {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		fmt.Printf("%s.\n", o)
	}
}

没有条件的 switch

没有条件的 switch 同 switch true 一样。这种形式能将一长串 if-then-else 写得更加清晰。

package main
import "fmt"
func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("上午")
	case t.Hour() < 17:
		fmt.Println("下午")
	default:
		fmt.Println("晚上")
	}
}

defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

package main
import "fmt"
func main() {
	for i := 0; i < 5; i++ {
		defer fmt.Println(i)
	}
	fmt.Println("end")
}
//结果(先进后出)
end
4
3
2
1
0

指针

Go 拥有指针。指针保存了值的内存地址。

类型 *T 是指向 T 类型值的指针。其零值为 nil

var p *int

& 操作符会生成一个指向其操作数的指针。

i := 42
p = &i

* 操作符表示指针指向的底层值。

fmt.Println(*p) // 通过指针 p 读取 i
*p = 21         // 通过指针 p 设置 i

这也就是通常所说的“间接引用”或“重定向”。

package main
import "fmt"
func main() {
	i, j := 12, 30
    //&获取i的指针
	p := &i
    //*获取指针的值
	fmt.Printf("i的指针值为:%v\n", *p)
    //*修改指针指向的值
	*p = 24
    //指针指向的值修改后, i的值也随之改变
	fmt.Printf("i 重定向为:%v\n", i)
    //&获取j指针
	p = &j
    //*修改和获取指针指向的值, j的值也随之改变
	*p += *p
	fmt.Printf("j 重定向为:%v\n", j)
}
//结果
i的指针值为:12
i 重定向为:24
j 重定向为:60

结构体

一个结构体(struct)就是一组字段(field)。

结构体字段使用点号来访问

结构体字段可以通过结构体指针来访问。

如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。

使用 Name: 语法可以仅列出部分字段。(与字段名的顺序无关)

特殊的前缀 & 返回一个指向结构体的指针。

package main
import "fmt"
func main() {
	// 创建一个 Vertex 类型的结构体
	v1 := Vertex{1, 2}
	// Y:0 被隐式地赋予
	v2 := Vertex{X: 1}
	// X:0 Y:0
	v3 := Vertex{}
	// 创建一个 *Vertex 类型的结构体(指针)
	p := &Vertex{3, 4}
	fmt.Println(v1, v2, v3, p)
	fmt.Println(v1.X)
	fmt.Println(p.Y)
}

数组

类型 [n]T 表示拥有 nT 类型的值的数组。

表达式 var a [10]int会将变量 a 声明为拥有 10 个整数的数组。

package main
import "fmt"
func main() {
	var a [2]string
	a[0] = "hello"
	a[1] = "world"
	fmt.Println(a)
    
    primes := [6]int{1, 2, 3, 4, 5, 6}
	fmt.Println(primes)
}
//结果
[hello world]
[1 2 3 4 5 6]

切片

先创建数组再切片

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

类型 []T 表示一个元素类型为 T 的切片。切片和python中的切片类似, 通过两个下标来界定, 下标区间左闭右开

切片并不存储任何数据,它只是描述了底层数组中的一段。更改切片的元素会修改其底层数组中对应的元素。与它共享底层数组的切片都会观测到这些修改。

package main
import "fmt"
func main() {
	//创建一个数组
	primes := [6]int{1, 2, 3, 4, 5, 6}
	fmt.Println(primes)
	//创建一个数组的切片
	a := primes[:3]
	fmt.Println(a)
	//修改切片的值
	a[2] = 0
	//数组对应的值也会被修改
	fmt.Println(primes)
}
//结果
[1 2 3 4 5 6]
[1 2 3]
[1 2 0 4 5 6]

直接创建切片

[3]bool{true, true, false}

下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:

[]bool{true, true, false}

切片的长度与容量

切片拥有 长度容量

切片的长度就是它所包含的元素个数。

切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。

切片 s 的长度和容量可通过表达式 len(s)cap(s) 来获取。

package main

import "fmt"

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	printSlice(s)
	// 截取切片使其长度为 0
	s = s[:0]
	// 此时s的类型为一个切片,值为空 [], 但是容量还是6
	// 容量是从切片的起始下标往后开始算的, 这里起始下标为空, 那么切片的容量还是从第一位开始算
	printSlice(s)
	// 拓展其长度, 在此之前s的长度为0,但是容量还是6,切片是根据容量来切的,不是长度,因此可以继续进行切片
	s = s[1:4]
	// 此时s值为[3 5 7], 长度为3, 容量为5, 因为s[1:4]是从下标1开始切的, 所以把下标0的位置给切掉了.
	printSlice(s)
	// 舍弃前两个值
	s = s[2:]
	// 从下标2开始切, 所以就把3和5给切掉了, 剩余容量为3
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
//
len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [3 5 7]
len=2 cap=4 [7]

nil 切片

切片的零值是 nil

nil 切片的长度和容量为 0 且没有底层数组。

用 make 创建切片(动态数组)

切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。

make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:

a := make([]int, 5)  // len(a)=5

要指定它的容量,需向 make 传入第三个参数:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

切片的内部结构

一个切片是一个数组片段的描述。它包含了指向数组的指针,片段的长度, 和容量(片段的最大长度)。

img

前面使用 make([]byte, 5) 创建的切片变量 s 的结构如下:

img

长度是切片引用的元素数目。容量是底层数组的元素数目(从切片指针开始)

我们继续对 s 进行切片,观察切片的数据结构和它引用的底层数组:

s = s[2:4]

img

切片操作并不复制切片指向的元素。它创建一个新的切片并复用原来切片的底层数组。 这使得切片操作和数组索引一样高效。因此,通过一个新切片修改元素会影响到原始切片的对应元素。

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

前面创建的切片 s 长度小于它的容量。我们可以增长切片的长度为它的容量:

s = s[:cap(s)]

img

切片增长不能超出其容量。增长超出切片容量将会导致运行时异常,就像切片或数组的索引超 出范围引起异常一样。同样,不能使用小于零的索引去访问切片之前的元素。

切片的生长(copy and append 函数)

copy

要增加切片的容量必须创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。 整个技术是一些支持动态数组语言的常见实现。下面的例子将切片 s 容量翻倍,先创建一个2倍 容量的新切片 t ,复制 s 的元素到 t ,然后将 t 赋值给 s

t := make([]byte, len(s), (cap(s)+1)*2) // +1 为了防止 cap(s) == 0
for i := range s {
        t[i] = s[i]
}
s = t

循环中复制的操作可以由 copy 内置函数替代。copy 函数将源切片的元素复制到目的切片。 它返回复制元素的数目。

func copy(dst, src []T) int

copy 函数支持不同长度的切片之间的复制(它只复制较短切片的长度个元素)。 此外, copy 函数可以正确处理源和目的切片有重叠的情况。

使用 copy 函数,我们可以简化上面的代码片段:

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t
append

一个常见的操作是将数据追加到切片的尾部。下面的函数将元素追加到切片尾部, 必要的话会增加切片的容量,最后返回更新的切片.

Go提供了一个内置函数 append , 用于大多数场合;它的函数签名:

func append(s []T, x ...T) []T

append 函数将 x 追加到切片 s 的末尾,并且在必要的时候增加容量。

a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

如果是要将一个切片追加到另一个切片尾部,需要使用 ... 语法将第2个参数展开为参数列表。

a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // equivalent to "append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

Range

for 循环的 range 形式可遍历切片或映射。

当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	for i, val := range s {
		fmt.Println(i, val)
	}
}
//结果
0 2
1 3
2 5
3 7
4 11
5 13

可以将下标或值赋予 _ 来忽略它。

for i, _ := range s{
}

映射map

映射将键映射到值。

映射的零值为 nilnil 映射既没有键,也不能添加键。

make 函数会返回给定类型的映射,并将其初始化备用。

package main
import "fmt"
type Vertex struct {
	X, Y int
}
func main() {
	m := make(map[string]Vertex)
	m["first"] = Vertex{1,2}
	fmt.Println(m["first"])
}
//结果
{1 2}

删除元素:

delete(m, key)

通过双赋值检测某个键是否存在:

elem, ok := m[key]

keym 中,oktrue ;否则,okfalse

key 不在映射中,那么 elem 是该映射元素类型的零值。

同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。

函数闭包

和python一样, Go中的函数也是一等对象, 即函数也可以作为函数的参数和返回值, 因此也具有闭包特性

方法

Go 没有类。不过你可以为结构体类型定义方法。

方法就是一类带特殊的 接收者 参数的函数。

方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。

接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。

package main
import (
	"fmt"
	"math"
)
//定义struct
type Vertex struct {
	X, Y float64
}
//定义方法
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
//定义主函数
func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

指针接收者

你可以为指针接收者声明方法。

这意味着对于某类型 T,接收者的类型可以用 *T 的文法。(此外,T 不能是像 *int 这样的指针。)

例如,这里为 *Vertex 定义了 Scale 方法。

指针接收者的方法可以修改接收者指向的值(就像 Scale 在这做的)。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。

试着移除第 16 行 Scale 函数声明中的 *,观察此程序的行为如何变化。

若使用值接收者,那么 Scale 方法会对原始 Vertex 值的副本进行操作。(对于函数的其它参数也是如此。)Scale 方法必须用指针接受者来更改 main 函数中声明的 Vertex 的值。

package main
import (
	"fmt"
	"math"
)
type Vertex struct {
	X, Y float64
}
//值接收者
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
//指针接收者
func (v *Vertex) Scale(f float64){
	v.X = v.X * f
	v.Y = v.Y * f
}
func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}
//结果
50

指针与函数

现在把上面的 AbsScale 方法重写为函数。

package main
import (
	"fmt"
	"math"
)
func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
//第一个参数为指针类型
func Scale(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}
func main() {
	v := Vertex{3, 4}
	fmt.Println(Abs(v))
    //传入的第一个参数需要传入v的指针, 用&v获取
	Scale(&v, 10)
	fmt.Println(Abs(v))
}
//结果
5
50

比较前两个程序,你大概会注意到带指针参数的函数必须接受一个指针:

var v Vertex
ScaleFunc(v, 5)  // 编译错误!
ScaleFunc(&v, 5) // OK

而以指针为接收者的方法被调用时,接收者既能为值又能为指针:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

对于语句 v.Scale(5),即便 v 是个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)

同样的事情也发生在相反的方向。

接受一个值作为参数的函数必须接受一个指定类型的值:

var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // 编译错误!

而以值为接收者的方法被调用时,接收者既能为值又能为指针:

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK

选择值还是指针作为接收者

使用指针接收者的原因有二:

首先,方法能够修改其接收者指向的值。

其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。

在本例中,ScaleAbs 接收者的类型为 *Vertex,即便 Abs 并不需要修改其接收者。

package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := &Vertex{3, 4}
	fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
	v.Scale(5)
	fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

Go程

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。

go f(x, y, z)

会启动一个新的 Go 程并执行

f(x, y, z)

f, x, yz 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。

信道

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。

ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。

(“箭头”就是数据流的方向。)

和映射与切片一样,信道在使用前必须创建:

ch := make(chan int)

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。

package main

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- 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)
}
//结果
3 12 15

带缓冲的信道

信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:

ch := make(chan int, 100)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	//ch <- 3
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	ch <- 3
	fmt.Println(<-ch)
}
//结果
1
2
3

信道的range 和 close

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

v, ok := <-ch

此时 ok 会被设置为 false

循环 for i := range c 会不断从信道接收值,直到它被关闭。

注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

还要注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

func finonacci(n int, c chan int) {
	a, b := 0, 1
	for i := 0; i < n; i++ {
		c <- a
		a, b = b, a+b
	}
	close(c)
}
func main() {
	c := make(chan int, 10)
	go finonacci(cap(c), c)
	for i := range c {
		fmt.Print(i, " ")
	}
}
//结果
0 1 1 2 3 5 8 13 21 34


这篇关于GO语言简单入门学习的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程