基础
go的程序结构:
package main
import (
"fmt"
"math/cmplx"
)
var (
//ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)
func main() {
ToBe = 1
fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
fmt.Printf("Type: %T Value: %v\n", z, z)
}
从库里使用方法的时候,首字母大写的方法或属性才能被使用(被导出)
go的基础数据类型:
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
在函数外,所有语句必须以关键字开头,因此在函数外无法通过
b := 1
来进行变量的定义。:=在函数内部使用实现自动推理类型和变量赋值。
初始化不给出值,则默认为空(0,false,"")
与C不同,go的类型转换需要进行强制类型转换
const World = "世界"
const (
c1 = 1
c2 = 2
)
通过使用关键字来定义常量,由于const属于关键字,因此可以直接在函数外部进行定义。
在作为函数参数时,会进行一定程度的自动类型转换。同时go的int类型极大,大概为64位。
流程控制
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}
func main() {
sum := 1
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum)
}
for循环其实类似于C,但是其必须要求使用大括号把流程语句括起来。对于go,其实没有while语句,而是使用了for语句作为替代:
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
如果条件不填写,则默认为无限循环。
if语句与for类似:
func sqrt(x float64) string {
if x < 0 {
return sqrt(-x) + "i"
}
return fmt.Sprint(math.Sqrt(x))
}
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
} else {
fmt.Printf("%g >= %g\n", v, lim)
}
// can't use v here, though
return lim
}
虽说go的if与for类似,但是if还有一种写法,在条件之前添加一个语句用于表示条件前执行的内容。
func main() {
fmt.Println("When's Saturday?")
today := time.Now().Weekday()
switch time.Saturday {
case today + 0:
fmt.Println("Today.")
case today + 1:
fmt.Println("Tomorrow.")
case today + 2:
fmt.Println("In two days.")
default:
fmt.Println("Too far away.")
}
}
switch语句也与C类似,其中switch语句位为从上往下的进行匹配。这里的time类里面的函数也很有意思哈哈哈。
func main() {
defer fmt.Println("world")
defer fmt.Println("a")
fmt.Println("hello")
}
//output:
//hello
//a
//world
defer的用途为延迟执行,而且是自底向上的执行(是一个栈)
更多的数据类型
go更接近C,其同样具有指针类型,而且操作起来类似C。但是一个区别在于无法对常量const取地址。
func main() {
i, j := 42, 2701
p := &i // point to i
fmt.Println(*p) // read i through the pointer
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i
p = &j // point to j
*p = *p / 37 // divide j through the pointer
fmt.Println(j) // see the new value of j
}
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
v.X = 4
p := &v
p.X = 1e9
fmt.Println(v.X)
}
这里是一个结构体的构造以及如何访问其中变量的过程,可以看出,结构体的构造中的参数通过大括号进行而不是使用圆括号。此外,go语言不具有类 ,转而代之的是type,因此对于每个type,不具有方法,可以通过接收器来进行type的方法的绑定。除了如何构造一个类,go对于类指针的处理方式也有所不同,对于C或C++,类指针应该使用->或者(*p).来进行内部属性的访问,但是对于go,同样可以使用.进行访问。
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // has type Vertex
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
p = &Vertex{1, 2} // has type *Vertex
)
type的构造方式有多种,如果参数不提供的话使用的是默认参数,此外,type在定义时无法设置默认值。关于这部分的内容后续可能还会详解。
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
fmt.Println(primes)
}
列表的例子也简单粗暴,每个列表中的数据类型必须相同,除此之外,其定义通常为[n]T。此外,go数组的访问也相当简单,与python一样是左闭右开。
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
虽然切片操作类似python,但是仍然有不同的地方,切片是对原有内容的引用而不是新的变量,因此对切片的修改都会导致原有数据的修改。
当[n]T中n不存在时,会根据构造的数量自动进行推导。
s := []struct {
i int
b bool
}{
{2, true},
{3, false},
{5, true},
{7, true},
{11, false},
{13, true},
}
上面的例子不仅说明n不存在时如何构造数组,同时算是临时构造了一个结构而不是一个type
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// Slice the slice to give it zero length.
s = s[:0]
printSlice(s)
// Extend its length.
s = s[:4]
printSlice(s)
// Drop its first two values.
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
这里使用了len和cap来表示切片的长度和容量。首先,数组和切片都是有长度的,而只有切片有容量,之所以出现了上面cap的奇怪结果,是因为cap表示的是切片的起始元素到其所引用的列表或切片最后一个元素的长度。
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
一个空列表是等价于nil的。这个nil后面应该会经常使用到。虽然数组的长度不能任意变化,但是切片的长度可以任意变化。
func main() {
a := make([]int, 5)
printSlice("a", a)
b := make([]int, 0, 5)
printSlice("b", b)
c := b[:2]
printSlice("c", c)
d := c[2:5]
printSlice("d", d)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}
上面的代码展示了如何使用make创建切片,其中make的三个参数,第一个参数为type、第二个为len、第三个为cap。相较于数组,go显然在切片上下了更大的功夫,这主要是由于slice是一个可变的容器,可以通过一些方法进行长度的增加或是减少
s = append(s, 2, 3, 4)
go的range类似python的enumerate
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
与enumerate不同的是,range是go中的一个关键字。
此外,也可以使用临时变量来取数据
func main() {
pow := make([]int, 10)
for i := range pow {
pow[i] = 1 << uint(i) // == 2**i
}
for _, value := range pow {
fmt.Printf("%d\n", value)
}
}
切片练习:
Implement Pic
. It should return a slice of length dy
, each element of which is a slice of dx
8-bit unsigned integers. When you run the program, it will display your picture, interpreting the integers as grayscale (well, bluescale) values.
The choice of image is up to you. Interesting functions include (x+y)/2
, x*y
, and x^y
.
(You need to use a loop to allocate each []uint8
inside the [][]uint8
.)
(Use uint8(intValue)
to convert between types.)
package main
import "golang.org/x/tour/pic"
func Pic(dx, dy int) [][]uint8 {
picture := make([][]uint8, dy)
for y := 0; y < dy; y++ {
picture[y] = make([]uint8, dx)
for x := 0; x < dx; x++ {
// Choose your desired function here
picture[y][x] = uint8((x + y) / 2) // (x * y) or (x ^ y)
}
}
return picture
}
func main() {
pic.Show(Pic)
}
这个练习一个很有趣的地方在于展示了go中二维数组的初始化有点类似C或C++,需要迭代的进行初始化。
接下来是字典,或者映射:
type Vertex struct {
Lat, Long float64
}
var m map[string]Vertex
func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
}
可以看出go的映射是map[T1]T2,同样也可以使用map构造,但是上述代码中初始构造出来的映射中,var构造出的为nil映射而make构造出来可以直接使用的映射
func main() {
m := make(map[string]int)
m["Answer"] = 42
fmt.Println("The value:", m["Answer"])
m["Answer"] = 48
fmt.Println("The value:", m["Answer"])
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])
v, ok := m["Answer"]
fmt.Println("The value:", v, "Present?", ok)
}
映射可以直接进行添加元素,除此之外, 可以通过delete进行元素的删除(注意不是面向对象操作),从映射取值时应该使用两个元素进行取值从而判断是否在其中(不使用第二个变量也行)
映射练习
Implement WordCount
. It should return a map of the counts of each “word” in the string s
. The wc.Test
function runs a test suite against the provided function and prints success or failure.
You might find strings.Fields helpful.
package main
import (
"golang.org/x/tour/wc"
"strings"
)
func WordCount(s string) map[string]int {
wordmap := make(map[string]int)
subwords := strings.Fields(s)
for _, v := range subwords{
word := string(v)
_, ok := wordmap[word]
if ok{
wordmap[word] += 1
} else{
wordmap[word] = 1
}
}
return wordmap
}
func main() {
wc.Test(WordCount)
}
go还有一些函数式的性质
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(5, 12))
fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}
上述代码将函数作为一个参数输入函数。
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
上述代码体现了go语言函数闭包的性质,sum变量即使在离开了adder函数之后仍然可以被使用。
函数闭包练习
Implement a fibonacci
function that returns a function (a closure) that returns successive fibonacci numbers (0, 1, 1, 2, 3, 5, …).
func fibonacci() func() int {
cur1 := 0
cur2 := 1
return func() int{
t := cur1 + cur2
cur1 = cur2
cur2 = t
return cur1
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
方法和接口
虽然go没有类,但是对应的实现了type的方法。
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())
}
go的方法使用了一种奇怪的设计来为不同的类别设计方法,即接收器。从上面的代码可以看到对于某一type的函数,首先需要使用接收器进行类的指明,之后才是函数的定义。而在使用时则是像绝大多数面向对象语言一样直接使用.func即可。在go中,方法就是函数+接收器。
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
func main() {
f := MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}
除了对结构体进行方法的定义,还可以通过使用type关键字”别名“原有类型并进行方法的绑定。要注意的是,你在哪个包里定义这个type,就只能在这个包里对方法使用接收器。
虽然在很早的地方就提到了指针,但是指针的具体用途一直不是很明确,在接收器这里详细体现了指针的用途:
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 (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())
}
当接收器使用指针时才能对原变量的值进行修改(未注释的部分)。上面代码还有一个有趣的地方在于,尽管一个方法的接收器可能是指针变量,但是go的编译器在推断时将v.Scale(5)
视为(&v).Scale(5)
。尽管可以通过类似的方法使得函数接收类作为参数从而实现其作为方法,但是go编译器无法将值变量和指针变量视为一体,从而导致错误
var v Vertex
fmt.Println(AbsFunc(v)) // OK
fmt.Println(AbsFunc(&v)) // Compile error!
相较于选择值接收器,选择指针接收器有两个好处:
- 可以对变量的属性值进行修改
- 可以在方法调用时避免参数变量的拷贝
在学会了对类型定义方法之后,接下来要了解的东西叫接口。接口类似于面向对象中的基类:
type Abser interface {
Abs() float64
}
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
v := Vertex{3, 4}
a = f // a MyFloat implements Abser
a = &v // a *Vertex implements Abser
// In the following line, v is a Vertex (not *Vertex)
// and does NOT implement Abser.
a = v
fmt.Println(a.Abs())
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
上述代码清楚的说明了一个接口的作用,代码定义了一个接口abser,其包含了Abs()方法,即包含了Abs()方法的类型均可以属于abser类型,之后定义了两个类型MyFloat和Vertex,这两个类型都具有Abs()方法,因此都可以作为abser类型。这里有一个比较烦人的细节,即由于未定义vertex值变量接收器的Abs方法,因此会提示:
cannot use v (variable of type Vertex) as Abser value in assignment
对于接口的实现是强调值变量或是指针变量的。
接口的内部实现是一个(值,类型)
元组,接口的值保存了底层对应类型的值,调用方法时会调用该类型的同名方法。
在使用接口的时候会遇到一些特殊情况,假设一个类型实现了接口的方法,但是该类型变量在初始化时的具体值为nil,根据前面的内容,会对nil调用该方法(在其他语言中可能因为类型问题而导致无法调用),但是对应的需要做异常处理:
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
func main() {
var i I
var t *T
i = t
describe(i)
i.M()
i = &T{"hello"}
describe(i)
i.M()
}
func describe(i I) {
fmt.Printf("(%v, %T)\n", i, i)
}
/* output
(<nil>, *main.T)
<nil>
(&{hello}, *main.T)
hello
*/
虽然接口可以看作是其它语言中的基类,但是与之不同的是,接口没有值也没有具体类型,对接口类型实例得到的元组为<nil,nil>
,因此会产生错误
type I interface {
M()
}
func main() {
var i I
describe(i)
i.M()
}
空接口是一种go的特殊用法
func main() {
var i interface{}
describe(i)
i = 42
describe(i)
i = "hello"
describe(i)
}
func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
不同的语言都有类似的实现,例如scala、c++中的any,但是对于go,其同样可以接受所有的数据类型,但是考虑到其速度并没有使用对应的类型快速,因此不需要在所有的场景使用空接口。
在提到空接口之后就要提到类型断言的内容了,类型断言将i
所具有的类型T
的值复制给对应的变量:
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // panic
fmt.Println(f)
}
第四组测试,在未使用ok接收另一个返回值时会提示一个panic,提示类型不对
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
得益于空接口的强大作用,可以实现一些基于不同类型参数的输入从而实现重载。go的Stringers巧妙地展示了如何使用接口进行方法的重载。
/*
type Stringer interface {
String() string
}
*/
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}
上面提供了Stringer接口类型,只要一个类型实现了String()方法就可以用于fmt.Println()
重载实验
Make the IPAddr
type implement fmt.Stringer
to print the address as a dotted quad.
For instance, IPAddr{1, 2, 3, 4}
should print as "1.2.3.4"
.
func (ip IPAddr) String() string{
return fmt.Sprintf("%d.%d.%v.%v",ip[0],ip[1],ip[2],ip[3])
}
// TODO: Add a "String() string" method to IPAddr.
func main() {
hosts := map[string]IPAddr{
"loopback": {127, 0, 0, 1},
"googleDNS": {8, 8, 8, 8},
}
for name, ip := range hosts {
fmt.Printf("%v: %v\n", name, ip)
}
}
另一个至关重要的重载方法是错误的重载方式
/*
type error interface {
Error() string
}
*/
type MyError struct {
When time.Time
What string
}
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}
func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}
自定义错误练习
Copy your Sqrt
function from the earlier exercise and modify it to return an error
value.
Sqrt
should return a non-nil error value when given a negative number, as it doesn’t support complex numbers.
Create a new type
type ErrNegativeSqrt float64
and make it an error
by giving it a
func (e ErrNegativeSqrt) Error() string
method such that ErrNegativeSqrt(-2).Error()
returns "cannot Sqrt negative number: -2"
.
Note: A call to fmt.Sprint(e)
inside the Error
method will send the program into an infinite loop. You can avoid this by converting e
first: fmt.Sprint(float64(e))
. Why?
Change your Sqrt
function to return an ErrNegativeSqrt
value when given a negative number.
type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string {
return fmt.Sprintf("cannot Sqrt negative numer: %v", float64(e))
}
func Sqrt(x float64) (float64, error) {
if x > 0 {
z := 1.0
for i := 0; i < 100; i += 1 {
z -= ((z*z - x) / (2 * z))
}
return z, nil
} else {
return 0, ErrNegativeSqrt(x)
}
}
func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}
本来在此还有对文件io、图片的相关方法重载,但是在此暂且先跳过。
最重要的部分 Goroutines
go自开发以来就是为了多线程而设计的,其有一个核心的关键字go
,通过go关键字可以轻松地开一个线程。
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
使用go function直接进行一个多线程的运行。
另一个特殊的东西是通道,有点类似一个先进先出的管道:
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to 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 // receive from c
fmt.Println(x, y, x+y)
}
尽管使用的是一个c,但是由于管道有先进先出的特定,因此可以依次收到数据并进行计算。通过make中构造,可以控制缓冲区的大小:
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
从上面的例子可以看出来,channel是一个类似切片的东西,因此也可以使用一些遍历方法进行访问:
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
time.Sleep(time.Second)
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
如果还固有线性的编程思维的话,上面的这段代码会很难理解。但是实际上Fibonacci函数和后面的输出是同时进行的,通过对c这一管道进行读取输出从而保证并发正常,妙
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
上面的代码演示了如何使用通道和 select
语句来实现一个同时处理 Fibonacci 数列生成和退出信号的示例。
在 main
函数中,我们创建了两个通道 c
和 quit
,分别用于传递 Fibonacci 数列的值和退出信号。
然后,我们启动了一个匿名的 goroutine,其中一个循环从通道 c
接收 Fibonacci 数列的值并打印出来,另一个循环从通道 quit
接收退出信号。
在 fibonacci
函数中,我们使用 for
循环和 select
语句不断地在 c
和 quit
之间进行选择。
- 当
c <- x
可以成功发送x
到通道c
时,我们更新x
和y
的值,继续生成下一个 Fibonacci 数,并将其发送到通道c
。 - 当
<-quit
可以接收到值时,说明主 goroutine 发送了退出信号到通道quit
,此时打印 “quit” 并返回,结束fibonacci
函数的执行。
整个过程中,main
函数和 fibonacci
函数是并发执行的。fibonacci
函数生成 Fibonacci 数列的值,并通过通道 c
发送出去,而 main
函数在一个 goroutine 中接收并打印这些值。当 main
函数发送退出信号到通道 quit
时,fibonacci
函数接收到该信号并退出,程序结束执行。
通过这种方式,我们实现了同时生成 Fibonacci 数列和接收退出信号的并发处理。
类似switch操作,如果case均不满足,则可以使用default操作:
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
一些小技巧
虽说相较于mutex,go更倾向于用户使用channel,但是mutex会遇到上锁解锁问题。这时候就要用到defer关键字了:
在加锁时就defer一个解锁在程序退出时运行,从而保证不会忘记关锁。