第一个Go程序
package main
import "fmt"
func main(){
fmt.Println("Hello world!")
}
运行
Go
是一门编译型语言,Go
语言的工具链将源代码及其依赖转换成计算机的机器指令。Go
语言提供的工具都通过一个单独的命令go
调用,go
命令有一系列子命令。
go build ./src/helloworld.go ./helloworld
Hello world!
<a name="GTnP1"></a>
### 代码结构
<a name="JBkVm"></a>
### package
`Go`语言的代码通过包`(package)`组织,包类似于其它语言里的库`(libraries)`或者模块`(modules)`。**一个包由位于单个目录下的一个或多个**`**.go**`**源代码文件组成,目录定义包的作用。每个源文件都以一条**`**package**`**声明语句开始,这个例子里就是**`**packagemain**`**,表示该文件属于哪个包,紧跟着一系列导入**`**(import)**`**的包,之后是存储在这个文件里的程序语句**
<a name="pW3K1"></a>
### import
必须告诉编译器源文件需要哪些包,这就是import声明以及随后的`package`声明扮演的角色。`helloworld`例子只用到了一个包,大多数程序需要导入多个包
<a name="Jli3O"></a>
### main
`main`包比较特殊。它定义了一个独立可执行的程序,而不是一个库。在`main`里的`main`函数也很特殊,它是整个程序执行时的入口7。`main`函数所做的事情就是程序做的。当然了,`main`函数一般调用其它包里的函数完成很多工作,比如`fmt.Println`。
<a name="RcY67"></a>
### func
一个函数的声明由`**func**`**关键字、函数名、参数列表、返回值列表**(这个例子里的main函数参数列表和返回值都是空的)以及包含在大括号里的**函数体**组成。
<a name="FSKRB"></a>
## 命令行参数
大多数的程序都是**处理输入,产生输出**;但是,程序如何获取要处理的输入数据呢?一些程序生成自己的数据,但通常情况下,输入来自于程序外部:文件、网络连接、其它程序的输出、敲键盘的用户、命令行参数或其它类似输入源。<br />`os`包以跨平台的方式,提供了一些与操作系统交互的函数和变量。程序的命令行参数可从`os`包的`Args`变量获取;`os`包外部使用`os.Args`访问该变量。
- `os.Args`变量是一个字符串`(string)`的切片`(slice)`
- 用`s[i]`访问单个元素,用`s[m:n]`获取子序列,和`python`基本一样
- 左闭右开
- `os.Args`的第一个元素,`os.Args[0]`,是命令本身的名字;其它的元素则是程序启动时传给它的参数
如下面的代码会输出每个传入命令的信息
- `var`声明定义了两个`string`类型的变量`s`和`sep`。变量会在声明时直接初始化。如果变量没有显式初始化,则被隐式地赋予其类型的零值`(zerovalue)`,数值类型是`0`,字符串类型是空字符串`""`
```go
package main
import(
"fmt"
"os"
)
func main(){
var s, sep string
for i:= 1; i < len(os.Args); i++{
s += sep + os.Args[i] // + 运算符连接字符串
sep = " "
}
fmt.Println(s)
}
## run
go run ./src/args.go arg1 arg2 arg3
## out
arg1 arg2 arg3
for
**Go**
语言只有**for**
循环这一种循环语句。for
循环有多种形式,其中一种如下所示
for initialization; condition; post {
// zero or more statements
}
**initialization**
:在循环开始前执行(可选),必须是一条简单语句(simplestatement)
,即,短变量声明、自增语句、赋值语句或函数调用**condition**
:布尔表达式booleanexpression
其值在每次循环迭代开始时计算。如果为true
则执行循环体语句**post**
:post
语句在循环体执行结束后执行,之后再次对conditon
求值。condition
值为false
时
for
循环的三个部分均可以省略,因此还有以下几种方式:
保留
condition
,实现while
的功能for condition { // zero or more statements }
全部不保留:无限循环
whileTrue
,可以使用return``break
打断for { // zero or more statements }
某种数据类型的区间
(range)
上遍历,如字符串或切片,例如下面(**range**
会同时返回元素的值和**index**
因此使用**_**
占位) ```go package main
import ( “fmt” “os” )
func main(){ seq := “ “ s := “” for _, arg := range os.Args[1:] { s += seq + arg } fmt.Println(s) }
连接字符还有一种更高效的办法,`strings`中的`Join`
```go
...
import (
...
"strings"
)
func main() {
fmt.Println(strings.Join(os.Args[1:], " "))
}
var
使用一条短变量声明来声明并初始化上述代码的s
和seps
,也可以将这两个变量分开声明,声明一个变量有好几种方式,下面这些都等价,实践中一般使用前两种形式中的某个,初始值重要的话就显式地指定变量的类型,否则使用隐式初始化
s := "" // 只可以用于函数内部,包内变量不可以
var s string // 利用初始化机制,s = ""
var s = "" // 用的少,除非一次声明多个变量
var s string = "" // 显式地标明变量的类型, 当变量类型与初值类型相同时, 类型冗余, 但如果两者类型不同, 变量类型就必须了
文件输入(查找重复行)
对文件做拷贝、打印、搜索、排序、统计或类似事情的程序都有一个差不多的程序结构:一个处理输入的循环,在每个元素上执行计算处理,在处理的同时或最后产生输出。
查找重复行
第一个版本打印标准输入中多次出现的行,以重复次数开头。该程序将引入if
语句,map
数据类型以及bufio
包
package main
import (
"fmt"
"os"
"bufio"
)
func main(){
counts := make(map[string]int)
input := bufio.NewScanner(os.Stdin)
for input.Scan() {
counts[input.Text()]++
}
for line, n := range counts {
if n > 1 {
fmt.Println("%d\t\n", n, line)
}
}
}
if
如for
循环一样,if
语句条件两边也不加括号,但是主体部分需要加。if
语句的else
部分是可选的,在if
的条件为false
时执行
map
map
存储了键/值(key/value)
的集合,对集合元素,提供常数时间的存、取或测试操作。键可以是任意类型(最常见的是字符串),只要其值能用==
运算符比较,和python
中的dict
类似
//每次 dup 读取一行输入, 该行被当做 map , 其对应的值递增。
counts[input.Text()]++
map自带默认值(类似于python
的defaultdict
),首次读到新行时,等号右边的表达式counts[line]的值被计算为其类型的零值,对于int
即0
range
为了打印结果,我们使用了基于range
的循环,并在counts
这个map
上迭代。跟之前类似,每次迭代得到两个结果,键和其在map
中对应的值。map
的迭代顺序并不确定,从实践来看,该顺序随机,每次运行都会变化。
bufio
Scanner类型是该包最有用的特性之一,它读取输入并将其拆成行或单词;通常是处理行形式的输入最简单的方法,用短变量声明创建**bufio.Scanner**
类型的变量**input**
。类似于创建了**bufio.Scanner**
的一个实例?
input := bufio.NewScanner(os.Stdin)
读取过程
for input.Scan() {
counts[input.Text()]++
}
- 每次调用
input.Scanner
,即读入下一行,并移除行末的换行符 - 读取的内容可以调用
input.Text()
得到 Scan
函数在读到一行时返回true
,在无输入时返回false
printf
格式化输出fmt.Println("%d\t\n", n, line)
从文件中读取内容
按行读取处理
使用os.Open
打开某个文件 ```go package main
import ( “fmt” “bufio” “os” )
func main() { counts := make(map[string]int) files := os.Args[1:] if len(files) == 0 { countLines(os.Stdin, counts) } else { for _, arg := range files { f, err := os.Open(arg) if err != nil { fmt.Fprintf(os.Stderr, “dup2: %v\n”, err) continue } countLines(f, counts) f.Close() } } for line, n := range counts { if n > 1 { fmt.Printf(“%d\t%s\n”, n, line) } } }
func countLines(f *os.File, counts map[string]int) { input := bufio.NewScanner(f) for input.Scan() { counts[input.Text()]++ } }
读取过程<br />`os.Open`函数返回**两个值**。
- 被打开的文件`(*os.File)`,其后被`Scanner`读取
- 内置`error`类型的值。如果`err`等于内置值`nil`(`NULL`),那么文件被成功打开。读取文件,直到文件结束,然后调用`Close`关闭该文件,并释放占用的所有资源。相反的话,如果`err`的值不是`nil`,说明打开文件时出错了。这种情况下,错误值描述了所遇到的问题。
使用`Fprintf`与表示任意类型默认格式值的动词`%v`,向标准错误流打印一条信息,然后程序继续处理下一个文件;`continue`语句直接跳到`for`循环的下个迭代开始执行
```go
...
for _, arg := range files {
f, err := os.Open(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
continue
...
map
是一个由make
函数创建的数据结构的引用。map
作为为参数传递给某函数时,该函数接收这个引用的一份拷贝(copy
,或译为副本),被调用函数对map
底层数据结构的任何修改,调用者函数都可以通过持有的**map**
引用看到。在我们的例子中,countLines
函数向counts
插入的值,也会被main函数看到
读取全部内容后进行处理
ReadFile
函数(来自于io/ioutil
包),其读取指定文件的全部内容,strings.Split
函数把字符串分割成子串的切片。
由于ReadFile
函数需要文件名作为参数,因此只读指定文件,不读标准输入。其次,由于行计数代码只在一处用到,故将其移回main
函数
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
)
func main() {
counts := make(map[string]int)
for _, filename := range os.Args[1:] {
data, err := ioutil.ReadFile(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "dup3: %v\n", err)
continue
}
// ReadFile 函数返回一个字节切片(byte slice),
// 必须把它转换为string,才能用strings.Split分割
for _, line := range strings.Split(string(data), "\n"){
counts[line]++
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
switch
一个例子, coinflip
函数返回几种不同的结果, 每一个case
都会对应一个返回结果, 这里需要注意, Go
语言并不需要显式地在每一个case
后写break
, 语言默认执行完case
后的逻辑语句会自动退出。
switch coinflip() {
case "heads":
heads++
case "tails":
tails++
default:
fmt.Println("landed on edge!")
}
switch
也可不跟操作对象(如 函数),此时默认用true
代替,case
中的每个表达式与true
比较, 称为tag switch(tagless switch)
; 和switch true
是等价的,switch
也可以紧跟一个简短的变量声明, 一个自增表达式、 赋值语句, 或者一个函数调用
func Signum(x int) int {
switch {
case x > 0:
return +1
default:
return 0
case x < 0:
return -1
}
}
break continue
break
和continue
语句会改变控制流。 和其它语言中的break
和continue
一样,
break
会中断当前的循环, 并开始执行循环之后的内容-
指针
Go
语言提供了指针。 指针是一种直接存储了变量的内存地址的数据类型。 在其它语言中, 比如C
语言, 指针操作是完全不受约束的。 在另外一些语言中, 指针一般被处理为“引用”, 除了到处传递这些指针之外, 并不能对这些指针做太多事情。**Go**
语言在这两种范围中取了一种平衡。 指针是可见的内存地址, &操作符可以返回一个变量的内存地址, 并且*****
操作符可以获取指针指向的变量内容, 但是在**Go**
语言里没有指针运算, 也就是不能像**C**
语言里可以对指针进行加或减操作。命名类型
类型声明使得我们可以很方便地给一个特殊类型一个名字。 因为
struct
类型声明通常非常地长, 所以我们总要给这种struct
取一个名字。type Point struct { X, Y int } var p Point
方法和接口
方法是和命名类型关联的一类函数。
Go
语言里比较特殊的是方法可以被关联到任意一种命名类型。有点像R
的S3 method
?- 接口是一种抽象类型, 这种类型可以让我们以同样的方式来处理不同的固有类型, 不用关心它们的具体实现, 而只需要关注它们提供的方法。