Right way to implement mongo go driver interfaces - mongodb

I'm trying to write interfaces for mongo go driver to write unit test for my post_repo.go, but have this error:
cannot use redditclone (type *mongo.Database) as type newPosts.DatabaseHelper in argument to newPosts.NewPostsDatabase:
*mongo.Database does not implement newPosts.DatabaseHelper (wrong type for Client method)
have Client() *mongo.Client
want Client() newPosts.ClientHelper
my interface file, took it from this post https://medium.com/better-programming/unit-testing-code-using-the-mongo-go-driver-in-golang-7166d1aa72c0:
package newPosts
import (
"context"
"rc/pkg/config"
// "localhost/medium-mongo-go-driver/config"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type DatabaseHelper interface {
Collection(name string) CollectionHelper
Client() ClientHelper
}
type CollectionHelper interface {
FindOne(context.Context, interface{}) SingleResultHelper
InsertOne(context.Context, interface{}) (interface{}, error)
DeleteOne(ctx context.Context, filter interface{}) (int64, error)
}
type SingleResultHelper interface {
Decode(v interface{}) error
}
type ClientHelper interface {
Database(string) DatabaseHelper
Connect() error
StartSession() (mongo.Session, error)
}
type mongoClient struct {
cl *mongo.Client
}
type mongoDatabase struct {
db *mongo.Database
}
type mongoCollection struct {
coll *mongo.Collection
}
type mongoSingleResult struct {
sr *mongo.SingleResult
}
type mongoSession struct {
mongo.Session
}
func NewClient(cnf *config.Config) (ClientHelper, error) {
c, err := mongo.NewClient(options.Client().SetAuth(
options.Credential{
Username: cnf.Username,
Password: cnf.Password,
AuthSource: cnf.DatabaseName,
}).ApplyURI(cnf.URL))
return &mongoClient{cl: c}, err
}
func NewDatabase(cnf *config.Config, client ClientHelper) DatabaseHelper {
return client.Database(cnf.DatabaseName)
}
func (mc *mongoClient) Database(dbName string) DatabaseHelper {
db := mc.cl.Database(dbName)
return &mongoDatabase{db: db}
}
func (mc *mongoClient) StartSession() (mongo.Session, error) {
session, err := mc.cl.StartSession()
return &mongoSession{session}, err
}
func (mc *mongoClient) Connect() error {
// mongo client does not use context on connect method. There is a ticket
// with a request to deprecate this functionality and another one with
// explanation why it could be useful in synchronous requests.
// https://jira.mongodb.org/browse/GODRIVER-1031
// https://jira.mongodb.org/browse/GODRIVER-979
return mc.cl.Connect(nil)
}
func (md *mongoDatabase) Collection(colName string) CollectionHelper {
collection := md.db.Collection(colName)
return &mongoCollection{coll: collection}
}
func (md *mongoDatabase) Client() ClientHelper {
client := md.db.Client()
return &mongoClient{cl: client}
}
func (mc *mongoCollection) FindOne(ctx context.Context, filter interface{}) SingleResultHelper {
singleResult := mc.coll.FindOne(ctx, filter)
return &mongoSingleResult{sr: singleResult}
}
func (mc *mongoCollection) InsertOne(ctx context.Context, document interface{}) (interface{}, error) {
id, err := mc.coll.InsertOne(ctx, document)
return id.InsertedID, err
}
func (mc *mongoCollection) DeleteOne(ctx context.Context, filter interface{}) (int64, error) {
count, err := mc.coll.DeleteOne(ctx, filter)
return count.DeletedCount, err
}
func (sr *mongoSingleResult) Decode(v interface{}) error {
return sr.sr.Decode(v)
}
my post_repo.go file:
package newPosts
import (
"context"
// "localhost/medium-mongo-go-driver/models"
)
const collectionName = "posts"
// PostsDatabase user database representation to find, update, delete users
type PostsDatabase interface {
FindOne(context.Context, interface{}) (*Post, error)
Create(context.Context, *Post) error
DeleteByUsername(context.Context, string) error
}
type postsDatabase struct {
// db *mongo.Database
db DatabaseHelper
}
// NewPostsDatabase creates new user database instance
func NewPostsDatabase(db DatabaseHelper) PostsDatabase {
return &postsDatabase{
db: db,
}
}
func (u *postsDatabase) FindOne(ctx context.Context, filter interface{}) (*Post, error) {
user := &Post{}
err := u.db.Collection(collectionName).FindOne(ctx, filter).Decode(user)
if err != nil {
return nil, err
}
return user, nil
}
func (u *postsDatabase) Create(ctx context.Context, usr *Post) error {
_, err := u.db.Collection(collectionName).InsertOne(ctx, usr)
return err
}
func (u *postsDatabase) DeleteByUsername(ctx context.Context, username string) error {
// In this case it is possible to use bson.M{"username":username} but I tend
// to avoid another dependency in this layer and for demonstration purposes
// used omitempty in the model
author := &Author{Username: username}
post := &Post{
Author: author,
}
_, err := u.db.Collection(collectionName).DeleteOne(ctx, post)
return err
}
My posts_repo_test.go tests works fine, but only if I pass newPosts.DatabaseHelper in function newPosts.NewPostsDatabase(). I guess I need to pass *mongo.Database for my program works.
Maybe I misunderstanding how works unit tests.
Any help

Related

(Golang) InvalidBSON when using custom MarshalBSON()

I am currently migrating some of my Python Services to GoLang, where I encountered some problems with the integration with the existing database.
When it comes to saving, the formatting of the date string is different, and I am not planning to change the format of my database entries.
So I decided to implement a time type, with custom (Un)MarshalBSON() methods, to keep pythons time formatting.
However when saving it, I get the following error:
2022/09/10 20:57:29 (InvalidBSON) Unrecognized BSON type 45 in element with field name 'created_at.09-10 20:57:27.798545' in object with _id: ObjectId('631cde192c2aad6a49bc52af')
I don't know how the field created_at.09-10 20:57:27.798545 came up.
Thank you for advance four your advice, here is my source code:
main.go:
package main
import (
"context"
"example/customdate/user"
"fmt"
"log"
)
func main() {
ctx := context.Background()
repo := user.NewRepository(ctx)
u1 := user.NewUser("Max")
u1, err := repo.Add(ctx, u1)
if err != nil {
log.Fatalln(err)
}
fmt.Println(u1)
}
user/repository.go:
package user
import (
"context"
"log"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type Repository interface {
Get(ctx context.Context) ([]User, error)
Add(ctx context.Context, user *User) (*User, error)
}
type repository struct {
client *mongo.Client
}
func NewRepository(ctx context.Context) repository {
c, err := mongo.NewClient(options.Client().ApplyURI("mongodb://127.0.0.1:27017/"))
if err != nil {
log.Fatalln(err)
}
err = c.Connect(ctx)
if err != nil {
log.Fatal(err)
}
return repository{
client: c,
}
}
func (r *repository) Get(ctx context.Context) ([]User, error) {
result, err := r.client.Database("db").Collection("user").Find(ctx, map[string]interface{}{})
if err != nil {
return nil, err
}
var users []User
if err = result.All(ctx, &users); err != nil {
return nil, err
}
return users, nil
}
func (r *repository) Add(ctx context.Context, user *User) (*User, error) {
result, err := r.client.Database("db").Collection("user").InsertOne(ctx, user)
if err != nil {
return nil, err
}
user.ID = result.InsertedID
return user, nil
}
user/dto.go:
package user
import "time"
type CustomTime struct {
time.Time
}
func (t *CustomTime) MarshalBSON() ([]byte, error) {
str := t.Format("2006-01-02 15:04:05.999999")
return []byte(str), nil
}
func (t *CustomTime) UnmarshalBSON(raw []byte) error {
parsed, err := time.Parse("2006-01-02 15:04:05.999999", string(raw))
if err != nil {
return err
}
t = &CustomTime{parsed}
return nil
}
func Now() CustomTime {
return CustomTime{time.Now()}
}
type User struct {
ID interface{}
Name string `bson:"name"`
CreatedAt CustomTime `bson:"created_at"`
}
func NewUser(name string) *User {
return &User{
Name: name,
CreatedAt: Now(),
}
}

how to create a mongo db package

I'd like to build an infrastructure, which is a package for the project. So that other developers can import this package to perform CRUD operations on the DB.
But I've got an error during the test:
type Students struct {
Name string
Age int
}
type InsertOneResult struct {
InsertedID interface{}
}
func dbGetOne(coll, document interface{}) (*InsertOneResult, error) {
...
}
func dbUpdateOne(coll, document interface{}) (*InsertOneResult, error) {
...
}
func dbDeleteOne(coll, document interface{}) (*InsertOneResult, error) {
...
}
func dbInsertOne(coll, document interface{}) (*InsertOneResult, error) {
res, err := coll.InsertOne(context.TODO(), document)
if err != nil {
log.Fatal(err)
}
return &InsertOneResult{InsertedID: res[0]}, err
}
func main() {
client, err := mongo.NewClient(options.Client().ApplyURI("mongodb://<user>:<password>#<host>:<port>/<dbname>"))
if err != nil {
log.Fatal(err)
}
ctx, _ := context.WithTimeout(context.Background(), 30*time.Second)
err = client.Connect(ctx)
if err != nil {
log.Fatal(err)
}
coll := client.Database("db").Collection("students")
data := Students{"Amy", 10}
res, err := dbInsertOne(coll, data)
if err != nil {
log.Fatal(err)
}
fmt.Printf("inserted document with ID %v\n", res.InsertedID)
}
Here's the error:
./main.go:24:18: coll.InsertOne undefined (type interface {} is interface with no methods)
Is there any way to solve this? Thanks in advance.
Hey it looks like the error could be coming from a type conversion issue. The solution would be to clearly define the type for coll as *mongo.Collection in the dbInsertOne() function. This allows the compiler at compile time to figure out the structure of the input instead of having to rely on an abstract interface.
func dbInsertOne(coll *mongo.Collection, document interface{}) (*InsertOneResult, error) {
res, err := coll.InsertOne(context.TODO(), document)
if err != nil {
log.Fatal(err)
}
return &InsertOneResult{InsertedID: res.InsertedID}, err
}
I would further suggest that the 2nd argument document should also be a typed known term if possible. e.g.
func dbInsertOne(coll *mongo.Collection, document Students)
Static typing will help quite a bit and clear up any confusion.

Using mgo with context

I have been using mgo for my API but I'm seeing many current connections in my MongoDB (while using less than 5 devices for testing). By executing db.serverStatus().connections in my Mongo server I get: { "current" : 641, "available" : 838219, "totalCreated" : 1136 }. Below I transcript my issue in mgo's Github (Issue #429):
Is my way of using mgo in a web server the correct way? If not, can you give me a full example?
This code is not functional, take it as almost pseudo code (because of the missing parts like the imports or where the configs come from and models), but it is exactly how I am using mgo.
I must clarify that I'm building an API which is used by several mobile devices and webapps.
main.go
func main() {
mongoDBDialInfo := &mgo.DialInfo{
Addrs: []string{config.DatabaseURL},
Timeout: 60 * time.Second,
Database: config.DatabaseName,
Username: config.DatabaseUsername,
Password: config.DatabasePassword,
}
db, err := mgo.DialWithInfo(mongoDBDialInfo)
if err != nil {
log.Fatal("Cannot Dial Mongo: ", err)
}
defer db.Close()
db.SetMode(mgo.Monotonic, true)
phoneIndex := mgo.Index{
Key: []string{"pp"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
err = db.DB(config.DatabaseName).C("users").EnsureIndex(phoneIndex)
if err != nil {
panic(err)
}
router := mux.NewRouter()
router.HandleFunc("/login", publicWithDB(login, db)).Methods("POST")
if err := http.ListenAndServe(":5000", router); err != nil {
log.Fatal(err)
}
}
func publicWithDB(fn http.HandlerFunc, db *mgo.Session) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
dbsession := db.Copy()
defer dbsession.Close()
fn(w, r.WithContext(context.WithValue(r.Context(), contextKeyDatabase, dbsession)))
}
}
func login(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // Parses the request body
device := r.Form.Get("device")
var deviceid bson.ObjectId
if bson.IsObjectIdHex(device) {
deviceid = bson.ObjectIdHex(device)
}
db := r.Context().Value(contextKeyDatabase).(*mgo.Session)
var device models.Device
err := db.DB(config.DatabaseName).C("devices").FindId(deviceid).One(&device)
w.WriteHeader(200)
w.Write([]byte(utils.ResponseToString(models.Response{Status: 200, Message: "asdasd", Data: device})))
}
I'm posting this because I couldn't find a complete implementation.
Here's an example of how I have seen myself and others structure web apps in Go. This code is untested and is purely for example. It's missing imports and potentially has errors.
EDIT Added a middleware example.
main.go
package main
func main() {
mongoDBDialInfo := &mgo.DialInfo{
Addrs: []string{config.DatabaseURL},
Timeout: 60 * time.Second,
Database: config.DatabaseName,
Username: config.DatabaseUsername,
Password: config.DatabasePassword,
}
db, err := mgo.DialWithInfo(mongoDBDialInfo)
if err != nil {
log.Fatal("Cannot Dial Mongo: ", err)
}
defer db.Close()
db.SetMode(mgo.Monotonic, true)
phoneIndex := mgo.Index{
Key: []string{"pp"},
Unique: true,
DropDups: true,
Background: true,
Sparse: true,
}
err = db.DB(config.DatabaseName).C("users").EnsureIndex(phoneIndex)
if err != nil {
panic(err)
}
mgoAdapter := mongo.NewAdapter(db, config.DatabaseName)
deviceStore := mongo.NewDeviceStore(mgoAdapter)
userStore := mongo.NewUserStore(mgoAdapter)
loginController := controllers.NewLoginController(deviceStore)
router := mux.NewRouter()
router.HandleFunc("/login", middleware.AuthorizeUser(userStore)(http.HandlerFunc(loginController.Login)).Methods("POST")
if err := http.ListenAndServe(":5000", router); err != nil {
log.Fatal(err)
}
}
controllers/login.go
package controllers
type LoginController struct {
store DeviceStore
}
func NewLoginController(store stores.DeviceStore) *LoginController {
return &LoginController{
store: store,
}
}
func (c *LoginController) Login(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // Parses the request body
device := r.Form.Get("device")
data, err := c.store.FindByDevice(device)
var respose models.Response
if err != nil {
w.WriteHeader(500)
response = models.Response{Status: 500, Message: fmt.Sprintf("error: %s", err)}
} else if data == nil {
w.WriteHeader(404)
response = models.Response{Status: 404, Message: "device not found"}
} else {
response = models.Response{Status: 200, Message: "device found", Data: data}
}
// Write sets header to 200 if it hasn't been set already
w.Write([]byte(utils.ResponseToString(response)))
}
stores/stores.go
package stores
type DeviceStore interface {
FindByDevice(device string) (*models.Device, error)
}
type UserStore interface {
FindByToken(token string) (*models.User, error)
}
middleware/auth.go
package middleware
func AuthorizeUser(store stores.UserStore) func(h *http.Handler) http.Handler {
return func(h *http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Logic for authorizing user
// Could also store user in the request context
})
}
}
mongo/mongo.go
package mongo
type Adapter struct {
session *mgo.Session
databaseName string
}
func NewAdapter(session *mgo.Session, dbName string) *Adapter {
return &Adapter{session: session, databaseName: dbName}
}
type deviceStore struct {
*Adapter
}
func NewDeviceStore(adapter *Adapter) stores.DeviceStore {
return &deviceStore{adapter}
}
const devices = "devices"
func (s *deviceStore) FindByDevice(d string) (*models.Device, err) {
sess := s.session.copy()
defer sess.close()
var deviceID bson.ObjectId
if bson.IsObjectIdHex(d) {
deviceID = bson.ObjectIdHex(d)
}
var device models.Device
err := db.DB(s.databaseName).C(devices).FindId(deviceID).One(&device)
if err == mgo.ErrNotFound {
return nil, nil
}
return &device, err
}
type userStore struct {
*Adapter
}
const users = "users"
func NewUserStore(adapter *Adapter) stores.UserStore {
return &userStore{adapter}
}
func (s *userStore) GetUserByToken(token string) (*models.User, error) {
sess := s.session.copy()
defer sess.close()
var user models.User
err := db.DB(s.databaseName).C(users).Find(bson.M{"token": token}).One(&user)
if err == mgo.ErrNotFound {
return nil, nil
}
return &user, err
}

Golang Mongodb %!(EXTRA

I'm trying to marshal a struct into JSON and then insert it into my Mongo database, but keep on getting this error: %!(EXTRA main.Test={575590180 Me}). What am I doing wrong? I took this code exactly from another project I worked on which could insert documents without any problems.
package main
import (
"utils"
"hash/fnv"
"log"
"gopkg.in/mgo.v2"
"encoding/json"
)
type Test struct {
Id uint32
Name string
}
func ConnectDB() *mgo.Session {
session, err := mgo.Dial("localhost:27017")
if err != nil {
panic(err)
}
return session
}
func SaveMgoDoc(dbName string, collectionName string, file Test) bool {
session, err := mgo.Dial("localhost:27017")
if err != nil {
panic(err)
}
defer session.Close()
fileJson, err := json.Marshal(file)
if err != nil {
log.Printf("failed to marshal struct to json...\n", file)
return false
}
collection := session.DB(dbName).C(collectionName)
err = collection.Insert(&fileJson)
if err != nil {
log.Printf("failed to insert doc into database...\n", file)
return false
}
return true
}
func hash(s string) uint32 {
h := fnv.New32a()
h.Write([]byte(s))
return h.Sum32()
}
func main() {
utils.SaveMgoDoc("mydb", "mydoc", Test{hash("Me"), "Me"})
}
Insert expects a pointer to a struct, not a json string. So, in this case, just use:
err = collection.Insert(&file)

Accessing the underlying socket of a net/http response

I'm new to Go and evaluating it for a project.
I'm trying to write a custom handler to serve files with net/http.
I can't use the default http.FileServer() handler because I need to have access to the underlying socket (the internal net.Conn) so I can perform some informational platform specific "syscall" calls on it (mainly TCP_INFO).
More precisly: I need to access the underlying socket of the http.ResponseWriter in the handler function:
func myHandler(w http.ResponseWriter, r *http.Request) {
...
// I need the net.Conn of w
...
}
used in
http.HandleFunc("/", myHandler)
Is there a way to this. I looked at how websocket.Upgrade does this but it uses Hijack() which is 'too much' because then I have to code 'speaking http' over the raw tcp socket I get. I just want a reference to the socket and not taking over completely.
After Issue #30694 is completed, it looks like Go 1.13 will probably support storing the net.Conn in the Request Context, which makes this fairly clean and simple:
package main
import (
"net/http"
"context"
"net"
"log"
)
type contextKey struct {
key string
}
var ConnContextKey = &contextKey{"http-conn"}
func SaveConnInContext(ctx context.Context, c net.Conn) (context.Context) {
return context.WithValue(ctx, ConnContextKey, c)
}
func GetConn(r *http.Request) (net.Conn) {
return r.Context().Value(ConnContextKey).(net.Conn)
}
func main() {
http.HandleFunc("/", myHandler)
server := http.Server{
Addr: ":8080",
ConnContext: SaveConnInContext,
}
server.ListenAndServe()
}
func myHandler(w http.ResponseWriter, r *http.Request) {
conn := GetConn(r)
...
}
Until then ... For a server listening on a TCP port, net.Conn.RemoteAddr().String() is unique for each connection and is available to the http.Handler as r.RemoteAddr, so it can be used as a key to a global map of Conns:
package main
import (
"net/http"
"net"
"fmt"
"log"
)
var conns = make(map[string]net.Conn)
func ConnStateEvent(conn net.Conn, event http.ConnState) {
if event == http.StateActive {
conns[conn.RemoteAddr().String()] = conn
} else if event == http.StateHijacked || event == http.StateClosed {
delete(conns, conn.RemoteAddr().String())
}
}
func GetConn(r *http.Request) (net.Conn) {
return conns[r.RemoteAddr]
}
func main() {
http.HandleFunc("/", myHandler)
server := http.Server{
Addr: ":8080",
ConnState: ConnStateEvent,
}
server.ListenAndServe()
}
func myHandler(w http.ResponseWriter, r *http.Request) {
conn := GetConn(r)
...
}
For a server listening on a UNIX socket, net.Conn.RemoteAddr().String() is always "#", so the above doesn't work. To make this work, we can override net.Listener.Accept(), and use that to override net.Conn.RemoteAddr().String() so that it returns a unique string for each connection:
package main
import (
"net/http"
"net"
"os"
"golang.org/x/sys/unix"
"fmt"
"log"
)
func main() {
http.HandleFunc("/", myHandler)
listenPath := "/var/run/go_server.sock"
l, err := NewUnixListener(listenPath)
if err != nil {
log.Fatal(err)
}
defer os.Remove(listenPath)
server := http.Server{
ConnState: ConnStateEvent,
}
server.Serve(NewConnSaveListener(l))
}
func myHandler(w http.ResponseWriter, r *http.Request) {
conn := GetConn(r)
if unixConn, isUnix := conn.(*net.UnixConn); isUnix {
f, _ := unixConn.File()
pcred, _ := unix.GetsockoptUcred(int(f.Fd()), unix.SOL_SOCKET, unix.SO_PEERCRED)
f.Close()
log.Printf("Remote UID: %d", pcred.Uid)
}
}
var conns = make(map[string]net.Conn)
type connSaveListener struct {
net.Listener
}
func NewConnSaveListener(wrap net.Listener) (net.Listener) {
return connSaveListener{wrap}
}
func (self connSaveListener) Accept() (net.Conn, error) {
conn, err := self.Listener.Accept()
ptrStr := fmt.Sprintf("%d", &conn)
conns[ptrStr] = conn
return remoteAddrPtrConn{conn, ptrStr}, err
}
func GetConn(r *http.Request) (net.Conn) {
return conns[r.RemoteAddr]
}
func ConnStateEvent(conn net.Conn, event http.ConnState) {
if event == http.StateHijacked || event == http.StateClosed {
delete(conns, conn.RemoteAddr().String())
}
}
type remoteAddrPtrConn struct {
net.Conn
ptrStr string
}
func (self remoteAddrPtrConn) RemoteAddr() (net.Addr) {
return remoteAddrPtr{self.ptrStr}
}
type remoteAddrPtr struct {
ptrStr string
}
func (remoteAddrPtr) Network() (string) {
return ""
}
func (self remoteAddrPtr) String() (string) {
return self.ptrStr
}
func NewUnixListener(path string) (net.Listener, error) {
if err := unix.Unlink(path); err != nil && !os.IsNotExist(err) {
return nil, err
}
mask := unix.Umask(0777)
defer unix.Umask(mask)
l, err := net.Listen("unix", path)
if err != nil {
return nil, err
}
if err := os.Chmod(path, 0660); err != nil {
l.Close()
return nil, err
}
return l, nil
}
Note that although in current implementation http.ResponseWriter is a *http.response (note the lowercase!) which holds the connection, the field is unexported and you can't access it.
Instead take a look at the Server.ConnState hook: you can "register" a function which will be called when the connection state changes, see http.ConnState for details. For example you will get the net.Conn even before the request enters the handler (http.StateNew and http.StateActive states).
You can install a connection state listener by creating a custom Server like this:
func main() {
http.HandleFunc("/", myHandler)
s := &http.Server{
Addr: ":8081",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
ConnState: ConnStateListener,
}
panic(s.ListenAndServe())
}
func ConnStateListener(c net.Conn, cs http.ConnState) {
fmt.Printf("CONN STATE: %v, %v\n", cs, c)
}
This way you will have exactly the desired net.Conn even before (and also during and after) invoking the handler. The downside is that it is not "paired" with the ResponseWriter, you have to do that manually if you need that.
You can use an HttpHijacker to take over the TCP connection from the ResponseWriter. Once you've done that you're free to use the socket to do whatever you want.
See http://golang.org/pkg/net/http/#Hijacker, which also contains a good example.
This can be done with reflection. it's a bit "dirty" but it works:
package main
import "net/http"
import "fmt"
import "runtime"
import "reflect"
func myHandler(w http.ResponseWriter, r *http.Request) {
ptrVal := reflect.ValueOf(w)
val := reflect.Indirect(ptrVal)
// w is a "http.response" struct from which we get the 'conn' field
valconn := val.FieldByName("conn")
val1 := reflect.Indirect(valconn)
// which is a http.conn from which we get the 'rwc' field
ptrRwc := val1.FieldByName("rwc").Elem()
rwc := reflect.Indirect(ptrRwc)
// which is net.TCPConn from which we get the embedded conn
val1conn := rwc.FieldByName("conn")
val2 := reflect.Indirect(val1conn)
// which is a net.conn from which we get the 'fd' field
fdmember := val2.FieldByName("fd")
val3 := reflect.Indirect(fdmember)
// which is a netFD from which we get the 'sysfd' field
netFdPtr := val3.FieldByName("sysfd")
fmt.Printf("netFDPtr= %v\n", netFdPtr)
// which is the system socket (type is plateform specific - Int for linux)
if runtime.GOOS == "linux" {
fd := int(netFdPtr.Int())
fmt.Printf("fd = %v\n", fd)
// fd is the socket - we can call unix.Syscall6(unix.SYS_GETSOCKOPT, uintptr(fd),....) on it for instance
}
fmt.Fprintf(w, "Hello World")
}
func main() {
http.HandleFunc("/", myHandler)
err := http.ListenAndServe(":8081", nil)
fmt.Println(err.Error())
}
Ideally the library should be augmented with a method to get the underlying net.Conn
Expanding on KGJV's answer, a working solution using reflection to maintain a map of connections indexed by net.Conn instance memory addresses.
Instances of net.Conn can be looked up by pointer, and pointers derived using reflection against http.Response.
It's a bit nasty, but given you can't access unpublished fields with reflection it's the only way I could see of doing it.
// Connection array indexed by connection address
var conns = make(map[uintptr]net.Conn)
var connMutex = sync.Mutex{}
// writerToConnPrt converts an http.ResponseWriter to a pointer for indexing
func writerToConnPtr(w http.ResponseWriter) uintptr {
ptrVal := reflect.ValueOf(w)
val := reflect.Indirect(ptrVal)
// http.conn
valconn := val.FieldByName("conn")
val1 := reflect.Indirect(valconn)
// net.TCPConn
ptrRwc := val1.FieldByName("rwc").Elem()
rwc := reflect.Indirect(ptrRwc)
// net.Conn
val1conn := rwc.FieldByName("conn")
val2 := reflect.Indirect(val1conn)
return val2.Addr().Pointer()
}
// connToPtr converts a net.Conn into a pointer for indexing
func connToPtr(c net.Conn) uintptr {
ptrVal := reflect.ValueOf(c)
return ptrVal.Pointer()
}
// ConnStateListener bound to server and maintains a list of connections by pointer
func ConnStateListener(c net.Conn, cs http.ConnState) {
connPtr := connToPtr(c)
connMutex.Lock()
defer connMutex.Unlock()
switch cs {
case http.StateNew:
log.Printf("CONN Opened: 0x%x\n", connPtr)
conns[connPtr] = c
case http.StateClosed:
log.Printf("CONN Closed: 0x%x\n", connPtr)
delete(conns, connPtr)
}
}
func HandleRequest(w http.ResponseWriter, r *http.Request) {
connPtr := writerToConnPtr(w)
connMutex.Lock()
defer connMutex.Unlock()
// Requests can access connections by pointer from the responseWriter object
conn, ok := conns[connPtr]
if !ok {
log.Printf("error: no matching connection found")
return
}
// Do something with connection here...
}
// Bind with http.Server.ConnState = ConnStateListener
It looks like you cannot "pair" a socket (or net.Conn) to either http.Request or http.ResponseWriter.
But you can implement your own Listener:
package main
import (
"fmt"
"net"
"net/http"
"time"
"log"
)
func main() {
// init http server
m := &MyHandler{}
s := &http.Server{
Handler: m,
}
// create custom listener
nl, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
l := &MyListener{nl}
// serve through custom listener
err = s.Serve(l)
if err != nil {
log.Fatal(err)
}
}
// net.Conn
type MyConn struct {
nc net.Conn
}
func (c MyConn) Read(b []byte) (n int, err error) {
return c.nc.Read(b)
}
func (c MyConn) Write(b []byte) (n int, err error) {
return c.nc.Write(b)
}
func (c MyConn) Close() error {
return c.nc.Close()
}
func (c MyConn) LocalAddr() net.Addr {
return c.nc.LocalAddr()
}
func (c MyConn) RemoteAddr() net.Addr {
return c.nc.RemoteAddr()
}
func (c MyConn) SetDeadline(t time.Time) error {
return c.nc.SetDeadline(t)
}
func (c MyConn) SetReadDeadline(t time.Time) error {
return c.nc.SetReadDeadline(t)
}
func (c MyConn) SetWriteDeadline(t time.Time) error {
return c.nc.SetWriteDeadline(t)
}
// net.Listener
type MyListener struct {
nl net.Listener
}
func (l MyListener) Accept() (c net.Conn, err error) {
nc, err := l.nl.Accept()
if err != nil {
return nil, err
}
return MyConn{nc}, nil
}
func (l MyListener) Close() error {
return l.nl.Close()
}
func (l MyListener) Addr() net.Addr {
return l.nl.Addr()
}
// http.Handler
type MyHandler struct {
// ...
}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}