关于指针未初始化、指针越界、指针悬挂
- 指针未初始化可能会导致程序崩溃或者出现奇怪的行为,看看下面这个例子:
package main import "fmt" type MyStruct struct { value string } func (s *MyStruct) SetValue(val string) { s.value = val } func (s *MyStruct) GetValue() string { return s.value } func main() { var myStruct *MyStruct myStruct.SetValue("hello") fmt.Println(myStruct.GetValue()) }
声明了一个指向MyStruct的指针myStruct,但是没有为它分配内存,也就是说它的值为nil。通过myStruct指针来设置结构体的值和获取结构体的值。由于myStruct指针没有初始化,它不会指向任何有效的内存地址,在SetValue和GetValue方法中会出现错误。
改造main函数即可
func main() { myStruct := &MyStruct{} myStruct.SetValue("tantianran") fmt.Println(myStruct.GetValue()) }
使用&MyStruct{}创建了一个新的结构体对象,并将其地址分配给指针myStruct。这样,指针就指向了一个有效的内存地址。
- 指针越界可能会导致程序崩溃或者出现奇怪的行为,看下面的案例:
package main import "fmt" func main() { mySlice := []int{1, 2, 3, 4, 5} myPointer := &mySlice[2] fmt.Println(*myPointer) *myPointer = 6 fmt.Println(mySlice) myPointer = &mySlice[len(mySlice)] // 超出数组范围 *myPointer = 7 fmt.Println(mySlice) }
创建了一个名为mySlice的整数切片,并且创建了一个指向切片中第三个元素的指针myPointer。然后通过指针myPointer来修改切片中的第三个元素,并且打印了修改后的切片。接着试图将指针myPointer指向超出切片范围的位置,即最后一个元素的下一个位置,然后再通过指针来修改该位置的值。这就是指针越界了,所以程序会崩溃并且抛出一个运行时错误。
现在修复这个问题,确保指针不越界:
package main import "fmt" func main() { mySlice := []int{1, 2, 3, 4, 5} myPointer := &mySlice[2] fmt.Println(*myPointer) *myPointer = 6 fmt.Println(mySlice) if len(mySlice) > 0 { myPointer = &mySlice[len(mySlice)-1] *myPointer = 7 fmt.Println(mySlice) } }
上面的代码中,我加入了一个判断语句,检查切片是否为空。如果切片不为空,就将指针指向最后一个元素,并且通过指针来修改该位置的值。这样指针就不会越界了,程序就可以正常运行啦!
- 指针悬挂是指一个指针指向已经释放的内存或者未被分配的内存区域,看下面的案例:
package main func foo(pp *int) { var x int = 10 pp = &x // pp指向x的地址,但是在函数返回时,pp指向的地址被释放了,变成了悬挂指针 } func main() { var p *int foo(p) *p = 10 // p是一个悬挂指针,此处会导致运行时错误 }
要修复指针悬挂问题,可以将foo函数修改为返回指向x的指针。这样,在main函数中,就可以使用这个指针来访问x变量,而不会出现悬挂指针的情况。下面是修改后的代码:
package main import "fmt" func foo() *int { var x int = 10 return &x // 返回指向x的指针 } func main() { var p *int p = foo() fmt.Println(*p) // 修改前 *p = 100 // 此时p指向x的地址,可以安全地修改x的值 fmt.Println(*p) // 修改后 }
使用指针的优点案例
- 假设有一个非常大的数据结构,它需要在程序中频繁地传递和操作。如果使用值传递来操作该数据结构,每次传递都会创建该数据结构的副本,这将浪费大量的时间和内存资源。使用指针传递该数据结构,只需传递指向该数据结构的内存地址,而不是复制整个数据结构。大大减少内存使用和程序运行时间:
package main import "fmt" type LargeData struct { // 一个非常大的数据结构 data [1024 * 1024 * 1024]int } func processLargeData(dt *LargeData) { // 对大型数据结构进行处理 dt.data[0] = 1 // ... } func main() { // 创建一个大型数据结构 data := LargeData{} // 传递指向该数据结构的指针进行处理 processLargeData(&data) fmt.Println(data.data[0]) // 输出 1 }
- 比如正在处理一个HTTP请求,并且需要从请求中读取和解析JSON数据,然后将其转换为一个Go结构体并进行处理。可以通过将请求体指针传递给解析函数来避免大量的内存拷贝,避免创建一个新的缓冲区将请求体数据复制到缓冲区中进行解析:
import ( "encoding/json" "net/http" ) type Person struct { Name string `json:"name"` Age int `json:"age"` } func handler(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) defer r.Body.Close() var person Person if err := decoder.Decode(&person); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } // 处理person结构体 // ... w.WriteHeader(http.StatusOK) }
Decode()函数的参数是&person,它将指向Person结构体的指针传递给解析函数。解析函数可以直接在请求体上工作,而不需要进行内存拷贝,从而提高了程序的性能。
- 打个比方,正在编写一个HTTP服务器,该服务器需要处理大量的请求并维护一个长时间运行的状态,可以使用指针来动态地分配和释放内存来存储和管理状态数据:
package main import ( "fmt" "log" "net/http" ) type ServerState struct { Count int } func main() { state := &ServerState{} // 用浏览器访问,每刷新一次就会累加 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { state.Count++ fmt.Fprintf(w, "计数: %d", state.Count) }) log.Fatal(http.ListenAndServe(":8080", nil)) // 在服务器关闭时释放状态内存 defer func() { fmt.Println("web服务器关闭...") // 释放ServerState结构体内存 if state != nil { state = nil } }() }
访问效果:
上面的代码中,使用&ServerState{}来动态地分配内存来存储服务器状态。在处理请求时,使用指针state来更新和访问状态数据。在服务器关闭时,使用指针state来释放状态内存。这就方便地管理程序的内存,也避免了内存泄漏和资源浪费的问题。所以说,使用指针来动态分配和释放内存,这在处理大量数据或长时间运行的服务时就显得尤为重要啦。