Golang开发手册

@wintry大约 20 分钟

官方文档

☣️Go语言之旅(交互式教程-适合零基础人群)open in new window

☢️Golang标准库open in new window

💫Golang Web编程open in new window

环境配置

Windows下多平台交叉编译

Go Install工具应用

go install工具为公共代码仓库中维护的开源代码而设计。

无论是否公布代码,该模型设置工作环境的方法都是相同的。

Go代码必须放在工作空间内,其实就是一个目录,其中包含三个子目录:

✅src 目录包含Go的源文件,它们被组织成包(每个目录都对应一个包)

✅pkg 目录包含包对象

✅bin 目录包含可执行程序

go 工具用于构建源码包,生成二进制文件到 bin 目录中。

GOPATH 环境变量

GOPATH 环境变量指定了工作空间位置。

工作空间可以放在任何地方, 但是它绝对不能和Go安装目录相同。

首先创建一个工作空间目录,并设置相应的 GOPATH

$ mkdir $HOME/work
$ export GOPATH=$HOME/work

作为约定,请将此工作空间的 bin 子目录添加到你的 PATH

export PATH=$PATH:$GOPATH/bin

包路径

标准库中的包有给定的短路径,比如 "fmt""net/http"

自己的包必须选择一个基本路径,保证不和标准库和其它第三方库冲突。

github.com/user 作为基本路径。在工作空间里创建一个目录, 将源码存放到其中:

$ mkdir -p $GOPATH/src/github.com/wintrysec

Go mod

Go Module与 maven 类似是一个包管理工具,控制依赖包版本。

go mod init 项目名称 #初始化当前文件夹,创建go.mod文件
go mod tidy			#同步包状态,增加缺少的包删除无用的包
go mod verify		#校验依赖

流程控制

流程控制在编程语言中是最伟大的发明,有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。

Go中流程控制分三大类:条件判断,循环控制、无条件跳转

条件判断 if

Go的if还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了.

if integer := computedValue(); integer == 3 {
    fmt.Println("The integer is equal to 3")
} else if integer < 3 {
    fmt.Println("The integer is less than 3")
} else {
    fmt.Println("The integer is greater than 3")
}

无条件跳转 goto

goto跳转到必须在当前函数内定义的标签.

func myFunc() {
    i := 0
Here:   //这行的第一个词,以冒号结束作为标签(大小写敏感)
    println(i)
    i++
    goto Here   //跳转到Here去
}

循环控制 for

它即可以用来循环读取数据,又可以当作while来控制逻辑,还能迭代操作。

sum := 0;
    for index:=0; index < 10 ; index++ {
        sum += index
    }
fmt.Println("sum is equal to ", sum)

//index:=0 	在循环开始之前调用
//index++	在每轮循环结束之时调用

while的功能

其中;可以省略,就变成了While

sum := 1
for sum < 1000 {
    sum += sum
}

注意点

1)在循环里面有两个关键操作,break操作是跳出当前循环,continue是跳过本次循环。

2)可以使用_来丢弃不需要的返回值

for _, v := range map{
    fmt.Println("map's val:", v)
}

Switch

有些时候你需要写很多的if-else来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候switch就能很好的解决这个问题。

i := 10
switch i {
case 1:
    fmt.Println("i is equal to 1")
case 2, 3, 4:
    fmt.Println("i is equal to 2, 3 or 4")
case 10:
    fmt.Println("i is equal to 10")
default:
    fmt.Println("All I know is that i is an integer")
}

Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。

Struct

结构体可以更方便的访问数据,且支持匿名字段。

匿名字段能够实现字段的继承,所有的内置类型和自定义类型都是可以作为匿名字段。

type Human struct {
    name string
    age int
    phone string  // Human类型拥有的字段
}

type Employee struct {
    Human  // 匿名字段Human,不带数据类型
    speciality string
    phone string  // 雇员的phone字段
}

func main() {
    Bob := Employee{Human{name:"Bob", age:34, phone:"777-444-XXXX"}, speciality:"Designer", phone:"333-222"}
    
    //访问结构体数据
    fmt.Println("Bob's work phone is:", Bob.phone)
    // 如果我们要访问Human的phone字段
    fmt.Println("Bob's personal phone is:", Bob.Human.phone)
}

interface

Go语言里面设计最精妙的应该算interface,它让面向对象,内容组织实现非常的方便.

简单的说,interface是一组method签名的组合,我们通过interface来定义对象的一组行为。

空interface

空interface(interface{})不包含任何的method,它可以存储任意类型的数值。

// 定义a为空接口
var a interface{}
var i int = 5
s := "Hello world"
// a可以存储任意类型的数值
a = i
a = s

Comma-ok断言

断言可以直接判断是否是该类型的变量。

 for index, element := range list {
          if value, ok := element.(int); ok {
              fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
          } else if value, ok := element.(string); ok {
              fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
          }else {
              fmt.Printf("list[%d] is of a different type\n", index)
          }
      }

嵌入

如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。

type Interface interface {
    sort.Interface //嵌入字段sort.Interface
    Push(x interface{}) //a Push method to push elements into the heap
    Pop() interface{} //a Pop elements that pops elements from the heap
}

看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:

type Interface interface {
    // Len is the number of elements in the collection.
    Len() int
    // Less returns whether the element with index i should sort
    // before the element with index j.
    Less(i, j int) bool
    // Swap swaps the elements with indexes i and j.
    Swap(i, j int)
}

反射

所谓反射就是能检查程序在运行时的状态。我们一般用到的包是reflect包。

1)使用reflect一般分成三步,要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。

t := reflect.TypeOf(i)    //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i)   //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值

转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如

tag := t.Elem().Field(0).Tag  //获取定义在struct里面的标签
name := v.Elem().Field(0).String()  //获取存储在第一个字段里面的值

获取反射值能返回相应的类型和数值

var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

如果要修改相应的值,必须这样写

var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

字符串操作

fmt包

fmt包实现了类似C语言printf和scanf的格式化I/O。Print系列函数会将内容输出到系统的标准输出。

//直接输出内容
fmt.Print("在终端打印该信息")

//格式化输出字符串
fmt.Printf("Hello %s\n", "World")

//在输出内容的结尾添加一个换行符
fmt.Println("在终端打印单独一行显示")

//格式化字符串不输出,相当于变量
fmt.Sprintf("%s:%v", ip, port)

strings包

修剪字符串

x := "!!@@@你好!!,@@@ Gophers@@@!!"

//将字符串左侧和右侧中匹配 cutset 中的任一字符的字符去掉
fmt.Println(strings.Trim(x, "@!"))

//将字符串左侧中匹配 cutset 中的任一字符的字符去掉
fmt.Println(strings.TrimLeft(x, "@!"))

//将字符串右侧中匹配 cutset 中的任一字符的字符去掉
fmt.Println(strings.TrimRight(x, "@!"))


//输出
你好!!,@@@ Gophers
你好!!,@@@ Gophers@@@!!
!!@@@你好!!,@@@ Gophers

Contains

判断是否存在某个子字符串

str := "test1234helloak47888ak47886"

if strings.Contains(str, "hello") {
	fmt.Println("子字符串在 str变量中 返回true")
} else {
	fmt.Println("子字符串 不在 str变量中 返回false")
}

Count计算某个子字符串出现的次数

fmt.Println(strings.Count(str, "ak47"))

字符串分割

Split把字符串分割为字符串数组

fmt.Printf("%q\n", strings.Split("foo,bar,baz", ","))
fmt.Printf("%q\n", strings.SplitAfter("foo,bar,baz", ","))

//输出
["foo" "bar" "baz"]
["foo," "bar," "baz"]
//也就是说,Split 会将 s 中的 sep 去掉,而 SplitAfter 会保留 sep分隔符

fmt.Printf("%q\n", strings.SplitN("foo,bar,baz", ",", 2))
//带 N 的方法可以通过最后一个参数 n 控制返回的结果中的 slice 中的元素个数,最后一个元素不会分割
["foo" "bar,baz"]

字符串是否有某个前缀或后缀

//字符串是否以Go开头
fmt.Println(strings.HasPrefix("Gopher", "Go"))

//字符串是否以go结尾
fmt.Println(strings.HasSuffix("Amigo", "go"))

字符串 JOIN 操作

将字符串数组(或 slice)连接起来可以通过 Join 实现

fmt.Println(Join([]string{"name=xxx", "age=xx"}, "&"))
//输出 name=xxx&age=xx

将字符串重复几次

fmt.Println("ba" + strings.Repeat("na", 2))
//输出 banana

字符串字串替换

fmt.Println(strings.Replace("oink oink oink", "k", "ky", 2))
fmt.Println(strings.ReplaceAll("oink oink oink", "oink", "moo"))

//输出
oinky oinky oink
moo moo moo

大小写转换

strings.ToLower()
strings.ToUpper()

strconv包

strconv包实现了基本数据类型与其字符串表示的转换,主要有: Atoi()、Itia()、parse系列等

str1 := "100"

//将字符串类型的整数转换为int类型
myNum, err := strconv.Atoi(str1)

//将int类型数据转换为对应的字符串表示
num1 := 200
str2 := strconv.Itoa(num1)

Parse系列函数

Parse类函数用于转换字符串为给定类型的值:ParseBool()、ParseFloat()、ParseInt()、ParseUint()

文件读取性能

Go语言提供了多种读写文件的方式

1)使用os包中的Open()和Close()函数,配合Read()和Write()方法读写文件。

此方式较为底层,需手动管理文件的打开和关闭及数据的缓冲区,但具有很高的灵活性和效率。

2)使用bufio包中的NewReader()和NewWriter()方法读写文件。

此方式比较高层,会自动进行数据的缓存,可以大幅提升读写效率。

3)使用ioutil包中的ReadFile()和WriteFile()方法读写文件。

此方式非常简单,适合用于一次性读取或写入整个文件内容,但是可能会出现内存占用过高的问题。

下面是几种方式的性能对比(测试文件大小为100MB)

读写方式读取时间写入时间
os.Read() 和 os.Write()0.25s0.25s
bufio.NewReader()和 bufio.NewWriter()0.1s0.1s
ioutil.ReadFile()和 ioutil.WriteFile() 已经弃用0.6s0.6s

可以看出,使用bufio包中的方法读写文件的性能最佳,而使用ioutil包中的方法读写文件的性能最差。但是需要注意的是,性能测试结果可能因系统环境、文件大小、文件类型等因素而有所不同,具体使用时还需要进行实际测试。

文件写入操作方式上的差异

1)打开文件,写入内容,关闭文件。如此重复多次

2)打开文件,写入内容,defer 关闭文件。如此重复多次

3)打开文件,重复多次写入内容,defer 关闭文件

第一种慢但是稳定,第二种压栈太多导致崩溃报错,第三种速度快减少了很多打开关闭文件的操作

bufio读写

读取

var Usernames []string

//打开文件
file, err := os.OpenFile(filepath, os.O_RDONLY, 0666)
if err != nil {
    return nil
}
defer file.Close()

//创建读取器
reader := bufio.NewReader(file)

for {
    line, _, err := reader.ReadLine()
    
    if err == io.EOF {
        break
    }
    Usernames = append(Usernames, string(line))
}

写入

// 打开文件以进行写入
Logpath := "文件路径"
file, err := os.OpenFile(Logpath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
    fmt.Printf("Open File failed for %v. Error: %s", Logpath, err.Error())
}
defer file.Close()

// 创建一个新的 writer 对象
writer := bufio.NewWriter(file)

// 写入字符串
_, err = writer.WriteString("text Content")
if err != nil {
    fmt.Printf("Writer failed %v. Error: %s", Logpath, err.Error())
}

// 将缓冲区中的数据刷新到磁盘中
err = writer.Flush()
if err != nil {
    fmt.Println(err)
}

HTTP客户端

GET请求

func main() {
	url := "https://www.baidu.com/"
    
    //请求参数设置
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("Connection", "close")
	req.Header.Set("Cookie", "rememberMe=1")

	//设置http客户端参数
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //忽略https验证
	}
	client := &http.Client{
		Transport: tr,
		Timeout:   time.Duration(8) * time.Second, //设置超时连接
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse /* 不进入重定向 */
		},
	}

    //发送请求
	resp, err := client.Do(req)
	if err != nil {
		// handle errorcls
	}
    //关闭网络连接
	defer resp.Body.Close()
    
    //读取HTTP响应体
	body, _ := io.ReadAll(resp.Body)
	fmt.Printf(string(body))
}

POST请求

基本的客户端设置都和GET方法一样

发送JSON数据

//要发送的JSON数据
data := map[string]interface{}{
    "query":   domain, //替换为域名
    "fields":  []string{"parsed.names", "parsed.extensions.subject_alt_name.dns_names"},
	"flatten": true,
}

//JSON序列化
jsonData, err := json.Marshal(data)
if err != nil {
    panic(err)
}

// 请求参数设置
req, _ := http.NewRequest("POST", URL, bytes.NewBuffer(jsonData))
req.SetBasicAuth(apiID, apiSecret) //Basic认证
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

发送form表单数据

//数据
payload := url.Values{}
payload.Set("foo", "a")
payload.Add("foo", "b")
payload.Set("foo2", "c")

// 请求参数设置
req, _ := http.NewRequest("POST", URL, strings.NewReader(payload.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

JSON解析

json.NewDecoder 和 json.Unmarshal 都可以将 JSON 数据解码为 Go 中的结构体。

解析到interface

如果我们不知道被解析的数据的格式,又应该如何来解析呢?

JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。

Go类型和JSON类型的对应关系如下:

  • bool 代表 JSON booleans,
  • float64 代表 JSON numbers,
  • string 代表 JSON strings,
  • nil 代表 JSON null.

目前bitly公司开源了一个叫做simplejson的包,在处理未知结构体的JSON时相当方便

js, err := NewJson([]byte(`{
    "test": {
        "array": [1, "2", 3],
        "int": 10,
        "float": 5.150,
        "bignum": 9223372036854775807,
        "string": "simplejson",
        "bool": true
    }
}`))

arr, _ := js.Get("test").Get("array").Array()
i, _ := js.Get("test").Get("int").Int()
ms := js.Get("test").Get("string").MustString()

https://github.com/bitly/go-simplejsonopen in new window

高并发

Golang并发解决方案goroutine类比协程。协程在内存消耗和切换调度开销都远比线程小。

goroutine有以下特点

1)轻量级:创建和销毁非常快速,每个 goroutine 只需要几 KB 的内存,因此可同时创建数百万个

2)并发安全:goroutine 本质上是协作式调度的,而非抢占式调度,因此不会发生数据竞争问题

3)通信机制:goroutine 之间可通过通道(channel)进行通信,高效且安全,可以避免锁的使用

一个简单的高并发端口扫描示例
package main

import (
	"fmt"
	"net"
	"sync"
	"time"
)

// 定义主机地址结构体
type Addr struct {
	ip   string
	port int
}

// 定义要扫描的IP地址和端口号
var (
	ips        = []string{"172.16.51.221", "172.16.51.18", "172.16.50.1", "172.16.50.4"}
	ports      = []int{80, 443, 445, 8080}
	AliveHosts []string     //存活服务结果存放数组
	timeout    int64    = 5 //连接超时时间
)

// 定义协程数量
const maxGoroutines = 50

// 定义等待组
var wg sync.WaitGroup

func main() {
	// 初始化通道,必须使用make 创建channel通道
	Addrs := make(chan Addr, len(ips)*len(ports))
	results := make(chan string, len(ips)*len(ports))
    //定义一个channel时,也需要定义发送到channel的值的类型。

	//接收结果
	go func() {
		for res := range results {
			fmt.Println(res)
			wg.Done() //收到结果,计数器减一
		}
	}()

	// 启动协程
	for i := 0; i < maxGoroutines; i++ {
		go func() {
			for addr := range Addrs {
				PortConnect(addr, results, timeout, &wg)
				wg.Done() //完成一个任务,计数器减一
			}
		}()
	}

	// 向通道中添加任务(添加扫描目标)
	for _, ip := range ips {
		for _, port := range ports {
			wg.Add(1) //添加一个任务,计数器加一
			Addrs <- Addr{ip, port} //channel通过操作符<-来接收和发送数据
		}
	}

	// 等待所有协程完成
	wg.Wait()

	// 关闭通道,必须在wg.Wait之后
	close(Addrs)
	close(results)

}

// TCP Connect-端口连接测试
func PortConnect(addr Addr, res chan<- string, timeout int64, wg *sync.WaitGroup) {
	ip, port := addr.ip, addr.port
	conn, err := net.DialTimeout("tcp4", fmt.Sprintf("%s:%v", ip, port), time.Duration(timeout)*time.Second)

	//关闭网络连接
	defer func() {
		if conn != nil {
			conn.Close()
		}
	}()

	if err == nil {
		//产出结果,计数器加一
		wg.Add(1)
		res <- fmt.Sprintf("%s:%v", ip, port)
	}
}

推荐阅读下面的这篇文章来理解Golang的高并发

GMP 并发调度器深度解析之手撸一个高性能 goroutine poolopen in new window

runtime包中有几个处理goroutine的函数

  • Goexit

    退出当前执行的goroutine,但是defer函数还会继续调用

  • Gosched

    让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。

  • NumCPU

    返回 CPU 核数量

  • NumGoroutine

    返回正在执行和排队的任务总数

  • GOMAXPROCS

    用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

异常处理

Golang通过panic抛出异常,然后在defer中,通过recover捕获异常并处理。

panic 的中文翻译为 恐慌,当程序遇到错误的时候就会恐慌造成程序崩溃!

defer语句将一个函数推迟到当前函数返回前执行。

package main

import (
	"errors"
	"fmt"
	"math"
)

func GetBallVolumn(radius float64) (vol float64, err error) {
	if radius < 0 {
		//直接panic程序不会崩溃,但是后边的程序语句不会再执行
		panic("小球半径不能为负数(From GetBallVolumn Func)")
	}
    
    //一种温和的错误提示,而不是直接 panic 让程序崩溃掉
    //定义错误信息,如果存在则直接return
	if radius < 5 || radius > 50 {
		err = errors.New("半径的合法范围是5~50")
		return 0, err
	}
	return (4 / 3.0) * math.Pi * math.Pow(radius, 3), nil //没错误err返回nil
}

func main() {

	//在函数结束之前处理异常
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("严重错误警告: ", err)
		}
	}()

	volumn, err := GetBallVolumn(2)

	//如果存在错误则提示错误信息
	if err != nil {
		fmt.Println("获取体积失败,err=", err)
	}

	fmt.Println("小球的体积是:", volumn)
	fmt.Println("程序运行结束")
}

函数-穿指针

  • 传指针使得多个函数能操作同一个对象。
  • 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
  • Go语言中channelslicemap这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)
//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
    *a = *a+1 // 修改了a的值
    return *a // 返回新值
}

func main() {
    x := 3

    fmt.Println("x = ", x)  // 应该输出 "x = 3"

    x1 := add1(&x)  // 调用 add1(&x) 传x的地址

    fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
    fmt.Println("x = ", x)    // 应该输出 "x = 4"
}

匿名函数和defer

匿名函数和defer语句不能乱用。

匿名函数

匿名函数是一种没有名称的函数,它可以在代码中直接定义和使用。

匿名函数可以在函数内部定义,也可以作为函数参数或返回值。

匿名函数的副作用主要有以下几点

1)可能会导致内存泄漏:匿名函数中使用外部变量时,会形成闭包,如果外部变量一直被引用,那么它所占用的内存就不会被释放,从而导致内存泄漏。

2)可能会导致并发问题:匿名函数中使用外部变量时,如果多个协程同时访问这个变量,可能会引发并发问题,如竞态条件等。

3)可能会影响代码可读性:过度使用匿名函数会使代码变得难以阅读和理解,从而降低代码的可维护性和可扩展性。

如何合理应用匿名函数

1)可以作为函数参数或返回值,这种方式可以使代码更加灵活和可扩展

2)可以作为协程的执行体,这种方式可以方便地实现并发编程

3)可以形成闭包,访问外部变量,从而实现一些有用的功能,如函数柯里化、延迟执行等

defer语句

defer语句可以用于在函数返回时执行某个操作,无论函数是正常返回还是发生了异常。

过度使用defer语句的危害

当一个函数中存在大量的defer语句时,这些语句会被添加到函数的调用栈中,并在函数返回时按照相反的顺序执行,这可能会导致函数的运行速度变慢,尤其是在函数被频繁调用的情况下。

defer使用场景

1)进行资源的释放:如文件句柄、网络连接、数据库连接等资源的释放。

2)锁的释放:如互斥锁、读写锁等锁的释放。

3)延迟执行某个操作:如对某个函数的延迟调用,或者对某个操作的延迟执行。

关于retrun

在函数没有返回值的情况下是没有必要的。

如果在函数中遇到了某些错误,你可能希望立即返回,而不是继续执行后面的代码。

在这种情况下,可以使用return语句来提前结束函数的执行。

泛型

泛型编程是一种计算机编程风格,编程范式,其中算法是根据稍后指定的类型编写的,然后在需要时为作为参数提供的特定类型实例化。

泛型有以下特点

1)类型的参数化:把类型当做函数参数传递。

2)更强的类型检查:可以在编译期间对类型进行检查,减少运行时由于对象类型不匹配引发的异常

3)代码节省与抽象呈现

示例如下

package main

import "fmt"

// 声明类型约束(整型和浮点型)
type Number interface {
	int64 | float64
}

// 通用型函数,自动判断参数的类型
// 在[]括号内,声明两个类型参数K和V,以及一个使用类型参数的参数m map[K]V
func SumIntsOrFloats[K comparable, V Number](m map[K]V) V {
	var s V //声明的s变量可能是整型 也可能是浮点型
	for _, v := range m {
		s += v
	}
	return s
}

func main() {
	// Initialize a map for the integer values
	ints := map[string]int64{
		"first":  34,
		"second": 12,
	}

	// Initialize a map for the float values
	floats := map[string]float64{
		"first":  35.98,
		"second": 26.99,
	}

	//引用泛型函数,自动判断参数类型
	fmt.Printf("Generic Sums: %v and %v\n",
		SumIntsOrFloats(ints),
		SumIntsOrFloats(floats))
}