Go语言程序设计(1)--基本语法

时间:2021-02-28 07:36:26

第一个程序

package main
import "fmt"
func main() {
fmt.Printf("Hello world")
}

通过阅读这个程序,解释几点:

  • 首行的package main 是必须的。所有的go文件以package something 开头,对于独立运行的执行文件必须是package main

编译运行

构建Go程序的最佳途径是使用Go工具,

% go build helloworld.go
% ./helloworld

变量、类型和关键字

在go中,声明和赋值是两个过程,但是可以连在一起。

声明和赋值分开:

var a int
var b bool
a = 15
b = false

声明和赋值连在一起:

a := 15
b := false

连在一起,Go将会进行类型推导。多个var声明,可以使用成组,注意圆括号的使用

var (
x int
b bool
)

多个同类型的变量可以在一行进行声明: var x, y int 让x, y都是 int类型变量。

a, b  := 20, 16

bool类型

定义的true和false代表bool值。

数字类型

Go都知道的类型是int,这个类型根据你的硬件来决定32位,还是64位,你可以使用int32,uint32来明确长度。完整的整数类型包含int8,uint8, int16, int32和 byte,uint8, uint16, uint32, uint64。注意到这些类型全部都是独立的,并且混用这些类型变量赋值,会引起编译器错误:

package main
func main() {
var b int
var b int32
a = 15
b = a + a
b = b + 5
}

常量

常量在Go中,也就是constant。它们在编译时创建,只能是数字,字符串或布尔值;const x = 42 生成x这个常量。可以使用iota生成枚举值。

const (
a = iota
b = iota
)

第一个iota表示为0,因此,a为0。当iota再次在新的一行使用的时候,它的值增加了1,因此b的值为1。如果需要,可以明确指定常量的类型:

const (
a = 0
b string = "0"
)

字符串

另一个重要的内建类型是string。赋值字符串的例子:

s :=  "hello world!"

字符串在Go中是UTF-8的由双引号包裹的字符序列。如果使用单引号,则表示一个字符(UTF-8)这种在Go中不是string。一旦变量赋值,字符串就不能修改:在Go中字符串是不可变的。

var s  string = "hello"
s[0] = 'c' //错误

在Go中实现这个,需要下面的方法:

s := "hello"
c := []rune(s)
c[0] = 'c'
s2 := string(c)
fmt.Printf("%s\n", s2)

转换s为rune数组。

rune

Rune是int32的别名,用UTF-8进行编码。这个类型在什么时候使用呢?例如需要变量遍历字符串中的字符。

复数

Go原生支持复数。它的变量类型是complex128(64位虚数部分)。

错误

任何足够大的程序或多或少都会需要使用到错误报告。因此Go有为了错误而存在的内建类型,叫做error。

var e error

自定义函数

函数中可以有任意的参数,如果没有参数,那么只有圆括号。否则要写成这样: params1 type1,...,paramN typeN ,param1是参数,type1是参数类型。多个参数用逗号分开。函数的返回值可以是任意个,如果没有,那么右括号后面直接跟左大括号。如果只有一个返回值,那么直接写返回类型,如果有多个没有命名的返回值,那么必须使用括号,要写成(type1,...,typeN).如果有一个或多个命名的返回值,则必须写成这样(value1 type1, .., valueN typeN)。注意返回值要么全部命名,要么不命名。

func functionName(optionalParameters) optionalReturnType {

}

func functionName(optionalParameters) (optionalReturnValue) {

}

1)函数的定义

func (p mytype) funcname(q int)(r, s, int) {
return 0, 0
}
  • 关键字func用于定义一个函数:
  • 函数可以绑定到特定的类型。这叫接受者,有接收者的函数被称为method。
  • 参数用传值的方式传递,意味着它们会被复制
  • 参数r和s是这个函数的命名返回值。在Go中的函数可以返回多个值。如果不想对返回的参数命名,也可以只提供类型。

可以随便安排函数定义的顺序 ,编译器会在执行前扫描每个文件。Go不允许函数嵌套。

func rec(i int) {
if i == 10 {
return
}
rec(i+1)
fmt.Printf("%d ", i)
}

2)作用域

在Go中,定义在函数外面的变量都是全局的,那些定义在函数内部变量,对于函数来说是局部的。如果命名覆盖,在函数执行的时候,局部变量将覆盖全局变量。

package main
var a = 6
func main() {
p()
q()
p()
} func p() {
println(a)
} func q() {
a := 5
println(a)
}

3)多值返回

Go语言和Python一样,可以返回多个值。这样可以用于改进一堆在C语言中槽糕的惯例用法:修改参数的方式,返回一个错误。在Go中,Write返回一个计数值和一个错误。

func(file *File) Write(b []byte)(n int, err error)

它返回写入的字节数,并且在n != len(b)的时候,返回nil的error。这是Go中常见的方式。

4)变参

接受不定的参数的函数叫做变参函数,定义函数使其接受变参:

func myfunc(argc ...int) {
}

arg ... int 告诉Go这个函数接受不定数量的参数。注意这些参数类型全部是int。在函数体,变量arg是一个int类型的slice:

for _, n = range arg {
fmt.Printf("And the number is : %d", n)
}

5)回调

由于函数是值,很容易传到其他函数里,然后作为回调。

func printit(x int) {
fmt.Printf("%v\n", x)
}

使用这个函数作为回调,创建一个新的函数来接受。

func callback(y int , f func(int)) {
f(y)
}

接口

在Go语言中,接口是一个自定义类型,它声明了一个或者多个方法签名。接口是完全抽象的,因此不能将其实例化。每种类型都有接口,意味着对那个类型定义了方法的集合。

1)定义一个接口

type Exchanger interface {
Exchange()
}

Exchanger接口声明了一个方法Exchange(),它不接受输入值也不返回输出。一个非空的接口并没有什么作用,一般我们需要创建一个自定义的类型,其中定义了一些接口所需要的方法。

type StringPair struct {
first string
second string
} func (pair * StringPair) Exchange {
pari.first, pair.second = pair.second,pair.first
} type Point [2]int
func (point *Point) Exchange() {
point[0], point[1] = point[1],point[0]
}

自定义的类型StringPair和Point完全不同,但是由于它们提供了Exchange()方法,因此两个都能够满足Exchanger接口。这意味着我们可以创建StringPair和Point值,并将它们传递给接受Exchanger的函数。下面看一下调用的情况:

jekyll := StringPair{"Henry","Jeklly"}
hyde := StringPair{"Edward","Hyde"}
point := Point{5,3}
fmt.Println("Before :" , jekyll,hyde,point)
hyde.Exchange()
point.Exchange()
fmt.Println("After #1:" ,jekyll,hyde,point)
exchangeThese(&jekyll,&hyde,&point)
fmt.Println("After #2 ",jekyll,hyde,point) func exchangeThese(exchangers...Exchanger) {
for_, exchanger := range exchangers {
exchanger.Exchange()
}
}

在调用exchangeThese()函数的时候,我们必须显示的传入值的地址,加入我么传的仅仅是值,Go编译器会发现StringPair不能够满足Exchanger接口,因为在StringPair的类型的值上并未定义方法。

2) 接口嵌入

Go语言的接口对嵌入的支持非常好。接口可以嵌入到其他接口,其效果与在接口中直接添加被嵌入的接口方法一样。

type LowerCaser interface {
LowerCase()
}
type UpperCaser interface {
UpperCase()
} type LowerUpperCaser interface {
LowerCaser
UpperCaser
}

下面定义一个结构体,一个字段,两个方法

type S struct {
i int
} func(p *S) Get() {
return p.i
} func(p *S)Put(v int) {
p.i = v
}

下面定义接口类型,仅仅是方法的集合。

type I interface {
Get() int
Put(int)
}

对于接口I,S是合法的实现,因为它定义I所需的方法。注意,即便是没有明确定义S实现I,(也就是在java中类似使用imiplements关键字来表示实现)也是正确的。Go程序员可以利用这个特点来实现接口的另一个含义,就是接口值。

func f(p I) {
fmt.Fprintf(p.Get())
p.Put(1)
}

我们发现不需要明确一个类型是否实现了一个接口意味着Go实现了叫做duck typing的模式。

3)空接口

每个类型都能够匹配到空接口:interface{},我们可以创建一个接受空借口作为参数的普通函数。

func g(something interface{}) int {
return something.(I).Get()
}

在这个函数中的return something.(I).Get()是有有一点窍门的,值something具有类型interface{},意味着方法没有任何约束:它能包含任何类型,.(I)是类型断言,用于转换something到I类型的接口。如果有这个类型,则可以调用Get()函数。

在Go中的创建指向接口的指针是没有意义的。实际上创建接口的指针也是非法的。

4)接口名字

根据规则,单方法接口命名为方法名加上er的后缀:Reader,Writer,Formatter等。有一堆这样的命名,高效的反映了它们的职责和包含的函数名。为了避免混淆,除非有类似的声明和含义,否则不要让方法与这些重命名。相反,如果类型实现了与众所周知的类型相同的方法,那么就用相同的名字和声明;将字符串转换方法命名String而不是ToString。

例子

下面看看冒泡排序的例子:

func bubble_sort(n []int)  {
for i := 0 ; i < len(n) -1 i++ {
for j: = i+1 ; j < len(n) ; j++ {
if n[j] < n[i] {
n[i], n[j] = n[j], n[i]
}
}
} }

如果要对字符串排序,版本是类似的。除了函数声明:

func bubble_sort_string(n []string)  {
/*
*
*/
}

基于此,可能需要两个函数,每个类型1个。而通过接口可以让这个变得通用。那么如何创建Go形式的这些通用的函数?

1)定义若干个排序相关的方法的接口类型(这里叫做Sorter)。至少需要获取slice的长度,比较两个值的函数和交换函数:

type Sorter interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}

2)定义用于排序slice的新类型。注意定义的slice类型:

type Xi []int
type Xs []String

3)实现Sorter接口的方法。整数的:

func (p Xi) Len() int  {
return len(p)
} func (p Xi) Less(i int, j int) bool {
return p[i] < p[j]
} func (p Xi) Swap(i int ,j int) {
p[i], p[j] = p[j], p[i]
} //字符串
func (p Xs) Len() int {
return len(p)
} func (p Xs) Less(i int, j int) bool {
return p[i] < p[j]
} func (p Xs) Swap(i int ,j int) {
p[i], p[j] = p[j], p[i]
}

4) 编写用于Sorter接口的通用函数:

func Sort(x Sorter) {
for i := 0 ; i < x.Len() - 1; i++ {
for j := i+1 ; j < x.Len(); j ++ {
if x.Less(i, j) {
x.Swap(i, j)
}
}
}
} ints := Xi{44, 67, 3, 18}
Strings := Xs{"nuts", "ape"}
Sort(ints)
fmt.Printf("%v\n")

集合类型

数组

Go语言的数组是一个定长的序列,其元素类型相同,多位数组简单的使用自身为数组元素来创建。

数组的创建
[length] Type
[N] Type{value1,value2...,valueN}
[...]Type{value1,value2,value3}

在第三种创建方式中,...表示Go语言会自动的就算数组的长度。任何情况下,数组的长度都是不可变的。

var buffer [20]byte
var grid1 [3][3]int
cities := [...]string{"Shanghai","Mumbai"}
cities[len(cites)-1] = "Karachi"

数组的长度是可以使用len()函数获取,但是由于长度固定,所以容量和长度相等。cap()函数和len()函数返回的数字是一样的。数组可以使用与字符串和切片一样的语法进行切片,只不过结果为切片,而非数组。数组也可以使用for....range来迭代。

Go中的数组和C语言中数组的不同点

  • 数组全部是值,所有将一个数组赋值到另一个数组将会引起拷贝。
  • 如果将数组传递给一个函数,那么将会拷贝数组中所有的元素。
  • 数组的大小是类型的一部分. [10] int 和 [20]int 是不同的类型
func Sum(a *[3]float64) float64 {
sum := 0.0
for _, v = range *a {
sum += v
}
return sum;
} array := [...]float64 {7.0, 2.0, 1}
x := Sum(&array)

切片

切片包裹了数组,提供了更加方便的接口来使用。在绝大多数的情况下,在Go中,你需要使用切片而不是数组来解决问题。切片保持了对一个数组的引用,所有你将一个切片赋值到另一变量,则两个切片都指向了一个数组

make([]Type, length, capacity)
make([]Type, length)
[]Type{}
[]Type{value1,value2,...,valueN}

内置函数make()用来创建切片映射,和通道。当用于创建一个切片时,它会创建一个隐藏的初始化为0值的数组,然后返回一个该隐藏数组的切片引用。该隐藏的数组和Go语言中的数组的长度都是固定的。我们可以使用append函数来有效的切片的容量。

s := []string{"A","B","C","D","E","F","G"}
t := s[:3] //[A,B,C]
u := s[3:len(s)-1]
u[1] = "x"

由于切片是s,t和u都是同一个底层数组的引用,其中一个改变会影响别人。

Map

Map是很方便的内置的数据结构。键可以是任何定义了=的类型,同样类似于切片,map也是引用类型。

var timezone = map[string]int {
"UTC" : 0 * 60 * 60,
"EST": -5*60*60,
}
offset := timeZone["EST"]

获取一个键不存在的值,那么返回的是该值类型对应的0值。

指针

Go有指针,然而没有指针运算,更像是引用。在Go中调用函数的时候,变量是值传递的。因此,为了修改一个传递函数的值的效率和可能性,有了指针。通过类型作为前缀来定义指针: var p *int 现在p指向了整数值的指针,所有新定义的变量都被赋值为nil。

var p *int
fmt.Printf("%v", p) var i int
p = &i
fmt.Printf("%v", p) // 0x7ff96b

自定义类型

Go允许定义新的类型,通过关键字type实现:

type foo int

这样创建了一个新的类型foo作用跟int一样。创建更加复杂的类型要用到struct关键字。一个数据结构中记录某人的姓名和年龄。

package main
type NameAge struct {
name String
age int
}
func main() {
a := new(NameAge)
a.name = "peter"
a.age = 42
fmt.Printf("%v\n", a)
}

内存分配

Go同样有垃圾回收,无需担心内存的分配和回收。Go有两个内存分配原语,new 和 make。 它们应用于不同的类型,做不同的工作。

用 new 分配内存

内建函数new本质上说跟其他语言中的同名函数功能一样:new(T)分配0值填充的T类型的内存空间,并且返回其地址,一个*T类型的值。用Go的术语中,它返回了一个指针,指向新分配的类型T的0值。0值是非常有用的

type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
p : = new(SyncedBuffer) // Type *SyncedBuffer 已经可以使用
var v SyncedBuffer

有时候,使用0值来初始化,是不合适的。需要使用初始化构造器。

构造器和复合常量

func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.name = name
f.fd = fd
f.dirinfo = nil
f.nepipe = 0
return f
}

上面写的有点冗长,我们可以使用复合声明来使其更加简洁。

func NewFile(fd int, name string) *File {
if fd < 0 {
return nill
}
f := File{fd, name, nil, 0}
return &f
}

不像是C,返回局部的地址是合适的,这块存储区在函数返回后,仍然存在。

用 make分配内存

回到内存分配。内建函数make(T, args)与new(T)有着不同的功能。它只能创建slice, map和channel,并且返回一个初始值(非0)的T类型,而不是*T。本质上来讲,这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。

var p*[]int = new ([]int)
var v []int = make([]int, 100) // 创建100个整数的数组

通信和并发

goroutine是程序中和其它goroutine完全相互独立而并发执行的函数或者是方法调用。每个Go程序至少有一个goroutine,即会执行main包中main函数的主goroutine。goroutine非常像轻量级的线程或者协程,它们可以被大批量的创建。所有的goroutine共享相同的地址空间,同时Go提供了锁原语来保证数据能够跨goroutine共享,但是Go推荐爱的并发编程方式是通信。

Go语言的通道是一个双向或者单向的通信管道,用于两个或者多个goroutine之间通信数据。goroutine使用go语句来创建:

go function(arguments)
go func(parameters) {block} (arguments)

语句

下面介绍Go中的语句,包括函数,if,for,select等基本语句。

基础

形式上看,Go语言的语法需要使用分号,来作为语句的分隔符号,但是Go编译器会自动添加分号。除了两个地方必须要加分号:一个是一行有多条语句的时候,另一个是在原始的for循环中。自动添加分号的结果就是左括号无法单独成为一行。

分支

Go提供了三种分支语句,分别是if,switch,select三种。

if语句

if optionalStatement1; booleanExpression1 {

} else if optionalStatement2; booleanExpression2 {

} else {

}

语句中大括号是必须的。但条件中的分号只有optionStatement出现的时候才需要,如果变量是指可选的声明语句中创建的,它们的作用域会从声明处扩展到if语句完成出。布尔表达式必须是bool型,Go语言不会自动转化非bool值。

if a := compute();a <0 {
fmt.Printf("(%d)\n",-a)
} else {
fmt.Println(a)
}

switch语句

Go语言中有两种switch语句,一种是表达式开关(expression switch),另一种是类型开关(type switch)。Go语言中的switch语句不会自动的向下贯穿。就是不必在每个case语句下,添加break。

表达式开关
switch optionalStatement; optionalExpression {
case expressionStatement: block1
...
case expressionListN: blockN
default: blockD
}

如果有可选的声明语句,那么其中的分号是必须的。如果switch语句中没有包含可选的表达式语句,那么编译器会假设其表达式为true。每个case语句必须有一个表达式列表。其中包含一个或多个由分号分隔的表达式。其类型与switch中可选的表达式类型相匹配。如果switch没有可选的表达式,那么会将表达式的类型设置为true。

func BoudedInt(mininum, value, maximum int) int {
switch {
case value < mininum :
return minimum
case value > maximum
return maximum
default:
return value
}
panic("unreachable")
}
类型开关

for循环语句

for循环中的大括号必须。但是分号不是必须的。如果变量中可选的声明语句中创建,那么他们的作用域一直到for语句的末尾。for循环可以使用break来结束。

for {       //无限循环

}
for booleanExpression { //while语句 }
for optioanalPreExpression; booleanExpression; optionalPostStatement { } // 遍历字符串
for index,char := range aString { }
for index := range aString { } for index, item := range anArrayOrSlice { } for index := range anArrayOrSlice { }
for item := range aChannel { }
for key,value := range aMap { }

select语句

defer,panic和recover

假设有一个函数,打开文件并且对其进行读写。在这样的函数中,经常有提前返回的地方。这样就会需要关闭文件描述符。所以出现了这样的代码:

func ReadWrite() bool {
file.Open("file")
if failureX {
file.Close()
return false
} if failureY {
file.Close()
return false
}
}

在这里有许多重复的代码,为了解决这个问题,Go有了defer语句。在defer后指定函数会在函数退出前调用(进行清理工作).

func ReadWrite() bool {
file.Open("file")
defer file.Close() //file.Close() 被添加到defer列表中。 if failureX {
return false
}
}

可以将多个函数放在延迟列表中,

for i := 0; i < 5 ; i++  {
defer fmt.Printf("%d" , i)
}

延迟的函数按照先进后出的顺序的执行,上面的代码打印4,3,2,1,0。

panic和recover

包是函数和数据的集合。用package关键字定义一个包。文件名不需要与包名一致。包名的约定是使用小写字母。Go包可以有多个文件组成。 每个文件都只能够属于一个包。但是使用package 这一行。让我们文件event.go中定义一个叫even的包。每个可执行程序都必须包含一个main包。

package even
func Even(i int) bool {
return i % 2 == 0
} func odd(i int) bool {
return i %2 == 1
}

名字的可见性规则:

Go中的标志符,(常量,变量,类型,函数,结构体字段等)如果是以大写字母开头,那么这个标志符相关的对象在包外是不能够被看到的。但是在包内是可以看到的。

引用了没有使用的包,被认为是错误。名字以大写字母开始是可以导出的。可以在外部调用。现在在myeven.go使用这个包:

package main
import (
"even"
"fmt"
) func main() {
i := 5
fmt.Printf("Is %d even? %v\n", even.Even(i))
}

在Go中,当函数的首字母大写的时候,函数会被从包中导出,因此函数名是Even。如果修改myeven.go中第10行。概况起来:

  • 公有函数的名字·以大写字母开头
  • 私有的函数以小写名字开头。

这个规则同样适用于包中其他名字(新类型、全局变量。如果首字母为大写,我们在导入包后,就能够直接访问该名字(可以是全局变量,函数,类型等)。

标识符

像其它语言一样,Go的命名是很重要的。在某些情况下,它们甚至有语义上的作用。

包名

当包导入(通过import)时,包名成为了内容的入口。

import "bytes"

导入后,可以调用bytes.Buffer。任何使用这个包的人,可以使用同样的名字访问它的内容,因此这样的包名是好的:短的,简洁的,好记的。根据规则,包名是小写的一个单词;不应该有下划线或混合大小写。保持简洁。

包的文档

每个包都应该有包的注释,在package前的一个注释块。对于多文件包,包的注释只需要在一个文件前,任意一个文件都可以。包注释应该对包进行介绍,并提供包的整理信息。每个定义(并且导出)的函数应该有一小段文字描述该函数的行为。

测试包

在Go中,为包编写单元测试是一种习惯。编写测试需要包含testing包和程序go test。两者都有良好的文档。go test程序调用所有的测试函数。even包没有定义任何测试函数,执行go test。在测试文件中定义一个测试来修复这个问题,测试文件在包目录中,被命名为 *_test.go 。这些测试文件同Go程序中的其他文件一样,但 go test只会执行测试函数,每个测试函数都有相同的标志,它的名字以Test开头

func TestXxxx(t *testing.T) {

}

导入包

常见的方式有两种,一种如下:

import "fmt"
import "math" // 下面这种是比较好的方式
import (
"fmt"
"math"
)

常用包

  • fmt

    包fmt实现了格式化的I/O函数,这与C的printf和scanf类似
  • io

    这个包提供了原始的I/O操作,主要对os包这样的原始的I/O进行封装。
  • bufio

    这个包实现了缓冲I/O。它封装了io.Reader和io.Writer对象
  • os

    os包提供了与平台无关的操作系统功能接口。
  • net/http

    net/http 实现了HTTP请求、响应、和URL的解析