Go 语言入门5 – 接口

一、接口

1.1、定义接口

// notifier 是一个定义了通知类行为的接口
type notifier interface {
    // 接口方法
    notify()
}

1.2、实现接口

  • 使用值接收者实现接口
import "fmt"

// notify 是使用值接收者实现 notifier interface 接口的方法
// sendNotification(&u) 和 sendNotification(u) 都可
func (u user) notify() {
   fmt.Println("notify", u)
}

对于值接收者,sendNotification(&u) 和 sendNotification(u) 都可

  • 使用指针接收者实现接口
// notify 是使用指针接收者实现 notifier interface 接口的方法
// 只能使用 sendNotification(&u)
func (u *user) notify() {
    fmt.Println("notify", *u)
}

对于指针接收者,只能使用 sendNotification(&u)

非常重要的点 - 值接收者与指针接收的对比
https://stackoverflow.com/questions/27775376/value-receiver-vs-pointer-receiver-in-golang:详细的介绍了什么时候使用值接收者,什么时候使用指针接收者
https://studygolang.com/articles/1113
https://blog.csdn.net/suiban7403/article/details/78899671
type User struct {
    Name  string
    Email string
}
// 注意这个是值接收者
func (u User) Notify() error {
    u.Name = "alimon"
    return nil
}

func main() {
    u := &User{"Damon", "damon@xxoo.com"}
    u.Notify()
    log.Println(u.Name)
}

注意

  • 值接收者(func (u User) Notify() error)操作的是 User 的副本(不管调用者使用值还是地址),所以不会改变其内部的值,这里输出还是 Damon
  • 指针接收者(func (u *User) Notify() error)操作的是 User 本身,所以其内部的值会发生变化,这里输出是 alimon

什么时候使用值接收者,什么时候使用指针接收者

If the receiver is a map, func or chan, don’t use a pointer to it.
If the receiver is a slice and the method doesn’t reslice or reallocate the slice, don’t use a pointer to it.
If the method needs to mutate(使 receiver 改变) the receiver, the receiver must be a pointer.
If the receiver is a struct that contains a sync.Mutex or similar synchronizing field, the receiver must be a pointer to avoid copying.
If the receiver is a large struct or array, a pointer receiver is more efficient. How large is large? Assume it’s equivalent to passing all its elements as arguments to the method. If that feels too large, it’s also too large for the receiver.
Can function or methods, either concurrently or when called from this method, be mutating the receiver? A value type creates a copy of the receiver when the method is invoked, so outside updates will not be applied to this receiver. If changes must be visible in the original receiver, the receiver must be a pointer.
If the receiver is a struct, array or slice and any of its elements is a pointer to something that might be mutating, prefer a pointer receiver, as it will make the intention more clear to the reader.
If some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used
If the receiver is a small array or struct that is naturally a value type (for instance, something like the time.Time type), with no mutable fields and no pointers, or is just a simple basic type such as int or string, a value receiver makes sense.
A value receiver can reduce the amount of garbage that can be generated; if a value is passed to a value method, an on-stack copy can be used instead of allocating on the heap. (The compiler tries to be smart about avoiding this allocation, but it can’t always succeed.) Don’t choose a value receiver type for this reason without profiling first.

Finally, when in doubt, use a pointer receiver.

总结一下:

  • 如果需要改变 receiver 内部的属性值,选择指针接收者;
  • 如果 struct 中的一个方法使用了指针接收者,那么该 struct 内的全部方法都是用指针接收者 – 一致性

1.3、使用接口

// sendNotification 接受一个实现了 notifier 接口的值并发送通知
func sendNotification(n notifier) {
    n.notify()
}

func main() {
    u := user{"nana"}
    sendNotification(&u) // notify {nana}
}

二、实现多态

api.go

package api

// 定义接口
type Notifier interface {
    Notify()
}

接口名和接口方法大写表示 public,小写表示 java 的 default(即包隔离级别)

user.go

package impl

import "fmt"

// 定义实现类 user
type User struct {
    Name string
}

func (u *User) Notify() {
    fmt.Println("user", *u)
}

admin.go

package impl

import "fmt"

// 定义实现类 Admin,首字母大写表示 public
type Admin struct {
    Name string
}

func (a *Admin) Notify() {
    fmt.Println("admin", *a)
}

main.go

package main

import (
    // 相对于 GOPATH/src 下的地址
    "github.com/zhaojigang/helloworld/ooi/api"
    "github.com/zhaojigang/helloworld/ooi/impl"
)

func sendNotification(n api.Notifier) {
    n.Notify()
}

func main() {
    u := impl.User{"nana"}
    sendNotification(&u) // user {nana}

    a := impl.Admin{"zhao"}
    sendNotification(&a) // admin {zhao}
}

三、嵌入类型内部接口实现提升

基于上述程序修改 admin.go 和 main.go。

admin.go

package impl

// 定义实现类 Admin,首字母大写表示 public
type Admin struct {
    User // 嵌入类型
    Name string
}

注意:该类没有实现接口 notifier 的 notify() 方法,但是由于该类的内嵌类 User 实现了,内嵌类会提升到外部类中,所以 Admin 类也是 notifier 接口的实现类,其实现函数就是 User#Notify() ,当然,可以 Admin 可以自己实现 Notify() 来覆盖 User#Notify()。

main.go

package main

import (
    // 相对于 GOPATH/src 下的地址
    "github.com/zhaojigang/helloworld/ooi/api"
    "github.com/zhaojigang/helloworld/ooi/impl"
)

func sendNotification(n api.Notifier) {
    n.Notify()
}

func main() {
    u := impl.User{"nana"}
    sendNotification(&u) // user {nana}

    a := impl.Admin{u,"zhao"}
    sendNotification(&a) // user {nana}
}

四、组合接口

=========================== Api1 ===========================
package api

type Api1 interface {
    Api1()
}
=========================== Api2 ===========================
package api

type Api2 interface {
    Api2()
} 
=========================== Api12(组合接口) ===========================
package api

type Api12 interface {
    Api1
    Api2
}
=========================== Api12Impl ===========================
package impl

import "fmt"

type Api12Impl struct {
    Name string
}

func (api12 *Api12Impl) Api1()  {
    fmt.Println("api1", api12.Name)
}

func (api12 *Api12Impl) Api2()  {
    fmt.Println("api2", api12.Name)
}
=========================== main ===========================
package main

import (
    // 相对于 GOPATH/src 下的地址
    "github.com/zhaojigang/helloworld/ooi/api"
    "github.com/zhaojigang/helloworld/ooi/impl"
)

func sendNotification(n api.Api12) {
    n.Api1() // api1 nana
    n.Api2() // api2 nana
}

func main() {
    api12 := impl.Api12Impl{"nana"}
    sendNotification(&api12)
}

Go 语言入门4 – 对象

一、自定义结构

定义结构:type 结构名称 struct

*注:go语言中是没有类这回事,但是它使用了其他方式来充当类,比如一个文件xxx.go那么就属于一个xxx类。go语言对命名方式做了一点智能化处理,如果名字是以小写开头的则只能在包范围内访问,如果是以大写字母开头的,则属于公开,类似于java、php中的public。

1.1、定义结构

type User struct {
   id   int
   name string
}

type Admin struct {
   user User
   role string
}

1.2、赋值

var user User
user.id = 1
user.name = "alan"
fmt.Println(user)

lisa := User{2, "lisa"}
lily := User{id:2,name: "lily"}
fmt.Println(lisa)
fmt.Println(lily)

二、方法

  • 方法的定义方法实际上也是函数,只是在声明时,在关键字 func 和方法名之间增加了一个参数
    • 普通的函数定义 func 方法名(入参) 返回值
    • 自定义类型的方法定义 func (接收者) 方法名 (入参)  返回值
  • 方法的值传递和指针传递
    • func (u user) notify() 拷贝一份 user
    • func (u *user) changeEmail(newEmail string) 传递指针(即地址),内部改变会影响外部
//定义一个类型
type User struct {
   id   int
   name string
}

//定义3个方法
func (u User) getName() string {
   return u.name;
}

func (u *User) setName(name string) {
   u.name = name
}
func (u User) println() {
   fmt.Println(u)
}

//测试方法
func test3() {
   var user User
   user.id = 1
   user.name = "alan"
   fmt.Println(user.getName())// alan
   user.setName("Alan2")
   user.println()// 1 Alan2
}

注意:“再强调一次,println() 操作的是一个副本,只不过这次操作的是从 user 指针指向的值的副本。”

三、嵌入类型

  • Go 语言允许用户扩展或者修改已有类型的行为。这个功能是通过嵌入类型 type embedding 完成的。嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。
  • Java 通常可以通过继承或组合的方式实现嵌入类型要实现的功能。
  • “要嵌入一个类型,只需要声明这个类型的名字就可以了”,即 user 而不是 u user, u user 是声明字段
  • 内部类型的方法可以被提升到外部类型直接使用

*注:嵌入类型实际上就是go的多态,类似于java的继承。

//定义一个user基础类型
type User struct {
   id   int
   name string
}
//定义个admin基础类型
type Admin struct {
   user User
   role string
}
//定义一个admin2嵌入user类型
type Admin2 struct {
   User
   role string
}

//测试方法
func test4() {
   var admin Admin;//定义一个admin对象
   admin.user = User{id: 1, name: "alan"};
   admin.role = "Administrator"

   admin.setName("test") //错误,这里的admin并不会继承user中的方法

   admin2 := Admin2{role: "Administrator"};
   admin2.id = 1
   admin2.name = "alan"
   admin2.getName()//这里的admin2对象会自动继承user中的方法
   admin2.println()

}
//给User类型添加getName函数
func (u User) getName() string {
   return u.name;
}
//给User类型添加setName函数
func (u *User) setName(name string) {
   u.name = name
}
//给User类型添加println函数
func (u User) println() {
   fmt.Println(u)
}

Go语言入门3 – 容器

容器

  • Array 数组
  • Slice 切片(可以看成动态的数组)
  • Map 映射

一、Array 数组

  • [10]int 和 [20]int 是不同类型,数组类型相同要长度和元素类型完全相同才可以
  • func loopArray(arr2 [3]int) 是值传递,入参会拷贝一份数组,所以如果数组很大,从内存和性能上函数传递数组值都是很大的开销,需要避免(使用指针可以实现”引用传递” func loopArray(arr2 *[3]int),调用方传入 &arr2)
  • 在 Go 中一般不直接使用数组,而是使用切片
  • 数组是定长的,不可扩展,切片相当于动态数组
import "fmt"

func defineArray() [3]int {
   // 定义数组,不赋初值(使用默认值)
   var arr1 [5]int // [0 0 0 0 0]
   // 定义数组,赋初值
   arr2 := [3]int{1, 2, 3} // [1 2 3]
   // 定义数组,由编译器来计算长度,不可写成[],不带长度或者 ... 的表示切片
   arr3 := [...]int{4, 5, 6, 7} // [4 5 6 7]
   // 创建指针数组
   arr4 := [2]*string{new(string), new(string)}
   *arr4[0] = "hello"
   *arr4[1] = "go"
   // 为指定索引位置设置值
   arr5 := [3]int{1:10} // [0,10,0]
   // 二维数组
   var grid [4][5]int // [[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]
   // 数组拷贝,直接复制一份 arr2 给 arr6
   arr6 := arr2

   fmt.Println(arr1, arr2, arr3, arr4, arr5, arr6, grid)// arr4 打印出来的是地址 [0xc00000e1e0 0xc00000e1f0]
   fmt.Println(*arr4[0]) // hello
   return arr2
}

// 数组是值传递,这里的入参会拷贝一份数组(使用指针可以实现"引用传递")
func loopArray(arr2 [3]int) {
   // 通用方法
   for i := 0; i < len(arr2); i++ {
      fmt.Println(arr2[i])
   }
   // 最简方法,只获取数组下标
   for i := range arr2 {
      fmt.Println(arr2[i])
   }
   // 最简方法,获取数组下标和对应的值
   for i, v := range arr2 {
      fmt.Println(i, v)
   }
   // 最简方法,只获取值,使用 _ 省略变量
   for _, v := range arr2 {
      fmt.Println(v)
   }
}

二、Slice 切片

切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。

切片有 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)

5842684-3ed9eb450701d434

从切片 slice1 创建出来的切片 slice2,slice1 和 slice2 共享底层数组,一个修改了共享部分的元素,另一个也会感知

2.1、创建切片

// 1、使用make函数创建一个字符串切片,长度和容量都是5
slice1 := make([]string, 5)

// 2、创建一个int切片,长度是3,容量是5
slice2 := make([]int, 3, 5)

// 3、使用字面量创建切片,长度是3,容量是3
slice3 := []int{1, 2, 3} // [3]int{1, 2, 3}

// 4、创建 nil 切片,长度为0,容量为0
var slice4 []int

// 5、创建空切片,长度为0,容量为0
slice5 := make([]int, 0)
slice6 := []int{}

// 6、自定义底层数组,通过该底层数组创建切片
arr := [5]int{1, 2, 3, 4, 5}

// 数组转化为切片,左闭右开 [arr[2]~arr[4])
slice7 := arr[2:4] // [3,4]
slice8 := arr[2:]  // [3,4,5]
slice9 := arr[:4]  // [1,2,3,4]
slice10 := arr[:]   // [1,2,3,4,5]

实际上还有第七种创建切片的方式:根据切片创建切片,称为 reslice

2.2、切片使用

slice1 := []int{1, 2, 3, 4, 5}

// 1、根据索引获取切片元素
fmt.Println(slice1[1]) // 2

// 2、根据索引修改切片元素
slice1[3] = 400
fmt.Println(slice1) // [1, 2, 3, 400, 5]

// 3、根据切片创建切片,和根据自定义数组创建切片方式相同,长度是2=3-1,容量是4=5-1
// 但是需要格外注意,新生成的切片 slice2 和原始切片 slice1 的指针元素指向了相同的底层数组,所以修改元素要注意
slice2 := slice1[1:3] // [2, 3]
slice2[1] = 300
fmt.Println(slice2) // [2, 300]
fmt.Println(slice1) // [1, 2, 300, 400, 5] slice1也发生了变化

// 4、拷贝 slice 中的元素
fmt.Println("copy")
slice3 := []int{0, 0, 0, 0, 0}
slice4 := []int{1, 2, 3}
copy(slice3, slice4)
fmt.Println(slice3) // [1, 2, 3, 0, 0]
fmt.Println(slice4) // [1, 2, 3]    

// 5、删除 slice 中的元素,删除slice5[2]=3
fmt.Println("delete")
slice5 := []int{1, 2, 3, 4}
slice5 = append(slice5[:2], slice5[3:]...)
fmt.Println(slice5) // [1, 2, 4]

2.3、append 增加切片长度

5842684-081821049d9e13f3

// 1、创建原始切片,长度是5,容量是5
slice := []int{10, 20, 30, 40, 50}

// 2、reslice 新切片,长度是2,容量是4
newSlice := slice[1:3] // [20, 30]

// 由于底层数组还有容量,可以直接追加元素而容量不变
newSlice = append(newSlice, 60) // [20, 30 ,60] 长度是3,容量是4
fmt.Println(newSlice)           // [20, 30 ,60]
fmt.Println(slice)              // [10, 20, 30 ,60, 50]

// 长度4,容量4
slice := []int{10, 20, 30, 40}
// 此时切片容量用完了,再追加需要扩容,此处会新加数组,长度为原数组的2倍,即 newSlice 的底层数组是新数组,新切片容量为8;
// 而 slice 的底层数组是旧数组,二者互不影响
newSlice := append(slice, 50)
fmt.Println(slice)    // [10, 20, 30, 40]
fmt.Println(newSlice) // [10, 20, 30, 40, 50]
newSlice[0] = 100
fmt.Println(slice)    // [10, 20, 30, 40]
fmt.Println(newSlice) // [100, 20, 30, 40, 50]

这里是我自己的测试代码:

b := make([]string, 3, 5)
b[0] = "A"
b[1] = "B"
b[2] = "C"
//b[3]="C1"
//b[4]="C2"
b_1 := append(b, "D")
b_1 = append(b, "E")
fmt.Println("b:", b)
fmt.Println("b_1:", b_1)

b1 := []int{1, 2, 3, 4, 5}
var b2 []int
b3 := b1[0:2]
fmt.Println(b1)

b1[0] = 100

fmt.Println(b2)
fmt.Println(b3)
fmt.Println(&b1[0])
fmt.Println(&b3[0])
fmt.Println(len(b3))

fmt.Println("b1:", b1)
b1_1 := append(b1, 6);
b1_2 := append(b1_1, 7);
b1_3 := append(b1_2, 8);
b1_3 = append(b1_3, 9);
b1_3 = append(b1_3, 10);
b1_3 = append(b1_3, 11);
b1_3 = append(b1_3, 12);
b1_3 = append(b1_3, 13);
b1_3 = append(b1_3, 14);
b1_3 = append(b1_3, 15);
b1_3 = append(b1_3, 16);
b1_3 = append(b1_3, 17);
b1_3 = append(b1_3, 18);
b1_3 = append(b1_3, 19);
b1_3 = append(b1_3, 20);
b1_3 = append(b1_3, 21);
//b1_1 = append(b1, 9);
//b1_1 = append(b1, 10);

fmt.Println("b1_1:", b1_1)
fmt.Println("b1_2:", b1_2)
fmt.Println("b1_3:", b1_3)
fmt.Println("b1_3:", len(b1_3))
fmt.Println("b1_3:", cap(b1_3))

5842684-dc4d519b7d344b50

  • 当切片容量(而非数组长度,默认切片容量等于数组长度,也可以显示指定)用完了,再追加需要扩容,此处会新建数组,长度为原数组的2倍,然后将旧数组元素拷贝到新数组,newSlice 的底层数组是新数组,newSlice 容量为8;而 slice 的底层数组是旧数组,二者互不影响。
  • slice 扩容机制:在切片的容量小于 1000 个元素时,总是会成倍地增加容量。一旦元素个数超过 1000,容量的增长因子会设为 1.25,也就是会每次增加 25% 的容量。

2.4、显示设置容量

在没有显示指定容量的情况下,切片容量就是其底层数组的长度,如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续修改

source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 长度为1=3-2,容量为1=3-2  source[i:j:k] 长度=j-i 容量=k-i
slice := source[2:3:3]
fmt.Println(source) // ["Apple", "Orange", "Plum", "Banana", "Grape"]
fmt.Println(slice) // ["Plum"]
// 超出切片容量3,需要新建数组
slice = append(slice, "Kiwi")
fmt.Println(source) // ["Apple", "Orange", "Plum", "Banana", "Grape"]
fmt.Println(slice) // ["Plum", "Kiwi"]

2.5、合并切片

s1 := []int{1, 2}
s2 := []int{3, 4}
fmt.Println(append(s1,s2...)) // [1, 2, 3, 4]

2.6、迭代切片

  slice := []int{10, 20, 30, 40}
    // 与数组迭代一样,可以使用 for range + 普通 for 循环
    for index,value := range slice {
        fmt.Println(index, value)
    }

2.7、函数间传递切片

在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片成本也很低;而在函数间传递数组是需要拷贝整个数组的,所以内存和性能上都不好
调用函数,传递一个切片副本,实际上内部还是传递了对数组的指针,所以 foo 内部的操作会影响 main 中的 slice。

import "fmt"

func foo(slice []int) []int {
   slice[0] = 100
   return slice
}

func main() {
   // 1、创建一个 slice
   slice := []int{1, 2, 3, 4, 5}
   fmt.Println(slice) // [1, 2, 3, 4, 5]
   // 2、调用函数,传递一个切片副本,实际上内部还是传递了对数组的指针,
   // 所以 foo 内部的操作会影响 main 中的 slice
   slice2 := foo(slice)
   fmt.Println(slice2) // [100, 2, 3, 4, 5]
   fmt.Println(slice) // [100, 2, 3, 4, 5]
}

三、Map 映射

  • Map 是一个存储键值对的无序集合,就是说不保证顺序
  • Slice、Map、function 以及包含切片的结构类型不能作为 Map 的 key
  • map在函数间传递,不会拷贝一份map,相当于是”引用传递”,所以remove函数对传入的map的操作是会影响到main函数中的map的

3.1、基本用法

// 1、使用 make 创建 map,key为string,value为int
map1 := make(map[string]int)
// 2、使用字面量创建 map - 最常用的姿势,key为string,value为slice,初始值中的slice可以不加 []string 定义
map2 := map[string][]string{"hi": {"go", "c"}, "hello": []string{"java"}}
// 3、创建空映射
map3 := map[string]string{} // map3 := map[string]string nil映射
fmt.Println(map1, map2, map3)

// 4、向映射添加值
fmt.Println("map put")
map3["a"] = "x"
map3["b"] = "y"
fmt.Println(map3) // map[a:x b:y]

// 5、获取值并判断是否存在
value, exist := map3["c"]
if exist {
fmt.Println(value)
} else {
fmt.Println("map3[\"c\"] does not exist")
}

// 6、迭代
fmt.Println("iterat")
for key, value := range map3 {
fmt.Println(key, value)
}

// 7、从 map 中删除元素
delete(map3, "a")
fmt.Println(map3) // map[b:y]

以下是我的笔记代码:

m1 := make(map[string]string)
m1["a"] = "A"
m1["b"] = "B"
m1["c"] = "C"
m1["d"] = "D"
fmt.Println(m1)

m2 := map[int]int{}
m2[0] = 100
m2[1] = 100 * m2[0]
m2[2] = 100 * m2[0]
fmt.Println(m2)

parserS1(m1);
//delete(m1,"a")
v, m_1 := m1["a"]
if m_1 {
   fmt.Println("存在的", v)
} else {
   fmt.Println("不存在的", m_1)
}

for i, n := range m1 {
   fmt.Printf("me-> Key %s : %s \n", i, n)
}

3.2、函数间传递映射

import "fmt"

// map在函数间传递,不会拷贝一份map,相当于是"引用传递",所以remove函数对传入的map的操作是会影响到main函数中的map的
func remove(map4 map[int]int)  {
   delete(map4, 1)
}

func main() {
   map4 := map[int]int{0:0, 1:1, 2:2}
   fmt.Println(map4) // map[0:0 1:1 2:2]
   remove(map4)
   fmt.Println(map4) // map[0:0 2:2]
}

 

Go语言入门2 – 语法

一、变量定义

四种方式:

  • 完全体:var name type
  • 类型推断:var name = value
  • 最简体:name := value(仅用于函数内变量,包内变量不行)
  • 变量聚合定义:var( name1=value1 name2=value2 )
package main

import "fmt"

// 定义包内变量
var (
   aa = 1
   bb = "lanxin"
)

// 定义变量,只使用默认初值
func variableZeroValue()  {
   var a int       // 0
   var s string    // ""
   var b bool      // false
   fmt.Println(a, s, b)
}

// 定义变量,赋初值
func variableInitialValue()  {
   var a, b int = 1, 2
   b = 3
   var s string = "lanxin"
   fmt.Println(a, b, s)
}

// 类型推断
func variableTypeDeduction()  {
   var a, b, c, d  = 1, 2, true, "hi"
   fmt.Println(a, b, c, d)
}

// 最简定义变量方式
func variableShorter() {
   a, b, c, d := 3, 2, true, "hi"
   fmt.Println(a, b, c, d)
}

func main() {
   variableZeroValue()
   variableInitialValue()
   variableTypeDeduction()
   variableShorter()
   fmt.Println(aa, bb)
}

二、内建变量类型

8 类变量类型:

  • bool
  • string
  • (u)int、(u)int8、(u)int16、(u)int32、(u)int64
  • uintptr 指针
  • byte
  • rune 字符型,32 位,类比 char
  • float32、float64
  • complex32、complex64 复数 i = √-1

注意:类型转换必须强制转,转化成的类型可以不带小括号,如下:

// 强制类型转换
func triangle()  {
   var a, b int = 3, 4
   var c int
   // float64 和 int 可以不加小括号,也可以加上
   // 开方内建函数定义:func Sqrt(x float64) float64
   c = int(math.Sqrt(float64(a*a + b*b)))
   fmt.Println(c)
}

三、常量与枚举

  • 常量定义方式(常量必须有 value)
    • 完全体:const name type = value
    • 后续的使用自动补类型:const name = value
  • 枚举定义方式(Go 没有枚举,用 const 块来定义)
    • 自定义枚举:const ( name1=value1 name2=value2 )
    • iota 表达式枚举:const ( name1=iota表达式 name2 )
// 常量
func consts() {
   // 指定类型
   const filename string = "filename-const"
   // 不指定类型,表示类型不定
   const a, b = 3, 4
   var c int
   // 由于类型不定,所以这里不需要强转,如果定义为 const a, b int = 3, 4,则需要强转
   c = int(math.Sqrt(a*a + b*b))
   fmt.Println(filename, a, b, c)
}

// 枚举
func enums() {
   // 使用 const 块来实现枚举
   const (
      java = 0
      cpp  = 1
      c    = 2
   )
   fmt.Println(java, cpp, c) // 0 1 2
   // 使用 iota 块来实现自增枚举
   const (
      java1 = iota
      cpp1
      c1
   )
   fmt.Println(java1, cpp1, c1) // 0 1 2
}

 

四、条件语句

if:变量可以定义在 if 块内,其作用域就只在 if 块内。

package main

import (
   "fmt"
   "io/ioutil"
)

func readFile() {
   const filename = "abc.txt"
   // Go 函数可以返回两个值
   // func ReadFile(filename string) ([]byte, error)
   contents, err := ioutil.ReadFile(filename)
   if err != nil {
      fmt.Println(err)
   } else {
      // contents 是 []byte, 用%s 可以打印出来
      fmt.Printf("%s", contents)
   }
   // if 语句外部可访问
   fmt.Printf("%s", contents)
}

func readFileShorter() {
   const filename = "abc.txt"
   // Go 函数可以返回两个值
   // func ReadFile(filename string) ([]byte, error)

   if contents, err := ioutil.ReadFile(filename); err != nil {
      fmt.Println(err)
   } else {
      // contents 是 []byte, 用%s 可以打印出来
      fmt.Printf("%s", contents)
   }
   // if 语句外部不可访问
   //fmt.Printf("%s", contents) // 报错
}

从上边可以看出 Go 对文件操作也非常简单。

switch:默认自带break,如果想穿下去执行,使用 fallthrough

// switch 默认自带break,如果想穿下去执行,使用 fallthrough
func eval(a, b int, op string) int {
   var value int
   switch op {
   case "+":
      value = a + b
   case "-":
      value = a - b
   default:
      panic("unsupport operator" + op)
   }
   return value
}

五、循环

for:for 的三个组件都可省略,Go 没有 while,用 for 来替代

package main

import (
   "bufio"
   "fmt"
   "os"
)

func sum() int {
   var value int
   for i := 0; i <= 100; i++ {
      value += i
   }
   return value
}

// 等同于 while(true)
func deadLoop() {
   for {
      fmt.Println("this is a deadLoop")
   }
}

// Go 没有while,循环全部用 for,for的三个组件都可以省略
func printFile(filename string) {
   // 打开文件
   file, err := os.Open(filename)
   // 如果出错,结束进程
   if err != nil {
      panic(err)
   }
   // 获取读取器
   scanner := bufio.NewScanner(file)
   // 读取:It returns false when the scan stops, either by reaching the end of the input or an error
   for scanner.Scan() {
      fmt.Println(scanner.Text())
   }
}

六、函数

func 函数:

  • 可以有多个返回值
  • 函数的参数类型可以是 func – 函数式编程
  • 支持可变长参数
======================== 可以有多个返回值 =========================
// 函数可返回多个值
// 接收:q, r := div(10, 3)
// 如果只用其中一个值,另一个用下划线:q, _ := div(10, 3)
func div(a, b int) (int, int) {
   return a / b, a % b
}
// use
q, _ := div(10, 3)

======================== 函数的参数类型可以是 func =========================
// 可以使用函数作为参数,函数参数与内部参数一样,函数名在前,函数类型在后
// 后续传参,可以使用匿名内部函数,也可以先定义函数再传入
func apply(op func(int, int) int, a,b int) int {
   return op(a, b)
}
// use
result := apply(func(x int, y int) int {
   return x + y
}, 10,4)

======================== 支持可变长参数 =========================
// 可变长参数
func sum2(nums ...int) int {
   s := 0
   for i := range nums {
      s += nums[i]
   }
   return s
}
// use
sum2(1, 2, 3)

七、指针

Go 只有值传递,引用传递需要借助指针实现,引用传递实际上也是值传递,只是传递的是地址

// 值传递,函数参数拷贝了一份外界的 a, b
func swap_by_value(a, b int) {
   b, a = a, b
}
// use
a, b := 3, 4
swap_by_value(a, b)
fmt.Println(*(&a), b) // 3 4 没有实现交换

函数参数拷贝了一份外界的 a, b

// Go 只有值传递,想实现引用传递,使用指针
// *int 代表是指针类型,此时会将外界传入的 &a 拷贝给 这里的a,即这里的 a 拿到的是外界的 a 的地址
// 通过 *a,由于 a 是 &a,这里的 *a 相当于 *(&a) ,即从地址中取值
// 由于函数内部直接操作的是外界的 a,b 的内存地址,所以可以实现引用传递
func swap_by_pointer(a, b *int) {
   *b, *a = *a, *b
}
// use
swap_by_pointer(&a, &b)
fmt.Println(a, b)
*int 代表是指针类型,此时会将外界传入的 &a 拷贝给 这里的a,即这里的 a 拿到的是外界的 a 的地址
通过 *a,由于 a 是 &a,这里的 *a 相当于 *(&a) ,即从地址中取值
由于函数内部直接操作的是外界的 a,b 的内存地址,所以可以实现引用传递

 

Go语言入门1 – 环境搭建

官方文档:https://golang.org/doc/install,被墙了,所以国内打不开,可以在这个网站下载:https://studygolang.com/dl 。本文以 windows 自动安装版为例:

一、下载:https://studygolang.com/dl/golang/go1.12.4.windows-amd64.msi

安装就不需要说了吧。

➜ ~ go version
go version go1.12.4 windows/amd64

 

二、第一个 Go 程序

使用 GoLand 进行开发,IDE下载地址:https://www.jetbrains.com/go/

package main

import (
   "fmt"
)

func init() {
   fmt.Printf("init\n")
}

func main() {
   fmt.Printf("main\n")
}

运行两种方式:

使用 GoLand 直接 debug / run
执行 go build,然后执行生成的可执行文件 ./helloworld 即可

 
Copyright © 2008-2021 lanxinbase.com Rights Reserved. | 粤ICP备14086738号-3 |