go的基础语法以及常用特性
输出
func main() {
fmt.Println("Hello, 世界")
}
随机数
这里通过运行代码会发现生成的随机数总是一样的
func main() {
fmt.Println("My favorite number is", rand.Intn(10))
}
rand包实现的是伪随机数生成器,需要先对随机数种子进行初始化,为了保证每次运行的随机性,通常在对于种子的sourse初始化用时间戳
rand.Seed(time.Now().UnixNano()) //随机数种子
rand.Intn()代表的是rand的取值范围
下面是一个随机数的猜数字游戏
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano()) //随机数种子
se := rand.Intn(maxNum)
fmt.Println(se)
fmt.Println("输入数字")
reader := bufio.NewReader(os.Stdin)
for {
input, err := reader.ReadString('\n') // 读取一行输入
if err != nil {
fmt.Println(err)
return
continue
}
//input = strings.TrimSuffix(input, "\n") // 去掉换行符
input = strings.TrimSpace(input) // 这个是去除字符串前后的空白字符 上面那个只能去除换行符 但是输入的时候会敲回车 导致/r 的出现 会报错
guess, err := strconv.Atoi(input) //转换字符串成数字
if err != nil {
fmt.Println(err)
return
continue
}
fmt.Println("ur guess is", guess)
if guess > se {
fmt.Println("large")
} else if guess < se {
fmt.Println("small")
} else {
fmt.Println("corret !")
break
}
}
函数
函数可以没有参数或者接受多个参数,同时在go语言里面函数可以同时返回多个值
接受参数
func add(x int, y int) int {
return x + y
}
func main() {
fmt.Println(add(42, 13))
}
注意返回类型以及变量类型后置的写法
有多个类型同时赋值的时候,可以这样写
x int, y int -> x, y int
返回参数
函数可以返回任意数量的值,可以这样定义
func swap(x, y string) (string, string) {
return y, x
}
函数命名
- 函数名称命名
函数名称小写代表函数私有,大写代表函数公有,公有的时候,可以被别的函数跨包调用
- 函数返回值命名
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
没有参数的return会直接返回,仅用于短的函数,长的函数会影响代码的可读性
变量
变量的初始化
在这个代码中展示了两种变量赋值的方法,就是创建和赋值,以及创建并赋值
注意go里面变量创建就必须要赋值并使用,否则会报错
var x, y int
x = 1
y = 2
z := 3
类型转换
go里面类型转换需要显式转换
var f float64 = math.Sqrt(float64(x*x + y*y))
常量
用const关键字
const Pi = 3.14
循环
for循环
- 一般for结构 go里面只有for循环结构,下面是一个例子
for i := 0; i < 10; i++ {
sum += i
}
go语言中没有条件括号,但是必须有花括号,其他的用法与cpp相同
- for的缺省结构
可以用这种写法替代while
sum := 1
for sum < 1000 {
sum += sum
}
判断
if
Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。
if x < 0 {
return sqrt(-x) + "i"
}
- if 的简短语句
通常可以在if的条件判断语句执行之前,执行一段简短的语句,该语句声明的变量作用域仅在 if 之内。
if value, err = judge(key); err!=nil {
fmt.Error("%v", err)
}
- if-else语句
语法与c++, java类似
swich
go中的switch语句不限制参数的类型,因此也可以替代if,以及if else语句,
同时switch中不需要加break因为go中执行了相应的case之后会跳出switch
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}
注意 switch中代替if的写法,不带switch
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
defer
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
这是一个栈式调用,通常用于关闭流
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
指针
Go 拥有指针。指针保存了值的内存地址。
go的指针定义与cpp非常相似
类型 *T 是指向 T 类型值的指针。其零值为 nil。
var p *int
& 操作符会生成一个指向其操作数的指针。
i := 42
p = &i
- 操作符表示指针指向的底层值。
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i
这也就是通常所说的“间接引用”或“重定向”。
与 C 不同,Go 没有指针运算。
结构体
定义
结构体与java类比 有点类似于实体类与对象
下面是一个简单例子
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2} //定义实例
v.X = 4 //访问字段
fmt.Println(v.X)
}
结构体指针
在这一段代码中,我之前的疑惑时p时结构体v的指针,
如果要访问v的内存,按照c++的写法时需要*p来访问,但是这么写会报错
查询资料后,发现,go语言中对于结构体指针的使用,允许隐式调用,也就是p.x等价于*p.x
原因是go中没有指针运算,一直写*p.x过于啰嗦
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)
}
结构体指针的实际应用
在函数中对于大的结构体进行调用的时候,直接用应用类型,会导致执行过程中会对结构体进行复制
非常消耗内存,此时可以用结构体指针进行传参,这样可以节省性能消耗
结构体实例化
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)
func main() {
fmt.Println(v1, p, v2, v3)
}
运行结果:
{1 2} &{1 2} {1 0} {0 0}
结构体方法
由于go中没有类,但是可以写结构体方法,未结构体加上方法
方法就是一类带特殊的 接收者 参数的函数。
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
} // 指针接收者 可以直接修改实例的值 更常用
而以指针为接收者的方法被调用时,接收者既能为值又能为指针:
var v Vertex
v.Scale(5) // OK
p := &v
p.Scale(10) // OK
- 选择值或指针作为接收者
使用指针接收者的原因有二:
首先,方法能够修改其接收者指向的值。
其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。
在本例中,Scale 和 Abs 接收者的类型为 *Vertex,即便 Abs 并不需要修改其接收者。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。
所以对于结构体方法的调用 我们通常先取地址 然后才调用函数
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)
}
数组
类型后置,其余一样
切片
定义
切片与数组相比,长度更加灵活,不受限制
切片并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
与它共享底层数组的切片都会观测到这些修改,共享内存
a := [4]int{1, 2, 3 ,4}
a[low, high]
左闭右开
切片写法
下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:
[]bool{true, true, false}
切片默认行为
在进行切片时,你可以利用它的默认行为来忽略上下界。
切片下界的默认值为 0,上界则是该切片的长度。
对于数组
var a [10]int
来说,以下切片是等价的:
a[0:10]
a[:10]
a[0:]
a[:]
切片与make实现动态数组
a := make([]int, 5) // len(a)=5
append() 向切片追加元素
使用append追加元素的时候,如果追加元素之后,超过了元素的长度,会将切片重新分配地址
由于我们常用空切片 ,因此在追加元素之后都会把切片赋值回去
// 添加一个空切片
s = append(s, 0)
printSlice(s)
// 这个切片会按需增长
s = append(s, 1)
printSlice(s)
// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)
map
定义
map[key]value
var m = map[string]int{
"a":1,
"b":2
}
创建
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
这里的Vertex也可以省略
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
修改map
在映射 m 中插入或修改元素:
m[key] = elem
获取元素:
elem = m[key]
删除元素:
delete(m, key)
通过双赋值检测某个键是否存在:
elem, ok = m[key]
若 key 在 m 中,ok 为 true ;否则,ok 为 false。
若 key 不在映射中,那么 elem 是该映射元素类型的零值。
同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。
注 :若 elem 或 ok 还未声明,你可以使用短变量声明:
elem, ok := m[key]
range遍历
- 遍历数组
返回两个参数,第一个参数是索引,第二个参数是value,如果不想要索引,可以用_替代
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)
}
}
- 遍历map
for key, value := range mapName {
xxxx
}
接口
接口类型 是由一组方法签名定义的集合。
type Abser interface {
Abs() float64
}
接口的实现
go中分为显式实现与隐式实现,而隐式实现更为常见
首先定义接口
type sleeper interface {
sleep()
}
然后定义两个结构体
type dog struct {
name string
}
type cat struct {
name string
}
定义结构体实现的接口的方法
其实就是把名字省去,然后名字一样的方法
func (d *dog) sleep() {
fmt.Printf("dog %s is sleeping", d.name)
}
func (c *cat) sleep() {
fmt.Printf("cat %s is sleeping", c.name)
}
接口同样是一种数据类型 类似于 java 的声明
通过对接口的赋值(实现了接口的结构体)
通过调用接口的方法 实现 Java 里面的上转型
func main() {
var s sleeper
dog := dog{"dd"}
cat := cat{"cc"}
s = &dog
s.sleep() // 实现多态 go会根据类型选择调用哪一个结构体的方法
s = &cat
s.sleep() // 实现多态 go会根据类型选择调用哪一个结构体的方法
}
- 还可以调用接口切片,然后循环赋值,调用方法
func main() {
food := "apple"
for _, i := range []lazy{&dog{"dd"}, &cat{"cc"}} {
i.eat(food)
i.sleep()
}
}
接口嵌套
首先定义接口
type sleeper interface {
sleep()
}
type eater interface {
eat(eat string)
}
type lazy interface {
sleeper
eater
}
然后实现方法
注意 结构体需要实现所有的方法才能够实现该接口 才能进行赋值
type dog struct {
name string
}
type cat struct {
name string
}
func (d *dog) sleep() {
fmt.Printf("dog %s is sleeping", d.name)
}
func (d *dog) eat(food string) {
fmt.Println(food + d.name)
}
func (c *cat) sleep() {
fmt.Printf("cat %s is sleeping", c.name)
}
func (c *cat) eat(food string) {
fmt.Println(food + c.name)
}
最后用切片 遍历赋值接口 实现多态
func main() {
food := "apple"
for _, i := range []lazy{&dog{"dd"}, &cat{"cc"}} {
i.eat(food)
i.sleep()
}
}
接口断言-检查接口被赋值的是哪个结构体
获取接口值得真正类型
dog, ok = s.(dog)
if ok {
fmt.println("is dog")
}
泛型
泛型是通过空接口实现的,所有的结构体都实现了空接口,因此可以通过对空接口进行赋值
空接口可以被任何的结构体赋值 所以这里就实现了泛型
var T interface{} // 空接口
fmt包就使用了泛型