golang WEB应用【2】:json数据处理应用

json数据处理应用

JSON格式简介

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它基于ECMAScript(欧洲计算机制造商协会指定的一种脚本程序标准)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得JSON成为理想的数据交换语言。它易于人阅读和编写,同时也易于机器解析和生成

JSON格式是键/值对的集合。在不同的编程语言中,这些键/值对被理解为对象(object)纪录(record)结构(struct)字典(dictionary)哈希表(hash table)有键列表(keyed list)或者关联数组(associative array

golang 中主要理解为结构(struct)接口(interface)

JSON的语法规则包括:

  • 数据在名称/值对中
  • 数据由逗号分隔
  • 花括号保存对象
  • 方括号保存数组

JSON数据的结构有两种:

  1. 对象(Object):一个无序的“‘名称/值’对”集合。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个名称后跟一个“:”(冒号);“‘名称/值’对”之间使用“,”(逗号)分隔。
  2. 数组(Array):值的有序集合。一个数组以"[“(左中括号)开始,”]"(右中括号)结束。值之间使用“,”(逗号)分隔。

JSON的值可以是以下几种类型:

  • 字符串(String):以双引号包围的Unicode字符序列。
  • 数值(Number):整数或浮点数。
  • 对象(Object):上面描述的JSON对象。
  • 数组(Array):上面描述的JSON数组。
  • 布尔值(Boolean):true或false。
  • 空值(Null):表示一个空值。

示例:

{
  "name": "张三",
  "age": 30,
  "isMarried": false,
  "children": null,
  "hobbies": ["reading", "traveling", "sports"],
  "address": {
    "city": "北京",
    "street": "长安街",
    "postcode": "100000"
  }
}

在这个例子中,我们定义了一个包含多种类型值的对象,其中包含了姓名、年龄、婚姻状态、爱好、子女和地址等信息

Go语言中的JSON序列化和反序列化

在Go语言中,encoding/json 标准库提供了JSON序列化和反序列化的功能。序列化(Serialization)是指将Go语言中的结构体(struct)或其他数据类型转换为JSON格式的字符串,而反序列化(Deserialization)则是将JSON格式的字符串转换回Go语言中的结构体或其他数据类型。

JSON序列化

序列化使用 json.Marshal 函数将Go语言的数据结构转换为JSON格式的字节切片(byte slice)

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Person struct {
	Name    string `json:"name"`
	Age     int    `json:"age"`
	Email   string `json:"email"`
	Address string `json:"address,omitempty"` // omitempty 表示如果字段为空(默认值),则不包含在JSON中
}

func main() {
	p := Person{
		Name:    "张三",
		Age:     30,
		Email:   "zhangsan@example.com",
		Address: "",
	}

	// 序列化
	data, err := json.Marshal(p)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(data)) // 输出:{"name":"张三","age":30,"email":"zhangsan@example.com"}
}

在上面的例子中,我们定义了一个 Person 结构体,并使用结构体标签(struct tags)来指定JSON字段的名称。json.Marshal 函数将 Person 实例序列化为JSON格式的字节切片,然后将其转换为字符串并打印出来。

JSON反序列化

反序列化使用 json.Unmarshal 函数将JSON格式的字节切片转换为Go语言的数据结构。

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

type Person struct {
	Name    string `json:"name"`
	Age     int    `json:"age"`
	Email   string `json:"email"`
	Address string `json:"address,omitempty"`
}

func main() {
	jsonData := []byte(`{
		"name": "张三",
		"age": 30,
		"email": "zhangsan@example.com"
	}`)

	var p Person

	// 反序列化
	err := json.Unmarshal(jsonData, &p)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", p) // 输出:{Name:张三 Age:30 Email:zhangsan@example.com Address:}
}

在这个例子中,我们定义了一个包含JSON字符串的 jsonData 变量,并使用 json.Unmarshal 函数将其反序列化为 Person 结构体的实例。反序列化成功后,我们可以打印出结构体的内容

注意事项

  • 在结构体标签中,可以使用 omitempty 选项来忽略空值字段。
  • 结构体中的字段名必须是大写开头的,否则在序列化和反序列化时不会被识别。
  • 如果JSON中的字段名与结构体中的字段名不一致,可以使用结构体标签来映射字段名。
  • 序列化和反序列化过程中可能会遇到多种错误,例如数据类型不匹配、字段不存在等,需要对这些错误进行处理。

JSON的映射

在上面的序列化和反序列化的例子当中,可以看到需要定义一些规则来指定如何将Go的结构体字段映射到JSON的键,以及如何将JSON的键映射回Go的结构体字段。Go在JSON的映射上,一般采用结构体(struct)接口(interface{})来进行映射,接下来会分别介绍

结构体映射

结构体在处理JSON的映射过程中,一般采用结构体标签(Struct Tags,结构体标签是附加在结构体字段后面的字符串,它们提供了在序列化和反序列化过程中如何处理字段的额外信息。结构体标签通常由一个或多个键值对组成,键和值之间用逗号分隔,键值对之间用空格分隔。在处理JSON时,json键用于指定JSON字段的名称和其他选项。

常用结构体标签选项

  • json:“name”: 指定JSON字段的名称。如果省略,则默认使用结构体字段的名称。
  • omitempty: 如果字段的值为空(例如,字符串为空、数字为0、布尔值为false、指针或接口为nil),则在序列化时省略该字段。这个选项通常与json:"name"一起使用,如json:“name,omitempty”
  • -: 表示该字段在序列化和反序列化过程中应该被忽略

示例

type Person struct {
	Name    string `json:"name"`
	Age     int    `json:"age"`
	Address string `json:"address,omitempty"`
}

func main() {
	p := Person{
		Name:    "张三",
		Age:     30,
		Address: "",
	}

	// 序列化
	data, err := json.Marshal(p)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(data)) // 输出:{"name":"张三","age":30}

	// 反序列化
	jsonData := []byte(`{"name":"李四","age":25,"address":"北京"}`)
	var p2 Person
	err = json.Unmarshal(jsonData, &p2)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%+v\n", p2) // 输出:{Name:李四 Age:25 Address:北京}
}

在这个示例中,我们定义了一个Person结构体,其中包含三个字段,每个字段都有自己的结构体标签。在序列化时,由于Address字段的值为空,并且标签中包含了omitempty选项,所以该字段被省略了。在反序列化时,JSON数据中的name、age和address键被映射回结构体中的Name、Age和Address字段

在go1.18及之后的版本,引入了泛型的概念,可以通过泛型来编写更灵活和可重用的代码。创建能够处理任何类型的JSON数据的函数

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// 泛型函数,用于解析JSON到任意类型的对象
func ParseJSON[T any](data []byte, obj T) (T, error) {
	err := json.Unmarshal(data, &obj)
	if err != nil {
		return obj, err
	}
	return obj, nil
}

type Person struct {
	Name  string `json:"name"`
	Age   int    `json:"age"`
	Email string `json:"email"`
}

func main() {
	// JSON数据
	jsonData := []byte(`{
		"name": "张三",
		"age": 30,
		"email": "zhangsan@example.com"
	}`)

	// 使用泛型函数解析JSON
	var person Person
	person, err := ParseJSON[Person](jsonData, person)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", person) // 输出:{Name:张三 Age:30 Email:zhangsan@example.com}
}

在这个示例中,我们定义了一个泛型函数 ParseJSON,它接受任意类型的对象作为参数,并将JSON数据解析到该对象中。我们使用类型参数 T 来表示任意类型,并通过泛型函数来解析JSON数据到一个 Person 对象中。

接口映射

结构体映射,比较适合固定和已知的JSON结构,我们可以看上面代码中使用的例子

{
	"name":"张三",
	"age":30
}

其对应的结构体

type Person struct {
	Name    string `json:"name"`
	Age     int    `json:"age"`
}

如果多添加一个address后

{
	"name":"李四", 
	"age": 25,
 	"address":"北京"
}

上面的结构体显然无法解析,当然也可以添加一个address字段来解析,但这需要建立在完全了解接收的JSON数据的结构才可以,有很多情况我们可能并不能完全确定数据结构,需要更灵活的方式来处理,一般会使用到接口(interface{})

interface{} 类型在Go中是一个空接口,它可以表示任何类型。这意味着你可以使用 interface{} 来解析任何JSON数据,而不需要预先定义一个具体的结构体。这在处理未知JSON结构或动态字段时非常有用。

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	// JSON数据
	jsonData := []byte(`{
		"name": "张三",
		"age": 30,
		"email": "zhangsan@example.com"
	}`)

	// 使用map[string]interface{}来解析JSON
	var result map[string]interface{}
	err := json.Unmarshal(jsonData, &result)
	if err != nil {
		log.Fatal(err)
	}

	// 访问解析后的数据
	name, ok := result["name"].(string)
	if ok {
		fmt.Println("Name:", name)
	}

	age, ok := result["age"].(float64)
	if ok {
		fmt.Println("Age:", age)
	}

	email, ok := result["email"].(string)
	if ok {
		fmt.Println("Email:", email)
	}
}

说明:可以看到在获取数据时使用了类型转换的句子来断言现在获取的类型name, ok := result["name"].(string),这里ok返回的是bool类型,表示是否存在这个name

在这个示例中,我们使用 map[string]interface{} 来解析JSON数据。这允许我们动态地访问JSON对象中的字段,而不需要预先定义一个结构体。我们通过类型断言来获取字段的特定类型值。

设计RESTful API的原则

RESTful API是一种遵循REST(Representational State Transfer)架构风格的网络应用程序接口。REST是一种设计网络服务的架构风格,它定义了一组用于创建分布式系统的原则和约束。RESTful API通常使用HTTP协议作为底层传输协议,并利用HTTP的方法(如GET、POST、PUT、DELETE等)来对资源进行操作

设计RESTful API的时候,一般遵循一定的原则:

  • 资源导向:RESTful API应该以资源为中心,每个资源都应有唯一的URL标识。资源通常与数据库中的表或实体对应
  • 无状态:API应该是无状态的,这意味着每个请求都应该包含所有必要的信息,服务器不应该存储任何关于客户端状态的信息。
  • 统一接口:API应该有一套统一的接口,使用标准的HTTP方法(如GET、POST、PUT、DELETE等)来进行资源的CRUD(创建、读取、更新、删除)操作。
  • URL命名规范:URL应该清晰地反映资源结构,使用名词而不是动词,并且应该使用复数形式。
  • 状态码使用:应该使用适当的HTTP状态码来表示请求的结果,例如200表示成功,201表示创建成功,404表示未找到资源等。
  • 数据格式:API应该返回结构化的数据格式,通常是JSON或XML。JSON由于其在Web中的普遍使用和轻量级特性,通常是首选。
  • 过滤、排序和分页:提供参数来允许客户端对返回的数据进行过滤、排序和分页,以便于用户根据需要获取数据。
  • 错误处理:应该提供有用的错误信息,以便于开发者调试和理解问题。错误信息应该清晰、具体,并且不应该暴露后端实现的细节。
  • 安全性:API应该考虑到安全性,使用HTTPS来保护数据传输,使用适当的认证和授权机制来保护资源。
  • 限流和缓存:为了防止滥用和提高性能,应该实现限流和缓存机制。
  • 版本控制:随着API的发展,可能需要更改现有的API。为了不破坏现有的客户端,应该实现版本控制,以便于平滑过渡。
  • 文档:提供详尽的文档是至关重要的,它应该包括API的每个端点的详细描述、参数、请求和响应示例等。
  • 可测试性:API应该易于测试,可以通过单元测试、集成测试和端到端测试来确保其可靠性和稳定性。
  • 可扩展性:设计时应该考虑到未来的扩展,API应该能够适应更多的资源和功能而无需大规模重构

实现和使用API

RESTful API实现

下面的简单例子我们单纯使用net/http来实现一个简单的RESTful API,包含了GET,PUT,POST,DELETE四种请求方式。如果使用现有的框架如Gin、Beego、Echo等(这些框架后面会再介绍),会更加方便,灵活,但框架也是基于net/http实现的

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
)

// 定义一个简单的产品结构体
type Product struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Price int    `json:"price"`
}

// 创建一个产品切片,模拟数据库中的数据
var products = []Product{
	{ID: 1, Name: "苹果", Price: 50},
	{ID: 2, Name: "香蕉", Price: 30},
	{ID: 3, Name: "橙子", Price: 40},
}

// 处理POST请求,创建新产品
func createProduct(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	var product Product
	_ = json.NewDecoder(r.Body).Decode(&product)

	products = append(products, product)
	json.NewEncoder(w).Encode(product)
}

// 处理GET请求,获取所有产品
func getProducts(w http.ResponseWriter, r *http.Request) {
	if r.Method != "GET" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	json.NewEncoder(w).Encode(products)
}

// 处理GET请求,获取单个产品
func getProduct(w http.ResponseWriter, r *http.Request) {
	if r.Method != "GET" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	id := r.URL.Query().Get("id")
	for _, product := range products {
		if fmt.Sprintf("%d", product.ID) == id {
			json.NewEncoder(w).Encode(product)
			return
		}
	}
	http.Error(w, "Product not found", http.StatusNotFound)
}

// 处理PUT请求,更新产品
func updateProduct(w http.ResponseWriter, r *http.Request) {
	if r.Method != "PUT" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	id, err := strconv.Atoi(r.URL.Query().Get("id"))
	if err != nil {
		http.Error(w, "Invalid product ID", http.StatusBadRequest)
		return
	}

	var product Product
	_ = json.NewDecoder(r.Body).Decode(&product)
	product.ID = id

	for i, p := range products {
		if p.ID == id {
			products[i] = product
			json.NewEncoder(w).Encode(product)
			return
		}
	}
	http.Error(w, "Product not found", http.StatusNotFound)
}

// 处理DELETE请求,删除产品
func deleteProduct(w http.ResponseWriter, r *http.Request) {
	if r.Method != "DELETE" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	id, err := strconv.Atoi(r.URL.Query().Get("id"))
	if err != nil {
		http.Error(w, "Invalid product ID", http.StatusBadRequest)
		return
	}

	for i, p := range products {
		if p.ID == id {
			products = append(products[:i], products[i+1:]...)
			break
		}
	}
	json.NewEncoder(w).Encode(map[string]string{"message": "Product deleted"})
}

func main() {
	http.HandleFunc("/products", getProducts)
	http.HandleFunc("/product", getProduct)
	http.HandleFunc("/createProduct", createProduct)
	http.HandleFunc("/updateProduct", updateProduct)
	http.HandleFunc("/deleteProduct", deleteProduct)

	log.Println("Server started on http://localhost:8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

在这个例子中,定义了5个路由以及函数

  • /product:getProducts函数处理GET请求,用于获取所有产品
  • /products:getProduct函数处理GET请求,用于获取单个产品
  • /createProduct:createProduct函数处理POST请求,用于创建新产品
  • /updateProduct:updateProduct函数处理PUT请求,用于更新产品信息
  • /deleteProduct:deleteProduct函数处理DELETE请求,用于删除产品

请求示例及结果
调用以及结果

调用RESTful API

调用RESTful API依然是使用net/http,我们在前面的章节已经有过简单介绍。在下面的例子中,我们将使用一个名为https://jsonplaceholder.typicode.com/的公开API,该API提供了一系列模拟的RESTful API端点。

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

// 定义一个请求结构体
type Request struct {
	Method  string `json:"method"`
	Url     string `json:"url"`
	Headers map[string]string `json:"headers"`
	Body    string `json:"body"`
}

// 处理GET请求
func handleGet(w http.ResponseWriter, r *http.Request) {
	// 解析请求参数
	if r.Method != "GET" {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	id := r.URL.Query().Get("id")

	// 构建请求
	request := Request{
		Method: "GET",
		Url:    fmt.Sprintf("https://jsonplaceholder.typicode.com/todos/%s", id),
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
	}

	// 发起请求
	response, err := http.NewRequest("GET", request.Url, nil)
	if err != nil {
		log.Fatal(err)
	}

	for key, value := range request.Headers {
		response.Header().Set(key, value)
	}

	client := &http.Client{}
	resp, err := client.Do(response)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	// 读取响应体
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}

	// 输出响应体
	fmt.Fprintf(w, "%s", body)
}

func main() {
	http.HandleFunc("/get", handleGet)

	log.Println("Server started on http://localhost:8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

在这个示例中,我们定义了一个Request结构体,用于存储请求的方法、URL、头信息和正文。我们创建了一个handleGet函数,它解析请求参数,构建请求,并发起GET请求。然后,我们使用ioutil.ReadAll函数读取响应体,并将其输出到客户端。

整个例子相当于创建了一个api转发,可以尝试在里面添加一些中间件将获取到的数据存储,清洗再进行优化展示。

  • 30
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
个人学习golang笔记,从各种教程中总结而来,作为入门参考。目录如下 目录 1. 入门 1 1.1. Hello world 1 1.2. 命令行参数 2 2. 程序结构 3 2.1. 类型 4 2.1.1. 命名类型(named type)与未命名类型(unamed type) 4 2.1.2. 基础类型(underlying type) 4 2.1.3. 可赋值性 5 2.1.4. 类型方法集 6 2.1.5. 类型声明 6 2.2. 变量 8 2.2.1. 变量声明 8 2.2.2. 类型零值 12 2.2.3. 指针 13 2.3. 赋值 17 2.4. 包和文件 17 2.5. 作用域 18 2.6. 语句 19 2.7. 比较运算符 20 2.8. 类型转换 21 2.9. 控制流 23 2.9.1. If 23 2.9.2. Goto 24 2.9.3. For 25 2.9.4. Switch 25 2.9.5. break语句 31 2.9.6. Continue语句 31 3. 基础数据类型 31 3.1. golang类型 31 3.2. Numeric types 32 3.3. 字符串 33 3.3.1. 什么是字符串 33 3.3.2. 字符串底层概念 35 3.3.3. 获取每个字节 38 3.3.4. Rune 39 3.3.5. 字符串的 for range 循环 40 3.3.6. 用字节切片构造字符串 41 3.3.7. 用rune切片构造字符串 42 3.3.8. 字符串的长度 42 3.3.9. 字符串是不可变的 42 3.3.10. UTF8(go圣经) 43 3.4. 常量 45 3.4.1. 常量定义 45 3.4.2. 常量类型 46 3.4.3. Iota 46 4. 组合数据类型 47 4.1. 数组 47 4.1.1. 数组概述 47 4.1.2. 数组的声明 49 4.1.3. 数组的长度 50 4.1.4. 遍历数组 50 4.1.5. 多维数组 51 4.2. 切片 52 4.2.1. 什么是切片 52 4.2.2. 切片概述 55 4.2.3. 创建一个切片 55 4.2.4. 切片遍历 57 4.2.5. 切片的修改 58 4.2.6. 切片的长度和容量 60 4.2.7. 追加切片元素 62 4.2.8. 切片的函数传递 65 4.2.9. 多维切片 66 4.2.10. 内存优化 67 4.2.11. nil slice和empty slice 69 4.2.12. For range 70 4.3. 结构 71 4.3.1. 什么是结构体? 71 4.3.2. 结构体声明 73 4.3.3. 结构体初始化 77 4.3.4. 嵌套结构体(Nested Structs) 81 4.3.5. 匿名字段 82 4.3.6. 导出结构体和字段 84 4.3.7. 结构体相等性(Structs Equality) 85 4.4. 指针类型 86 4.5. 函数 87 4.6. map 87 4.6.1. 什么是map 87 4.6.2. 声明、初始化和make 89 4.6.3. 给 map 添加元素 91 4.6.4. 获取 map 中的元素 91 4.6.5. 删除 map 中的元素 92 4.6.6. 获取 map 的长度 92 4.6.7. Map 的相等性 92 4.6.8. map的排序 92 4.7. 接口 93 4.7.1. 什么是接口? 93 4.7.2. 接口的声明与实现 96 4.7.3. 接口的实际用途 97 4.7.4. 接口的内部表示 99 4.7.5. 空接口 102 4.7.6. 类型断言 105 4.7.7. 类型选择(Type Switch) 109 4.7.8. 实现接口:指针接受者与值接受者 112 4.7.9. 实现多个接口 114 4.7.10. 接口的嵌套 116 4.7.11. 接口的零值 119 4.8. Channel 120 4.9. 类型转换 120 5. 函数 120 5.1. 函数的声明 121 5.2. 一个递归函数的例子( recursive functions) 121 5.3. 多返回值 121 5.4. 命名返回值 121 5.5. 可变函数参数 122 5.6. Defer 123 5.6.1. Defer语句介绍 123 5.6.2. Defer使用场景 128 5.7. 什么是头等(第一类)函数? 130 5.8. 匿名函数 130 5.9. 用户自定义的函数类型 132 5.10. 高阶函数(装饰器?) 133 5.10.1. 把函数作为参数,传递给其它函数 134 5.10.2. 在其它函数中返回函数 134 5.11. 闭包 135 5.12. 头等函数的实际用途 137 6. 微服务创建 140 6.1. 使用net/http创建简单的web server 140 6.2. 读写JSON 144 6.2.1. Marshal go结构到JSON 144 6.2.2. Unmarshalling JSON 到Go结构 146 7. 方法 146 7.1. 什么是方法? 146 7.2. 方法示例 146 7.3. 函数和方法区别 148 7.4. 指针接收器与值接收器 153 7.5. 那么什么时候使用指针接收器,什么时候使用值接收器? 155 7.6. 匿名字段的方法 156 7.7. 在方法中使用值接收器 与 在函数中使用值参数 157 7.8. 在方法中使用指针接收器 与 在函数中使用指针参数 159 7.9. 在非结构体上的方法 161 8. 并发入门 162 8.1. 并发是什么? 162 8.2. 并行是什么? 162 8.3. 从技术上看并发和并行 163 8.4. Go 对并发的支持 164 9. Go 协程 164 9.1. Go 协程是什么? 164 9.2. Go 协程相比于线程的优势 164 9.3. 如何启动一个 Go 协程? 165 9.4. 启动多个 Go 协程 167 10. 信道channel 169 10.1. 什么是信道? 169 10.2. 信道的声明 169 10.3. 通过信道进行发送和接收 169 10.4. 发送与接收默认是阻塞的 170 10.5. 信道的代码示例 170 10.6. 信道的另一个示例 173 10.7. 死锁 174 10.8. 单向信道 175 10.9. 关闭信道和使用 for range 遍历信道 176 11. 缓冲信道和工作池(Buffered Channels and Worker Pools) 179 11.1. 什么是缓冲信道? 179 11.2. 死锁 182 11.3. 长度 vs 容量 183 11.4. WaitGroup 184 11.5. 工作池的实现 186 12. Select 188 12.1. 什么是 select? 188 12.2. 示例 189 12.3. select 的应用 190 12.4. 默认情况 190 12.5. 死锁与默认情况 191 12.6. 随机选取 191 12.7. 这下我懂了:空 select 191 13. 文件读写 191 13.1. GoLang几种读文件方式的比较 197 14. 个人 197 14.1. ++,-- 198 14.2. 逗号 198 14.3. 未使用的变量 199 14.4. Effective go 199 14.4.1. 指针 vs. 值 199 14.5. 可寻址性-map和slice的区别 201 14.5.1. slice 201 14.5.2. map 202 14.6. golang库 203 14.6.1. unicode/utf8包 203 14.6.2. time包 205 14.6.3. Strings包 205 14.6.4. 输入输出 212 14.6.5. 正则处理 224 14.6.6. Golang内建函数 226

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值