Hello, World
Hello world
package main
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
package main
表明这是一个可独立执行的程序包;import "fmt"
导入标准库中的fmt
包,用于格式化输入输出 ;main
函数是程序的入口,fmt.Println
函数输出 “Hello, 世界”。
程序运行
go run
命令:$ go run helloworld.go
可直接编译、链接并运行.go
源文件,输出 “Hello, 世界” 。go build
命令:$ go build helloworld.go
将生成名为helloworld
的二进制程序,之后执行$./helloworld
也能输出 “Hello, 世界” ,适用于创建可复用程序。
语言特性
Unicode 支持:Go 原生支持 Unicode,能处理各国语言。
包与导入:Go 代码通过包组织,
package
声明文件所属包,import
导入其他包。main
包用于定义独立可执行程序,main
函数是程序执行起点。导入包需精确,缺失或多余导入会致编译失败,import
声明要在package
声明之后 。函数声明:由
func
关键字、函数名、参数列表、返回值列表(可空)和函数体组成。语法格式:Go 语言语句或声明后一般不用分号结尾(特殊情况除外);对代码格式化要求严格,可使用
gofmt
工具格式化代码,goimports
工具可按需管理导入声明。 还提到可通过go get
命令获取goimports
工具,多数 Go 操作可通过go
工具实现。
命令行参数
程序常需处理输入产生输出,输入源多样,命令行参数是常见输入源之一 。Go 通过os
包中的os.Args
获取命令行参数,os.Args
是字符串切片(slice) 。os.Args[0]
是命令本身名字,os.Args[1:]
是程序执行时传入参数 。
echo
package main
import (
"fmt"
"os"
"strings"
)
// echo1
func main() {
var s, sep string
for i := 1; i < len(os.Args); i++ {
s += sep + os.Args[i];
sep = " ";
}
fmt.Println(s);
}
导入
fmt
和os
包,在main
函数中,通过for
循环遍历os.Args[1:]
,使用+=
操作符将参数追加到字符串s
中,参数间用空格分隔,最后通过fmt.Println
输出所有参数 。Go 语言中变量声明(
var s, sep string
)、操作符(+=
)以及for
循环语法 。for
循环有多种形式,for initialization; condition; post
,初始化、条件判断和后置操作部分可省略,可实现无限循环等 。
// echo2
func main() {
s, sep := "", ""
for _, arg := range os.Args[1:] {
s += sep + arg;
sep = " ";
}
fmt.Println(s);
}
利用
range
关键字遍历os.Args[1:]
,range
会返回索引和对应元素值。当不需要索引时,可使用空白标识符_
。短变量声明(如
s, sep := "", ""
)以及等价的变量声明方式 。
// echo3
func main() {
fmt.Println(strings.Join(os.Args[1:], " "));
}
- 使用
strings.Join
函数,传入os.Args[1:]
和分隔符" "
,可将命令行参数以指定分隔符连接成字符串输出,更为简洁高效 。
找出重复行
// dup1
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.Printf("%d\t%s\n", n, line)
}
}
}
- 功能:输出标准输入中出现次数大于 1 的行,前面是出现次数 。
- 实现:
- 导入
bufio
、fmt
、os
包 。在main
函数中,用make
函数创建map[string]int
类型的counts
,用于统计每行出现次数。 - 通过
bufio.NewScanner(os.Stdin)
创建扫描器input
读取标准输入,for input.Scan()
循环逐行读取,counts[input.Text()]++
统计每行出现次数。 - 再通过
for line, n := range counts
遍历counts
,用if n > 1
判断,若出现次数大于 1,使用fmt.Printf("%d\t%s\n", n, line)
格式化输出次数和行内容 。
- 导入
- 相关知识:介绍了
if
语句语法(条件部分不放在圆括号,程序体用大括号 );map
数据结构(存储键值对,make
新建,键类型任意可比较,值任意类型 );bufio.Scanner
扫描器读取输入 ;fmt.Printf
格式化输出(介绍常用占位符如%d
、%s
等 ) 。
// dup2
func main() {
counts := make(map[string]int)
files := os.Args[1:];
if len(files) == 0 {
countsLines(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
}
countsLines(f, counts)
f.Close()
}
}
for line, n := range counts {
if n > 1 {
fmt.Printf("%d\t%s\n", n, line)
}
}
}
func countsLines(f* os.File, counts map[string]int) {
input := bufio.NewScanner(f)
for input.Scan() {
counts[input.Text()]++
}
}
- 功能:可从标准输入或指定文件列表读取,打印输入中多次出现的行的个数和文本 。
- 实现:
- 同样导入相关包,创建
counts
统计次数 。先判断命令行参数os.Args[1:]
,若为空,直接统计标准输入countLines(os.Stdin, counts)
。 - 若有参数,遍历参数列表,用
os.Open
打开文件,若打开失败用fmt.Fprintf(os.Stderr, "dup2: %v\n", err)
输出错误信息并continue
继续下一个文件 。打开成功则调用countLines(f, counts)
统计行数,最后关闭文件f.Close()
。countLines
函数逻辑和dup1
类似,通过扫描器逐行读取统计 。
- 同样导入相关包,创建
- 相关知识:介绍
os.Open
返回文件指针和错误值,文件读取完需Close
释放资源;简单错误处理方式 ;函数声明顺序可任意 ;map
作为引用类型,函数中对其修改在调用处可见 。
// dup3
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
}
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)
}
}
}
- 功能:从指定文件读取,统计重复行 。
- 实现:导包,遍历命令行参数
os.Args[1:]
,用ioutil.ReadFile
读取文件内容到data
,若失败输出错误信息并continue
。成功读取后,用strings.Split(string(data), "\n")
按行分割内容,再遍历分割后的行统计次数并输出重复行 。 - 相关知识:
ioutil.ReadFile
读取文件内容为字节切片,可转换为字符串;strings.Split
分割字符串 。
GIF动画
var palette = []color.Color{color.White, color.Black}
const (
whiteIndex = 0
blackIndex = 1
)
func main() {
rand.Seed(time.Now().UTC().UnixNano())
if len(os.Args) > 1 && os.Args[1] == "web" {
handler := func (w http.ResponseWriter, r *http.Request) {
lissajous(w)
}
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
return
}
lissajous(os.Stdout)
}
func lissajous(out io.Writer) {
const (
cycles = 5
res = 0.001
size = 100
nframes = 64
delay = 8
)
freq := rand.Float64() * 3.0
anim := gif.GIF{LoopCount: nframes}
phase := 0.0
for i := 0; i < nframes; i++ {
rect:= image.Rect(0, 0, 2*size + 1, 2*size + 1)
img := image.NewPaletted(rect, palette)
for t := 0.0; t < cycles*2*math.Pi; t += res {
x := math.Sin(t)
y := math.Sin(t*freq + phase)
img.SetColorIndex(size + int(x*size + 0.5), size + int(y*size + 0.5), blackIndex)
}
phase += 0.1
anim.Delay = append(anim.Delay, delay)
anim.Image = append(anim.Image, img)
}
gif.EncodeAll(out, &anim)
}
- 功能:利用 Go 标准图像包创建一系列位图图像,并编码为 GIF 动画,展示利萨茹图形(参数化的二维谐振曲线,类似示波器 x 轴和 y 轴馈电输入的两个正弦波图形 )。
实现:
- 常量与变量声明:
- 用
var
声明palette
切片存储颜色 ,包含白色和黑色 。 - 用
const
声明whiteIndex
和blackIndex
常量,分别表示画板中白色和黑色的索引 。const
用于给编译期间值固定的量命名,可在包级别或函数内声明,常量类型为数字、字符串或布尔值 。
- 用
- 结构体与复合字面量:
gif.GIF
是结构体类型,anim
变量是该结构体实例 。通过复合字面量gif.GIF{LoopCount: nframes}
创建anim
,并后续通过点记法显式更新其Delay
和Image
字段 。复合字面量是用一系列元素值初始化 Go 复合类型的紧凑表达方式 。
- 核心函数
lissajous
:- 函数接受
io.Writer
类型的out
参数,用于输出 GIF 动画 。函数内通过一系列常量定义动画参数,如cycles
(x 振荡器变化个数 )、res
(角度分辨率 )、size
(图像画布尺寸 )、nframes
(动画帧数 )、delay
(帧间延迟 )等 。 - 生成随机的 y 振荡器相对频率
freq
,创建gif.GIF
结构体anim
并设置循环帧数 。通过两层嵌套循环,外层循环控制帧数,内层循环在每个帧内,基于正弦函数计算 x 和 y 坐标,设置图像对应坐标点颜色(白色或黑色 ),并将延迟和图像帧追加到anim
中 ,最后用gif.EncodeAll
将动画编码写入out
。
- 函数接受
main
函数:设置随机数种子,根据命令行参数判断是否作为 Web 服务运行(若参数为"web"
),若为 Web 服务则注册处理函数并启动服务;否则直接调用lissajous
函数将动画输出到标准输出 。
- 常量与变量声明:
使用:
go run .\main\lissajous.go web
运行代码,浏览器中访问http://localhost:8000
,就能看到生成的 Lissajous 曲线的 GIF 动画。
获取一个URL
func main() {
for _, url := range os.Args[1:] {
resp, err := http.Get(url)
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
os.Exit(1)
}
b, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n", url, err)
os.Exit(1)
}
fmt.Printf("%s", b)
}
}
功能:
fetch
程序用于获取指定 URL 返回的 HTTP 内容,并将其输出,是从互联网获取信息的示例 。背景:Go 语言在
net
包下提供系列工具处理网络信息,利用这些工具可通过互联网收发信息、创建服务器等,其并发特性在网络应用场景很有用 。实现:
通过
for _, url := range os.Args[1:]
遍历命令行参数中传入的 URL 。对每个 URL,使用
http.Get(url)
发起 HTTP 请求,若请求出错,通过fmt.Fprintf(os.Stderr, "fetch: %v\n", err)
在标准错误输出打印错误信息,然后用os.Exit(1)
以状态码 1 退出程序 。若请求成功,用
ioutil.ReadAll(resp.Body)
读取响应体内容到b
,读取完成后关闭响应体resp.Body.Close()
,若读取过程出错,打印错误信息并退出 。最后用fmt.Printf("%s", b)
将获取到的内容输出到标准输出 。
使用:
go run .\main\fetch.go https://example.com
<!doctype html>
<html>
<head>
<title>Example Domain</title>
// 省略...
</div>
</body>
</html>
并发获取多个URL
func main() {
start := time.Now()
ch := make(chan string)
for _, url := range os.Args[1:] {
go fetch(url, ch) // 启动 goroutine
}
for range os.Args[1:] {
fmt.Println(<-ch) // 从通道 ch 接收
}
fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds)
}
func fetch(url string, ch chan<- string) {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprint(err)
return
}
nbytes, err := io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
if err != nil {
ch <- fmt.Sprintf("while reading %s: %v", url, err)
return
}
secs := time.Since(start).Seconds()
ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
}
功能:
fetchall
程序并发获取多个 URL 的内容,报告每个响应的大小和花费时间,不处理响应具体内容 。背景:Go 语言支持并发编程,
fetchall
程序简单展示了 Go 的并发机制—goroutine 和通道(channel) 。main
函数逻辑:- 记录开始时间
start := time.Now()
,创建字符串通道ch := make(chan string)
。 - 通过
for _, url := range os.Args[1:]
遍历命令行参数中的 URL ,对每个 URL 使用go fetch(url, ch)
启动一个 goroutine 来处理 。 - 再通过
for range os.Args[1:]
循环从通道ch
接收信息并打印 ,最后打印总耗时fmt.Printf("%.2fs elapsed\n", time.Since(start).Seconds())
。
- 记录开始时间
fetch
函数逻辑:- 记录每个 URL 请求开始时间
start := time.Now()
,使用http.Get(url)
发起 HTTP 请求 ,若出错将错误信息通过ch <- fmt.Sprint(err)
发送到通道 。 - 若请求成功,用
io.Copy(ioutil.Discard, resp.Body)
丢弃响应内容(获取字节数 ),关闭响应体resp.Body.Close()
,若读取过程出错也将错误信息发送到通道 。 - 计算请求耗时
secs := time.Since(start).Seconds()
,将耗时、字节数、URL 信息通过ch <- fmt.Sprintf("%.2fs %7d %s", secs, nbytes, url)
发送到通道 。
- 记录每个 URL 请求开始时间
并发机制原理:
goroutine:是一种并发执行的函数,
main
函数在一个 goroutine 中执行,go
语句创建额外的 goroutine ,可异步执行任务 。通道(channel):用于在 goroutine 之间传递指定类型的值 ,起到同步和通信作用 。
fetch
函数向通道发送信息,main
函数从通道接收并处理信息,避免多个 goroutine 输出交织 。
使用:
go run .\main\fetchall.go https://baidu.com https://bilibili.com https://example.com
0.32s 1374 https://bilibili.com
0.47s 2381 https://baidu.com
0.74s 1256 https://example.com
%!f(func() float64=0x7e9900)s elapsed
一个 Web 服务器
// server1
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
- 功能:创建一个迷你 Web 服务器,返回客户端请求 URL 的路径部分 。
// server2
var mu sync.Mutex
var count int
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
mu.Unlock()
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
- 功能:在
server1
基础上扩展,除返回 URL 路径外,对特定的/count
请求,返回当前为止请求的数量 。 handler
函数:在处理请求时,先加锁mu.Lock()
,计数count++
,解锁mu.Unlock()
,再返回 URL 路径 ;counter
函数加锁后,通过fmt.Fprintf(w, "Count %d\n", count)
返回请求计数 ,然后解锁 。引入互斥锁是为防止并发请求时计数变量count
的竞争问题 。
func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host)
fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}
功能:处理程序回显 HTTP 请求,包括请求方法、URL、协议,以及请求头和表单数据等信息 。
使用:
go run .\main\server.go
运行代码,浏览器中访问http://localhost:8000
,就能对应的HTTP请求。
其他
switch coinflip() {
case "heads":
heads++
case "tails":
tails++;
default:
fmt.Println("landed on edge!")
}
switch {
case x > 0:
return +1
default:
return 0
case x < 0
return -1
}
switch
语句:是多路分支控制语句。switch coinflip()
根据coinflip
函数返回值与case
条件值比较,执行第一个匹配的case
语句块,若都不匹配则执行default
语句块 。switch
语句无需操作数,可像布尔表达式列表,还有无标签(tagless)选择形式switch { }
。switch
可包含初始语句,如变量声明、递增赋值或函数调用等 。同时break
和continue
可改变控制流,break
用于中断for
、switch
或select
最内层,continue
让for
内层循环开始新迭代 ,还有goto
语句,但一般不建议使用 。
命名类型:用
type
声明为已有类型命名,如定义2D
图形系统的Point
类型type Point struct { X, Y int }
,可通过var p Point
声明该类型变量 ,类型声明和命名 。指针:Go 语言提供指针,
&
操作符获取变量地址,*
操作符获取指针引用变量的值,但指针不支持算术运算 。方法:关联命名类型的函数,Go 中方法可关联几乎所有命名类型 。
接口:用于处理不同具体类型的抽象类型,基于类型包含的方法定义 。
包:Go 有可扩展实用的标准库,社区也有众多共享库。编程时优先使用现有包,可在
https://golang.org/pkg
查找标准库索引,https://godoc.org
找社区包,通过go doc
工具可查看包文档 ,如$ go doc http.ListenAndServe
。注释:在函数声明前写注释说明行为是好风格,可被
go doc
和godoc
工具识别展示 。多行注释可用类似/*...*/
形式,注释内//
和/*
无特殊含义,且注释不能嵌套 。
Y int } ,可通过
var p Point`声明该类型变量 ,类型声明和命名 。
指针:Go 语言提供指针,
&
操作符获取变量地址,*
操作符获取指针引用变量的值,但指针不支持算术运算 。方法:关联命名类型的函数,Go 中方法可关联几乎所有命名类型 。
接口:用于处理不同具体类型的抽象类型,基于类型包含的方法定义 。
包:Go 有可扩展实用的标准库,社区也有众多共享库。编程时优先使用现有包,可在
https://golang.org/pkg
查找标准库索引,https://godoc.org
找社区包,通过go doc
工具可查看包文档 ,如$ go doc http.ListenAndServe
。注释:在函数声明前写注释说明行为是好风格,可被
go doc
和godoc
工具识别展示 。多行注释可用类似/*...*/
形式,注释内//
和/*
无特殊含义,且注释不能嵌套 。
参考资料:《Go程序设计语言》